@@ -122,6 +122,7 @@ Configuration is loaded from (in order, merged):
122122 rootCmd .Flags ().Bool ("force-build" , false , "Force rebuild of container image" )
123123 rootCmd .Flags ().Bool ("no-cache" , false , "Disable build cache (implies --force-build)" )
124124 rootCmd .Flags ().BoolP ("verbose" , "v" , false , "Show detailed output instead of progress bar" )
125+ rootCmd .Flags ().String ("tool-version" , "" , "Pin a specific tool version (forces synchronous build)" )
125126
126127 // Define command groups (order here determines display order in --help)
127128 rootCmd .AddGroup (
@@ -147,6 +148,7 @@ Configuration is loaded from (in order, merged):
147148 toolCmd .Flags ().Bool ("no-cache" , false , "Disable build cache (implies --force-build)" )
148149 toolCmd .Flags ().BoolP ("verbose" , "v" , false , "Show detailed output instead of progress bar" )
149150 toolCmd .Flags ().String ("entrypoint" , "" , "Run a custom command instead of the tool (e.g. /bin/bash)" )
151+ toolCmd .Flags ().String ("tool-version" , "" , "Pin a specific tool version (forces synchronous build)" )
150152 rootCmd .AddCommand (toolCmd )
151153 }
152154
@@ -282,6 +284,18 @@ Use --local or --global to skip the prompt.`,
282284 shellCmd .Flags ().String ("backend" , "" , "Backend to use: docker, container (default: both)" )
283285 rootCmd .AddCommand (shellCmd )
284286
287+ // Hidden __build subcommand — used by background build launcher.
288+ buildCmd := & cobra.Command {
289+ Use : "__build" ,
290+ Hidden : true ,
291+ RunE : func (cmd * cobra.Command , args []string ) error {
292+ dir , _ := cmd .Flags ().GetString ("dir" )
293+ return runBackgroundBuild (dir , stderr )
294+ },
295+ }
296+ buildCmd .Flags ().String ("dir" , "" , "Build state directory" )
297+ rootCmd .AddCommand (buildCmd )
298+
285299 rootCmd .Version = version
286300 rootCmd .SetVersionTemplate ("silo version {{.Version}}\n " )
287301
@@ -346,16 +360,20 @@ func runSilo(cmd *cobra.Command, args []string, stdout, stderr io.Writer) error
346360 // Get verbose flag
347361 verbose , _ := cmd .Flags ().GetBool ("verbose" )
348362
363+ // Get tool-version flag
364+ toolVersion , _ := cmd .Flags ().GetString ("tool-version" )
365+
349366 // Run the tool
350367 return run .Tool (run.Options {
351- ToolDef : * toolDef ,
352- Config : cfg ,
353- Dockerfile : Dockerfile (supportedTools ),
354- ForceBuild : forceBuild ,
355- NoCache : noCache ,
356- Verbose : verbose ,
357- Stdout : stdout ,
358- Stderr : stderr ,
368+ ToolDef : * toolDef ,
369+ Config : cfg ,
370+ Dockerfile : Dockerfile (supportedTools ),
371+ ForceBuild : forceBuild ,
372+ NoCache : noCache ,
373+ Verbose : verbose ,
374+ ToolVersion : toolVersion ,
375+ Stdout : stdout ,
376+ Stderr : stderr ,
359377 })
360378}
361379
@@ -389,18 +407,22 @@ func runTool(cmd *cobra.Command, toolDef tools.Tool, args []string, stdout, stde
389407 // Get entrypoint flag
390408 entrypoint , _ := cmd .Flags ().GetString ("entrypoint" )
391409
410+ // Get tool-version flag
411+ toolVersion , _ := cmd .Flags ().GetString ("tool-version" )
412+
392413 // Run the tool
393414 return run .Tool (run.Options {
394- ToolDef : toolDef ,
395- ToolArgs : toolArgs ,
396- Config : cfg ,
397- Dockerfile : Dockerfile (supportedTools ),
398- ForceBuild : forceBuild ,
399- NoCache : noCache ,
400- Verbose : verbose ,
401- Entrypoint : entrypoint ,
402- Stdout : stdout ,
403- Stderr : stderr ,
415+ ToolDef : toolDef ,
416+ ToolArgs : toolArgs ,
417+ Config : cfg ,
418+ Dockerfile : Dockerfile (supportedTools ),
419+ ForceBuild : forceBuild ,
420+ NoCache : noCache ,
421+ Verbose : verbose ,
422+ Entrypoint : entrypoint ,
423+ ToolVersion : toolVersion ,
424+ Stdout : stdout ,
425+ Stderr : stderr ,
404426 })
405427}
406428
@@ -911,3 +933,62 @@ func formatMemoryUsage(bytes uint64, isRunning bool) string {
911933 }
912934 return humanize .IBytes (bytes )
913935}
936+
937+ // runBackgroundBuild is the entry point for the hidden `silo __build` command.
938+ // It reads all build parameters from the manifest in the build state directory,
939+ // acquires a build lock, builds the image, and updates state.
940+ func runBackgroundBuild (dir string , stderr io.Writer ) error {
941+ if dir == "" {
942+ return fmt .Errorf ("__build: --dir is required" )
943+ }
944+
945+ // Read build manifest.
946+ imageTag , tool , backendType , dockerfile , buildArgs , err := run .ReadBuildManifest (dir )
947+ if err != nil {
948+ return fmt .Errorf ("__build: %w" , err )
949+ }
950+
951+ // Acquire build lock — exit cleanly if another build is already running.
952+ lock , err := run .TryLock (imageTag )
953+ if err != nil {
954+ return fmt .Errorf ("__build: lock: %w" , err )
955+ }
956+ if lock == nil {
957+ // Another process is already building this image.
958+ return nil
959+ }
960+ defer lock .Unlock ()
961+
962+ // Create backend.
963+ backendClient , err := run .CreateBackend (backendType , stderr , false )
964+ if err != nil {
965+ lock .WriteStatus ("failed" )
966+ return fmt .Errorf ("__build: backend: %w" , err )
967+ }
968+ defer backendClient .Close ()
969+
970+ // Build image.
971+ fmt .Fprintf (stderr , "==> Background build: %s (%s)\n " , tool , imageTag )
972+ ctx := context .Background ()
973+ _ , err = backendClient .Build (ctx , backend.BuildOptions {
974+ Dockerfile : dockerfile ,
975+ Target : tool ,
976+ Tag : imageTag ,
977+ BuildArgs : buildArgs ,
978+ OnProgress : func (msg string ) {
979+ fmt .Fprint (stderr , msg )
980+ },
981+ })
982+ if err != nil {
983+ lock .WriteStatus ("failed" )
984+ return fmt .Errorf ("__build: build: %w" , err )
985+ }
986+
987+ lock .WriteStatus ("done" )
988+ fmt .Fprintf (stderr , "==> Background build complete: %s\n " , imageTag )
989+
990+ // Update last-image so next run picks up the new image.
991+ run .SaveLastImage (tool , imageTag )
992+
993+ return nil
994+ }
0 commit comments