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  }