github.com/Benchkram/bob@v0.0.0-20220321080157-7c8f3876e225/bob/playbook/build.go (about)

     1  package playbook
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"sort"
     9  	"time"
    10  
    11  	"github.com/Benchkram/bob/bobtask"
    12  	"github.com/Benchkram/bob/pkg/boblog"
    13  	"github.com/Benchkram/errz"
    14  	"github.com/logrusorgru/aurora"
    15  )
    16  
    17  var colorPool = []aurora.Color{
    18  	1,
    19  	aurora.BlueFg,
    20  	aurora.GreenFg,
    21  	aurora.CyanFg,
    22  	aurora.MagentaFg,
    23  	aurora.YellowFg,
    24  	aurora.RedFg,
    25  }
    26  var round = 10 * time.Millisecond
    27  
    28  // Build the playbook starting at root.
    29  func (p *Playbook) Build(ctx context.Context) (err error) {
    30  	done := make(chan error)
    31  
    32  	{
    33  		tasks := []string{}
    34  		for _, t := range p.Tasks {
    35  			tasks = append(tasks, t.Name())
    36  		}
    37  		sort.Strings(tasks)
    38  
    39  		// Adjust padding of first column based on the taskname length.
    40  		// Also assign fixed color to the tasks.
    41  		p.namePad = 0
    42  		for i, name := range tasks {
    43  			if len(name) > p.namePad {
    44  				p.namePad = len(name)
    45  			}
    46  
    47  			color := colorPool[i%len(colorPool)]
    48  			p.Tasks[name].Task.SetColor(color)
    49  		}
    50  		p.namePad += 14
    51  
    52  		dependencies := len(tasks) - 1
    53  		rootName := p.Tasks[p.root].ColoredName()
    54  		boblog.Log.V(1).Info(fmt.Sprintf("Running task %s with %d dependencies", rootName, dependencies))
    55  	}
    56  
    57  	processedTasks := []*bobtask.Task{}
    58  
    59  	go func() {
    60  		// TODO: Run a worker pool so that multiple tasks can run in parallel.
    61  
    62  		c := p.TaskChannel()
    63  		for t := range c {
    64  			// copy for processing
    65  			task := t
    66  			processedTasks = append(processedTasks, &task)
    67  
    68  			err := p.build(ctx, &task)
    69  			if err != nil {
    70  				done <- err
    71  				break
    72  			}
    73  		}
    74  
    75  		close(done)
    76  	}()
    77  
    78  	err = p.Play()
    79  	errz.Fatal(err)
    80  
    81  	err = <-done
    82  	if err != nil {
    83  		p.Done()
    84  	}
    85  	errz.Fatal(err)
    86  
    87  	// iterate through tasks and log
    88  	// skipped input files.
    89  	var skippedInputs int
    90  	for _, task := range processedTasks {
    91  		skippedInputs = logSkippedInputs(
    92  			skippedInputs,
    93  			task.ColoredName(),
    94  			task.LogSkippedInput(),
    95  		)
    96  	}
    97  
    98  	// summary
    99  	boblog.Log.V(1).Info("")
   100  	boblog.Log.V(1).Info(aurora.Bold("● ● ● ●").BrightGreen().String())
   101  	t := fmt.Sprintf("Ran %d tasks in %s ", len(processedTasks), p.ExecutionTime().Round(round))
   102  	boblog.Log.V(1).Info(aurora.Bold(t).BrightGreen().String())
   103  	for _, t := range processedTasks {
   104  		stat, err := p.TaskStatus(t.Name())
   105  		if err != nil {
   106  			fmt.Println(err)
   107  			continue
   108  		}
   109  
   110  		execTime := ""
   111  		status := stat.State()
   112  		if status != StateNoRebuildRequired {
   113  			execTime = fmt.Sprintf("\t(%s)", stat.ExecutionTime().Round(round))
   114  		}
   115  
   116  		taskName := t.Name()
   117  		boblog.Log.V(1).Info(fmt.Sprintf("  %-*s\t%s%s", p.namePad, taskName, status.Summary(), execTime))
   118  	}
   119  	boblog.Log.V(1).Info("")
   120  
   121  	return err
   122  }
   123  
   124  // didWriteBuildOutput assures that a new line is added
   125  // before writing state or logs of a task to stdout.
   126  var didWriteBuildOutput bool
   127  
   128  // build a single task and update the playbook state after completion.
   129  func (p *Playbook) build(ctx context.Context, task *bobtask.Task) (err error) {
   130  	defer errz.Recover(&err)
   131  
   132  	var taskSuccessFul bool
   133  	var taskErr error
   134  	defer func() {
   135  		if !taskSuccessFul {
   136  			errr := p.TaskFailed(task.Name(), taskErr)
   137  			if errr != nil {
   138  				boblog.Log.Error(errr, "Setting the task state to failed, failed.")
   139  			}
   140  		}
   141  	}()
   142  
   143  	coloredName := task.ColoredName()
   144  
   145  	done := make(chan struct{})
   146  	defer close(done)
   147  
   148  	go func() {
   149  		select {
   150  		case <-done:
   151  		case <-ctx.Done():
   152  			if errors.Is(ctx.Err(), context.Canceled) {
   153  				boblog.Log.V(1).Info(fmt.Sprintf("%-*s\t%s", p.namePad, coloredName, StateCanceled))
   154  				_ = p.TaskCanceled(task.Name())
   155  			}
   156  		}
   157  	}()
   158  
   159  	hashIn, err := task.HashIn()
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	rebuildRequired, rebuildCause, err := p.TaskNeedsRebuild(task.Name(), hashIn)
   165  	errz.Fatal(err)
   166  
   167  	// task might need a rebuild due to a input change.
   168  	// but could still be possible to load the targets from the artifact store.
   169  	// If a task needs a rebuild due to a dependency change => rebuild.
   170  	if rebuildRequired && rebuildCause != DependencyChanged && rebuildCause != TaskForcedRebuild {
   171  		success, err := task.ArtifactUnpack(hashIn)
   172  		errz.Fatal(err)
   173  		if success {
   174  			rebuildRequired = false
   175  		}
   176  	}
   177  
   178  	if !rebuildRequired {
   179  		status := StateNoRebuildRequired
   180  		boblog.Log.V(2).Info(fmt.Sprintf("%-*s\t%s", p.namePad, coloredName, status.Short()))
   181  		taskSuccessFul = true
   182  		return p.TaskNoRebuildRequired(task.Name())
   183  	}
   184  
   185  	if !didWriteBuildOutput {
   186  		boblog.Log.V(1).Info("")
   187  		didWriteBuildOutput = true
   188  	}
   189  	boblog.Log.V(1).Info(fmt.Sprintf("%-*s\trunning task...", p.namePad, coloredName))
   190  
   191  	err = task.Clean()
   192  	errz.Fatal(err)
   193  
   194  	err = task.Run(ctx, p.namePad)
   195  	if err != nil {
   196  		taskSuccessFul = false
   197  		taskErr = err
   198  	}
   199  	errz.Fatal(err)
   200  
   201  	taskSuccessFul = true
   202  
   203  	err = task.VerifyAfter()
   204  	errz.Fatal(err)
   205  
   206  	target, err := task.Target()
   207  	if err != nil {
   208  		errz.Fatal(err)
   209  	}
   210  
   211  	// Check targets are created correctly.
   212  	// On success the target hash is computed
   213  	// inside TaskCompleted().
   214  	if target != nil {
   215  		if !target.Exists() {
   216  			boblog.Log.V(1).Info(fmt.Sprintf("%-*s\t%s\t(invalid targets)", p.namePad, coloredName, StateFailed))
   217  			err = p.TaskFailed(task.Name(), fmt.Errorf("targets not created"))
   218  			if err != nil {
   219  				if errors.Is(err, ErrFailed) {
   220  					return err
   221  				}
   222  			}
   223  		}
   224  	}
   225  
   226  	err = p.TaskCompleted(task.Name())
   227  	errz.Fatal(err)
   228  
   229  	taskStatus, err := p.TaskStatus(task.Name())
   230  	errz.Fatal(err)
   231  
   232  	state := taskStatus.State()
   233  	boblog.Log.V(1).Info(fmt.Sprintf("%-*s\t%s", p.namePad, coloredName, "..."+state.Short()))
   234  
   235  	return nil
   236  }
   237  
   238  const maxSkippedInputs = 5
   239  
   240  // logSkippedInputs until max is reached
   241  func logSkippedInputs(count int, taskname string, skippedInputs []string) int {
   242  	if len(skippedInputs) == 0 {
   243  		return count
   244  	}
   245  	if count >= maxSkippedInputs {
   246  		return maxSkippedInputs
   247  	}
   248  
   249  	for _, f := range skippedInputs {
   250  		count = count + 1
   251  		boblog.Log.V(1).Info(fmt.Sprintf("skipped %s '%s' %s", taskname, f, os.ErrPermission))
   252  
   253  		if count >= maxSkippedInputs {
   254  			boblog.Log.V(1).Info(fmt.Sprintf("skipped %s %s", taskname, "& more..."))
   255  			break
   256  		}
   257  	}
   258  
   259  	return count
   260  }