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