github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/buildcontrol/target_queue.go (about) 1 package buildcontrol 2 3 import ( 4 "context" 5 "fmt" 6 7 "github.com/distribution/reference" 8 "github.com/pkg/errors" 9 10 "github.com/tilt-dev/tilt/internal/container" 11 "github.com/tilt-dev/tilt/internal/store" 12 "github.com/tilt-dev/tilt/pkg/logger" 13 "github.com/tilt-dev/tilt/pkg/model" 14 ) 15 16 // Allows the caller to inject its own build strategy for dirty targets. 17 type BuildHandler func( 18 target model.TargetSpec, 19 depResults []store.ImageBuildResult) (store.ImageBuildResult, error) 20 21 type ReuseRefChecker func(ctx context.Context, iTarget model.ImageTarget, namedTagged reference.NamedTagged) (bool, error) 22 23 // A little data structure to help iterate through dirty targets in dependency order. 24 type TargetQueue struct { 25 sortedTargets []model.TargetSpec 26 27 // The state from the previous build. 28 // Contains files-changed so that we can recycle old builds. 29 state store.BuildStateSet 30 31 // The results of this build. 32 results map[model.TargetID]store.ImageBuildResult 33 34 // Whether the target itself needs a rebuilt, either because it has dirty files 35 // or has never been built before. 36 // 37 // A target with dirty files might be able to use the files changed 38 // since the previous result to build the next result. 39 needsOwnBuild map[model.TargetID]bool 40 41 // Whether the target depends transitively on something that needs rebuilding. 42 // A target that depends on a dirty target should never use its previous 43 // result to build the next result. 44 depsNeedBuild map[model.TargetID]bool 45 } 46 47 func NewImageTargetQueue(ctx context.Context, iTargets []model.ImageTarget, state store.BuildStateSet, canReuseRef ReuseRefChecker) (*TargetQueue, error) { 48 targets := make([]model.TargetSpec, 0, len(iTargets)) 49 for _, iTarget := range iTargets { 50 if iTarget.IsLiveUpdateOnly { 51 continue 52 } 53 targets = append(targets, iTarget) 54 } 55 56 sortedTargets, err := model.TopologicalSort(targets) 57 if err != nil { 58 return nil, err 59 } 60 61 needsOwnBuild := make(map[model.TargetID]bool) 62 for _, target := range sortedTargets { 63 id := target.ID() 64 if state[id].NeedsImageBuild() { 65 needsOwnBuild[id] = true 66 } else if state[id].LastResult != nil { 67 image := store.LocalImageRefFromBuildResult(state[id].LastResult) 68 imageRef, err := container.ParseNamedTagged(image) 69 if err != nil { 70 return nil, errors.Wrapf(err, "parsing image") 71 } 72 ok, err := canReuseRef(ctx, target.(model.ImageTarget), imageRef) 73 if err != nil { 74 return nil, errors.Wrapf(err, "error looking up whether last image built for %s exists", image) 75 } 76 if !ok { 77 logger.Get(ctx).Infof("Rebuilding %s because image not found in image store", image) 78 needsOwnBuild[id] = true 79 } 80 } 81 } 82 83 depsNeedBuild := make(map[model.TargetID]bool) 84 for _, target := range sortedTargets { 85 for _, depID := range target.DependencyIDs() { 86 if needsOwnBuild[depID] || depsNeedBuild[depID] { 87 depsNeedBuild[target.ID()] = true 88 break 89 } 90 } 91 } 92 93 results := make(store.ImageBuildResultSet, len(targets)) 94 queue := &TargetQueue{ 95 sortedTargets: sortedTargets, 96 state: state, 97 results: results, 98 needsOwnBuild: needsOwnBuild, 99 depsNeedBuild: depsNeedBuild, 100 } 101 err = queue.backfillExistingResults() 102 if err != nil { 103 return nil, err 104 } 105 return queue, nil 106 } 107 108 // New results that were built with the current queue. Omits results 109 // that were re-used previous builds. 110 // 111 // Returns results that the BuildAndDeploy contract expects. 112 func (q *TargetQueue) NewResults() store.ImageBuildResultSet { 113 newResults := store.ImageBuildResultSet{} 114 for id, result := range q.results { 115 if q.isBuilding(id) { 116 newResults[id] = result 117 } 118 } 119 return newResults 120 } 121 122 // Reused results that were not built with the current queue. 123 // 124 // Used for printing out which builds are cached from previous builds. 125 func (q *TargetQueue) ReusedResults() store.ImageBuildResultSet { 126 reusedResults := store.ImageBuildResultSet{} 127 for id, result := range q.results { 128 if !q.isBuilding(id) { 129 reusedResults[id] = result 130 } 131 } 132 return reusedResults 133 } 134 135 // All results for targets in the current queue. 136 func (q *TargetQueue) AllResults() store.ImageBuildResultSet { 137 allResults := store.ImageBuildResultSet{} 138 for id, result := range q.results { 139 allResults[id] = result 140 } 141 return allResults 142 } 143 144 func (q *TargetQueue) isBuilding(id model.TargetID) bool { 145 return q.needsOwnBuild[id] || q.depsNeedBuild[id] 146 } 147 148 func (q *TargetQueue) CountBuilds() int { 149 result := 0 150 for _, target := range q.sortedTargets { 151 if q.isBuilding(target.ID()) { 152 result++ 153 } 154 } 155 return result 156 } 157 158 func (q *TargetQueue) backfillExistingResults() error { 159 for _, target := range q.sortedTargets { 160 id := target.ID() 161 if !q.isBuilding(id) { 162 // We can re-use results from the previous build. 163 lastResult := q.state[id].LastResult 164 imageResult, ok := lastResult.(store.ImageBuildResult) 165 if !ok { 166 return fmt.Errorf("Internal error: build marked clean but last result not found: %+v", q.state[id]) 167 } 168 q.results[id] = imageResult 169 } 170 } 171 return nil 172 } 173 174 func (q *TargetQueue) RunBuilds(handler BuildHandler) error { 175 for _, target := range q.sortedTargets { 176 id := target.ID() 177 if q.isBuilding(id) { 178 result, err := handler(target, q.dependencyResults(target)) 179 if err != nil { 180 return err 181 } 182 q.results[id] = result 183 } 184 } 185 return nil 186 } 187 188 func (q *TargetQueue) dependencyResults(target model.TargetSpec) []store.ImageBuildResult { 189 depIDs := target.DependencyIDs() 190 results := make([]store.ImageBuildResult, 0, len(depIDs)) 191 for _, depID := range depIDs { 192 results = append(results, q.results[depID]) 193 } 194 return results 195 }