github.com/grahambrereton-form3/tilt@v0.10.18/internal/engine/target_queue.go (about) 1 package engine 2 3 import ( 4 "context" 5 "fmt" 6 7 "github.com/docker/distribution/reference" 8 "github.com/pkg/errors" 9 10 "github.com/windmilleng/tilt/internal/store" 11 "github.com/windmilleng/tilt/pkg/model" 12 ) 13 14 // Allows the caller to inject its own build strategy for dirty targets. 15 type BuildHandler func( 16 target model.TargetSpec, 17 state store.BuildState, 18 depResults []store.BuildResult) (store.BuildResult, error) 19 20 type ImageExistsChecker func(ctx context.Context, namedTagged reference.NamedTagged) (bool, error) 21 22 // A little data structure to help iterate through dirty targets in dependency order. 23 type TargetQueue struct { 24 sortedTargets []model.TargetSpec 25 26 // The state from the previous build. 27 // Contains files-changed so we can do incremental builds. 28 state store.BuildStateSet 29 30 // The results of this build. 31 results store.BuildResultSet 32 33 // Whether the target itself needs a rebuilt, either because it has dirty files 34 // or has never been built before. 35 // 36 // A target with dirty files might be able to use the files changed 37 // since the previous result to build the next result. 38 needsOwnBuild map[model.TargetID]bool 39 40 // Whether the target depends transitively on something that needs rebuilding. 41 // A target that depends on a dirty target should never use its previous 42 // result to build the next result. 43 depsNeedBuild map[model.TargetID]bool 44 } 45 46 func NewImageTargetQueue(ctx context.Context, iTargets []model.ImageTarget, state store.BuildStateSet, imageExists ImageExistsChecker) (*TargetQueue, error) { 47 targets := make([]model.TargetSpec, 0, len(iTargets)) 48 for _, iTarget := range iTargets { 49 targets = append(targets, iTarget) 50 } 51 52 sortedTargets, err := model.TopologicalSort(targets) 53 if err != nil { 54 return nil, err 55 } 56 57 needsOwnBuild := make(map[model.TargetID]bool) 58 for _, target := range sortedTargets { 59 id := target.ID() 60 if state[id].NeedsImageBuild() { 61 needsOwnBuild[id] = true 62 } else if state[id].LastSuccessfulResult != nil { 63 image := store.ImageFromBuildResult(state[id].LastSuccessfulResult) 64 exists, err := imageExists(ctx, image) 65 if err != nil { 66 return nil, errors.Wrapf(err, "error looking up whether last image built for %s exists", image.String()) 67 } 68 if !exists { 69 needsOwnBuild[id] = true 70 } 71 } 72 } 73 74 depsNeedBuild := make(map[model.TargetID]bool) 75 for _, target := range sortedTargets { 76 for _, depID := range target.DependencyIDs() { 77 if needsOwnBuild[depID] || depsNeedBuild[depID] { 78 depsNeedBuild[target.ID()] = true 79 break 80 } 81 } 82 } 83 84 results := make(store.BuildResultSet, len(targets)) 85 return &TargetQueue{ 86 sortedTargets: sortedTargets, 87 state: state, 88 results: results, 89 needsOwnBuild: needsOwnBuild, 90 depsNeedBuild: depsNeedBuild, 91 }, nil 92 } 93 94 func (q *TargetQueue) CountDirty() int { 95 result := 0 96 for _, target := range q.sortedTargets { 97 if q.needsOwnBuild[target.ID()] || q.depsNeedBuild[target.ID()] { 98 result++ 99 } 100 } 101 return result 102 } 103 104 func (q *TargetQueue) RunBuilds(handler BuildHandler) error { 105 for _, target := range q.sortedTargets { 106 id := target.ID() 107 if q.depsNeedBuild[id] { 108 // If the dependencies are dirty, we can't use any state from the previous build. 109 result, err := handler(target, store.BuildState{}, q.dependencyResults(target)) 110 if err != nil { 111 return err 112 } 113 q.results[id] = result 114 } else if q.needsOwnBuild[id] { 115 // If only files are dirty, we can try to do an incremental build. 116 result, err := handler(target, q.state[id], q.dependencyResults(target)) 117 if err != nil { 118 return err 119 } 120 q.results[id] = result 121 } else { 122 // Otherwise, we can re-use results from the previous build. 123 // If needsOwnBuild is false, then LastSuccessfulResult must exist if it's empty. 124 lastResult := q.state[id].LastSuccessfulResult 125 image := store.ImageFromBuildResult(lastResult) 126 if image == nil { 127 return fmt.Errorf("Internal error: build marked clean but last result not found: %+v", q.state[id]) 128 } 129 q.results[id] = lastResult 130 } 131 } 132 return nil 133 } 134 135 func (q *TargetQueue) dependencyResults(target model.TargetSpec) []store.BuildResult { 136 depIDs := target.DependencyIDs() 137 results := make([]store.BuildResult, 0, len(depIDs)) 138 for _, depID := range depIDs { 139 results = append(results, q.results[depID]) 140 } 141 return results 142 }