github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/buildcontrol/local_target_build_and_deployer.go (about) 1 package buildcontrol 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "k8s.io/apimachinery/pkg/types" 9 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 10 11 "github.com/tilt-dev/tilt/internal/analytics" 12 "github.com/tilt-dev/tilt/internal/build" 13 "github.com/tilt-dev/tilt/internal/controllers/core/cmd" 14 "github.com/tilt-dev/tilt/internal/store" 15 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 16 "github.com/tilt-dev/tilt/pkg/model" 17 ) 18 19 var _ BuildAndDeployer = &LocalTargetBuildAndDeployer{} 20 21 // TODO(maia): CommandRunner interface for testability 22 type LocalTargetBuildAndDeployer struct { 23 clock build.Clock 24 ctrlClient ctrlclient.Client 25 cmds *cmd.Controller 26 } 27 28 func NewLocalTargetBuildAndDeployer( 29 c build.Clock, 30 ctrlClient ctrlclient.Client, 31 cmds *cmd.Controller) *LocalTargetBuildAndDeployer { 32 return &LocalTargetBuildAndDeployer{ 33 clock: c, 34 ctrlClient: ctrlClient, 35 cmds: cmds, 36 } 37 } 38 39 func (bd *LocalTargetBuildAndDeployer) BuildAndDeploy(ctx context.Context, st store.RStore, specs []model.TargetSpec, stateSet store.BuildStateSet) (resultSet store.BuildResultSet, err error) { 40 targets := bd.extract(specs) 41 if len(targets) != 1 { 42 return store.BuildResultSet{}, SilentRedirectToNextBuilderf( 43 "LocalTargetBuildAndDeployer requires exactly one LocalTarget (got %d)", len(targets)) 44 } 45 46 targ := targets[0] 47 if targ.UpdateCmdSpec == nil { 48 // Even if a LocalResource has no update command, we push it through the build-and-deploy 49 // pipeline so that it gets all the appropriate logs. 50 return bd.successfulBuildResult(targ), nil 51 } 52 53 startTime := time.Now() 54 defer func() { 55 analytics.Get(ctx).Timer("build.local", time.Since(startTime), map[string]string{ 56 "hasError": fmt.Sprintf("%t", err != nil), 57 }) 58 }() 59 60 var cmd v1alpha1.Cmd 61 err = bd.ctrlClient.Get(ctx, types.NamespacedName{Name: targ.UpdateCmdName()}, &cmd) 62 if err != nil { 63 return store.BuildResultSet{}, DontFallBackErrorf("Loading command: %v", err) 64 } 65 66 status, err := bd.cmds.ForceRun(ctx, &cmd) 67 if err != nil { 68 // (Never fall back from the LocalTargetBaD, none of our other BaDs can handle this target) 69 return store.BuildResultSet{}, DontFallBackErrorf("Command %q failed: %v", 70 model.ArgListToString(cmd.Spec.Args), err) 71 } else if status.Terminated == nil { 72 return store.BuildResultSet{}, DontFallBackErrorf("Command didn't terminate") 73 } else if status.Terminated.ExitCode != 0 { 74 return store.BuildResultSet{}, DontFallBackErrorf("Command %q failed: %v", 75 model.ArgListToString(cmd.Spec.Args), status.Terminated.Reason) 76 } 77 78 // HACK(maia) Suppose target A modifies file X and target B depends on file X. 79 // 80 // Consider this sequence: 81 // 82 // 1. A starts 83 // 2. A modifies X at time T1 84 // 3. A modifies X at time T2 85 // 4. A finishes 86 // 5. B starts, caused by change at T1 87 // 6. Tilt observes change at T2 88 // 7. B finishes building 89 // 8. B builds again, because the change at T2 was observed after the first build started. 90 // 91 // Empirically, this sleep ensures that any local file changes are processed 92 // before the next build starts. 93 // 94 // At the moment (2020-01-31), local_resources will not build in parallel with 95 // other resources by default, so this works fine. 96 // 97 // Possible approaches for a better system: 98 // 99 // - Use mtimes rather than our own internal modification tracking 100 // for determining dirtiness. Here is some good discussion of this approach: 101 // https://github.com/ninja-build/ninja/blob/master/src/deps_log.h#L29 102 // https://apenwarr.ca/log/20181113 103 // which has a lot of caveats, but you can boil it down to "using mtimes can 104 // make things a lot more efficient, but be careful how you use them" 105 // 106 // - Make a "dummy" change to the file system and make sure it propagates 107 // through the watch system before we start the next build (like fsync() does 108 // in our watch tests). 109 time.Sleep(250 * time.Millisecond) 110 111 return bd.successfulBuildResult(targ), nil 112 } 113 114 // Extract the targets we can apply -- i.e. LocalTargets 115 func (bd *LocalTargetBuildAndDeployer) extract(specs []model.TargetSpec) []model.LocalTarget { 116 var targs []model.LocalTarget 117 for _, s := range specs { 118 if s, ok := s.(model.LocalTarget); ok { 119 targs = append(targs, s) 120 } 121 } 122 return targs 123 } 124 125 func (bd *LocalTargetBuildAndDeployer) successfulBuildResult(t model.LocalTarget) store.BuildResultSet { 126 br := store.NewLocalBuildResult(t.ID()) 127 return store.BuildResultSet{t.ID(): br} 128 }