@@ -49,6 +49,44 @@ type Options struct {
4949 Stderr io.Writer
5050}
5151
52+ // buildAction is how Run obtains the image to run when the desired image may
53+ // not already exist.
54+ type buildAction int
55+
56+ const (
57+ actionUseDesired buildAction = iota // desired image is cached; run it
58+ actionSyncBuild // build the desired image synchronously, then run it
59+ actionUseFallback // run an older fallback image now; build desired in background
60+ )
61+
62+ // buildPlan is the outcome of planBuild: what to run, and which image (if any)
63+ // to build in the background.
64+ type buildPlan struct {
65+ action buildAction
66+ bgBuildTag string // image tag to build in the background ("" = none)
67+ }
68+
69+ // planBuild decides how Run should obtain a runnable image given cache state.
70+ //
71+ // The fallback case is the subtle one: when the desired image is absent but a
72+ // structurally-identical older image exists (only the tool version differs),
73+ // Run uses the old image immediately and must launch a background build for the
74+ // desired image. Otherwise — since the cached version already equals the latest
75+ // fetched version — nothing else would ever build it and Run would fall back to
76+ // the old image forever.
77+ func planBuild (imageTag string , imageExists , explicitRebuild , fallbackUsable bool ) buildPlan {
78+ switch {
79+ case imageExists :
80+ return buildPlan {action : actionUseDesired }
81+ case explicitRebuild :
82+ return buildPlan {action : actionSyncBuild }
83+ case fallbackUsable :
84+ return buildPlan {action : actionUseFallback , bgBuildTag : imageTag }
85+ default :
86+ return buildPlan {action : actionSyncBuild }
87+ }
88+ }
89+
5290// Tool runs a tool inside a container.
5391func Tool (opts Options ) error {
5492 tool := opts .ToolDef .Name
@@ -316,7 +354,41 @@ func Tool(opts Options) error {
316354 })
317355 }
318356
319- if imageExists {
357+ // launchBgBuild spawns a detached build for the given image tag unless one
358+ // is already in flight.
359+ launchBgBuild := func (tag string , bArgs map [string ]string ) {
360+ if IsBuilding (tag ) {
361+ return
362+ }
363+ _ = LaunchBackgroundBuild (BackgroundBuildOptions {
364+ ImageTag : tag ,
365+ Dockerfile : dockerfile ,
366+ BuildArgs : bArgs ,
367+ Backend : cfg .Backend ,
368+ FlyApp : cfg .Backends .Fly .App ,
369+ FlyRegion : cfg .Backends .Fly .Region ,
370+ NamespaceSize : cfg .Backends .Namespace .Size ,
371+ NamespaceSite : cfg .Backends .Namespace .Site ,
372+ Tool : tool ,
373+ })
374+ }
375+
376+ // Determine whether a structurally-identical older image is usable as a
377+ // fallback while the desired image builds. Only relevant when the desired
378+ // image is absent and we're not doing an explicit rebuild, so we avoid the
379+ // extra ImageExists call otherwise.
380+ explicitRebuild := opts .ForceBuild || opts .NoCache || opts .ToolVersion != ""
381+ fallbackTag := loadLastImage (tool )
382+ fallbackUsable := false
383+ if ! imageExists && ! explicitRebuild && fallbackTag != "" && fallbackTag != imageTag {
384+ if exists , _ := backendClient .ImageExists (ctx , fallbackTag ); exists {
385+ fallbackUsable = structuralTag == loadStructuralTag (tool )
386+ }
387+ }
388+
389+ plan := planBuild (imageTag , imageExists , explicitRebuild , fallbackUsable )
390+ switch plan .action {
391+ case actionUseDesired :
320392 // Image is cached — use it directly (scenario 2 & 3 cache hit).
321393 if err := buildEnvironment (ctx , backendClient , buildEnvOptions {
322394 tool : tool ,
@@ -340,36 +412,28 @@ func Tool(opts Options) error {
340412 }
341413 return err
342414 }
343- } else if opts .ForceBuild || opts .NoCache || opts .ToolVersion != "" {
344- // Scenario 3 cache miss, or explicit rebuild — build synchronously.
415+ case actionUseFallback :
416+ // Desired image is missing but a structurally-identical older image
417+ // exists (only the tool version differs). Run it now; the desired
418+ // image builds in the background (launched below via plan.bgBuildTag).
419+ logSection ("Using cached environment (update building in background)..." )
420+ runImageTag = fallbackTag
421+ case actionSyncBuild :
422+ // Explicit rebuild, or no usable fallback — build synchronously.
345423 if err := syncBuild (); err != nil {
346424 if progress != nil {
347425 progress .Complete ()
348426 }
349427 return err
350428 }
351- } else {
352- // Image doesn't exist — the background build from a previous run
353- // may not have finished yet. Use the previous image as a fallback
354- // if only the version changed; otherwise build synchronously.
355- fallbackTag := loadLastImage (tool )
356- var fallbackExists bool
357- if fallbackTag != "" && fallbackTag != imageTag {
358- fallbackExists , _ = backendClient .ImageExists (ctx , fallbackTag )
359- }
360-
361- onlyVersionChanged := fallbackExists && structuralTag == loadStructuralTag (tool )
362- if onlyVersionChanged {
363- logSection ("Using cached environment (update building in background)..." )
364- runImageTag = fallbackTag
365- } else {
366- if err := syncBuild (); err != nil {
367- if progress != nil {
368- progress .Complete ()
369- }
370- return err
371- }
372- }
429+ }
430+
431+ // The desired image was missing and we fell back to an older one — relaunch
432+ // its build here. The version-change goroutine below won't, because the
433+ // cached version already equals the latest fetched version, so without this
434+ // we'd fall back forever.
435+ if plan .bgBuildTag != "" {
436+ launchBgBuild (plan .bgBuildTag , buildArgs )
373437 }
374438
375439 // Save state after image is resolved but before the tool runs, so
@@ -397,17 +461,7 @@ func Tool(opts Options) error {
397461 }
398462 newBuildArgs ["TOOL_VERSION" ] = newVersion
399463 logSection ("New version available (%s), building in background..." , newVersion )
400- _ = LaunchBackgroundBuild (BackgroundBuildOptions {
401- ImageTag : newImageTag ,
402- Dockerfile : dockerfile ,
403- BuildArgs : newBuildArgs ,
404- Backend : cfg .Backend ,
405- FlyApp : cfg .Backends .Fly .App ,
406- FlyRegion : cfg .Backends .Fly .Region ,
407- NamespaceSize : cfg .Backends .Namespace .Size ,
408- NamespaceSite : cfg .Backends .Namespace .Site ,
409- Tool : tool ,
410- })
464+ launchBgBuild (newImageTag , newBuildArgs )
411465 }()
412466 }
413467
0 commit comments