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  }