123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- package task
- import (
- "context"
- "fmt"
- "io"
- "os"
- "runtime"
- "slices"
- "sync"
- "sync/atomic"
- "time"
- "mvdan.cc/sh/v3/interp"
- "github.com/go-task/task/v3/errors"
- "github.com/go-task/task/v3/internal/compiler"
- "github.com/go-task/task/v3/internal/env"
- "github.com/go-task/task/v3/internal/execext"
- "github.com/go-task/task/v3/internal/fingerprint"
- "github.com/go-task/task/v3/internal/logger"
- "github.com/go-task/task/v3/internal/output"
- "github.com/go-task/task/v3/internal/slicesext"
- "github.com/go-task/task/v3/internal/sort"
- "github.com/go-task/task/v3/internal/summary"
- "github.com/go-task/task/v3/internal/templater"
- "github.com/go-task/task/v3/taskfile/ast"
- "github.com/sajari/fuzzy"
- "golang.org/x/sync/errgroup"
- )
- const (
- // MaximumTaskCall is the max number of times a task can be called.
- // This exists to prevent infinite loops on cyclic dependencies
- MaximumTaskCall = 1000
- )
- type TempDir struct {
- Remote string
- Fingerprint string
- }
- // Executor executes a Taskfile
- type Executor struct {
- Taskfile *ast.Taskfile
- Dir string
- Entrypoint string
- TempDir TempDir
- Force bool
- ForceAll bool
- Insecure bool
- Download bool
- Offline bool
- Timeout time.Duration
- Watch bool
- Verbose bool
- Silent bool
- AssumeYes bool
- AssumeTerm bool // Used for testing
- Dry bool
- Summary bool
- Parallel bool
- Color bool
- Concurrency int
- Interval time.Duration
- Stdin io.Reader
- Stdout io.Writer
- Stderr io.Writer
- Logger *logger.Logger
- Compiler *compiler.Compiler
- Output output.Output
- OutputStyle ast.Output
- TaskSorter sort.TaskSorter
- UserWorkingDir string
- fuzzyModel *fuzzy.Model
- concurrencySemaphore chan struct{}
- taskCallCount map[string]*int32
- mkdirMutexMap map[string]*sync.Mutex
- executionHashes map[string]context.Context
- executionHashesMutex sync.Mutex
- }
- // Run runs Task
- func (e *Executor) Run(ctx context.Context, calls ...*ast.Call) error {
- // check if given tasks exist
- for _, call := range calls {
- task, err := e.GetTask(call)
- if err != nil {
- if _, ok := err.(*errors.TaskNotFoundError); ok {
- if _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
- return err
- }
- }
- return err
- }
- if task.Internal {
- if _, ok := err.(*errors.TaskNotFoundError); ok {
- if _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
- return err
- }
- }
- return &errors.TaskInternalError{TaskName: call.Task}
- }
- }
- if e.Summary {
- for i, c := range calls {
- compiledTask, err := e.FastCompiledTask(c)
- if err != nil {
- return nil
- }
- summary.PrintSpaceBetweenSummaries(e.Logger, i)
- summary.PrintTask(e.Logger, compiledTask)
- }
- return nil
- }
- regularCalls, watchCalls, err := e.splitRegularAndWatchCalls(calls...)
- if err != nil {
- return err
- }
- g, ctx := errgroup.WithContext(ctx)
- for _, c := range regularCalls {
- c := c
- if e.Parallel {
- g.Go(func() error { return e.RunTask(ctx, c) })
- } else {
- if err := e.RunTask(ctx, c); err != nil {
- return err
- }
- }
- }
- if err := g.Wait(); err != nil {
- return err
- }
- if len(watchCalls) > 0 {
- return e.watchTasks(watchCalls...)
- }
- return nil
- }
- func (e *Executor) splitRegularAndWatchCalls(calls ...*ast.Call) (regularCalls []*ast.Call, watchCalls []*ast.Call, err error) {
- for _, c := range calls {
- t, err := e.GetTask(c)
- if err != nil {
- return nil, nil, err
- }
- if e.Watch || t.Watch {
- watchCalls = append(watchCalls, c)
- } else {
- regularCalls = append(regularCalls, c)
- }
- }
- return
- }
- // RunTask runs a task by its name
- func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
- t, err := e.FastCompiledTask(call)
- if err != nil {
- return err
- }
- if !shouldRunOnCurrentPlatform(t.Platforms) {
- e.Logger.VerboseOutf(logger.Yellow, `task: %q not for current platform - ignored\n`, call.Task)
- return nil
- }
- t, err = e.CompiledTask(call)
- if err != nil {
- return err
- }
- if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall {
- return &errors.TaskCalledTooManyTimesError{
- TaskName: t.Task,
- MaximumTaskCall: MaximumTaskCall,
- }
- }
- release := e.acquireConcurrencyLimit()
- defer release()
- return e.startExecution(ctx, t, func(ctx context.Context) error {
- e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
- if err := e.runDeps(ctx, t); err != nil {
- return err
- }
- skipFingerprinting := e.ForceAll || (!call.Indirect && e.Force)
- if !skipFingerprinting {
- if err := ctx.Err(); err != nil {
- return err
- }
- if err := e.areTaskRequiredVarsSet(t, call); err != nil {
- return err
- }
- preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
- if err != nil {
- return err
- }
- // Get the fingerprinting method to use
- method := e.Taskfile.Method
- if t.Method != "" {
- method = t.Method
- }
- upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
- fingerprint.WithMethod(method),
- fingerprint.WithTempDir(e.TempDir.Fingerprint),
- fingerprint.WithDry(e.Dry),
- fingerprint.WithLogger(e.Logger),
- )
- if err != nil {
- return err
- }
- if upToDate && preCondMet {
- if e.Verbose || (!call.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
- e.Logger.Errf(logger.Magenta, "task: Task %q is up to date\n", t.Name())
- }
- return nil
- }
- }
- if t.Prompt != "" && !e.Dry {
- if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
- return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
- } else if errors.Is(err, logger.ErrPromptCancelled) {
- return &errors.TaskCancelledByUserError{TaskName: call.Task}
- } else if err != nil {
- return err
- }
- }
- if err := e.mkdir(t); err != nil {
- e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err)
- }
- var deferredExitCode uint8
- for i := range t.Cmds {
- if t.Cmds[i].Defer {
- defer e.runDeferred(t, call, i, &deferredExitCode)
- continue
- }
- if err := e.runCommand(ctx, t, call, i); err != nil {
- if err2 := e.statusOnError(t); err2 != nil {
- e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
- }
- exitCode, isExitError := interp.IsExitStatus(err)
- if isExitError {
- if t.IgnoreError {
- e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
- continue
- }
- deferredExitCode = exitCode
- }
- if call.Indirect {
- return err
- }
- return &errors.TaskRunError{TaskName: t.Task, Err: err}
- }
- }
- e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task)
- return nil
- })
- }
- func (e *Executor) mkdir(t *ast.Task) error {
- if t.Dir == "" {
- return nil
- }
- mutex := e.mkdirMutexMap[t.Task]
- mutex.Lock()
- defer mutex.Unlock()
- if _, err := os.Stat(t.Dir); os.IsNotExist(err) {
- if err := os.MkdirAll(t.Dir, 0o755); err != nil {
- return err
- }
- }
- return nil
- }
- func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
- g, ctx := errgroup.WithContext(ctx)
- reacquire := e.releaseConcurrencyLimit()
- defer reacquire()
- for _, d := range t.Deps {
- d := d
- g.Go(func() error {
- err := e.RunTask(ctx, &ast.Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
- if err != nil {
- return err
- }
- return nil
- })
- }
- return g.Wait()
- }
- func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) {
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- origTask, err := e.GetTask(call)
- if err != nil {
- return
- }
- cmd := t.Cmds[i]
- vars, _ := e.Compiler.GetVariables(origTask, call)
- cache := &templater.Cache{Vars: vars}
- extra := map[string]any{}
- if deferredExitCode != nil && *deferredExitCode > 0 {
- extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
- }
- cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
- if err := e.runCommand(ctx, t, call, i); err != nil {
- e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error())
- }
- }
- func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call, i int) error {
- cmd := t.Cmds[i]
- switch {
- case cmd.Task != "":
- reacquire := e.releaseConcurrencyLimit()
- defer reacquire()
- err := e.RunTask(ctx, &ast.Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
- if err != nil {
- return err
- }
- return nil
- case cmd.Cmd != "":
- if !shouldRunOnCurrentPlatform(cmd.Platforms) {
- e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.Cmd)
- return nil
- }
- if e.Verbose || (!call.Silent && !cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
- e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd)
- }
- if e.Dry {
- return nil
- }
- outputWrapper := e.Output
- if t.Interactive {
- outputWrapper = output.Interleaved{}
- }
- vars, err := e.Compiler.FastGetVariables(t, call)
- outputTemplater := &templater.Cache{Vars: vars}
- if err != nil {
- return fmt.Errorf("task: failed to get variables: %w", err)
- }
- stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
- err = execext.RunCommand(ctx, &execext.RunCommandOptions{
- Command: cmd.Cmd,
- Dir: t.Dir,
- Env: env.Get(t),
- PosixOpts: slicesext.UniqueJoin(e.Taskfile.Set, t.Set, cmd.Set),
- BashOpts: slicesext.UniqueJoin(e.Taskfile.Shopt, t.Shopt, cmd.Shopt),
- Stdin: e.Stdin,
- Stdout: stdOut,
- Stderr: stdErr,
- })
- if closeErr := close(err); closeErr != nil {
- e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
- }
- if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
- e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
- return nil
- }
- return err
- default:
- return nil
- }
- }
- func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func(ctx context.Context) error) error {
- h, err := e.GetHash(t)
- if err != nil {
- return err
- }
- if h == "" {
- return execute(ctx)
- }
- e.executionHashesMutex.Lock()
- if otherExecutionCtx, ok := e.executionHashes[h]; ok {
- e.executionHashesMutex.Unlock()
- e.Logger.VerboseErrf(logger.Magenta, "task: skipping execution of task: %s\n", h)
- // Release our execution slot to avoid blocking other tasks while we wait
- reacquire := e.releaseConcurrencyLimit()
- defer reacquire()
- <-otherExecutionCtx.Done()
- return nil
- }
- ctx, cancel := context.WithCancel(ctx)
- defer cancel()
- e.executionHashes[h] = ctx
- e.executionHashesMutex.Unlock()
- return execute(ctx)
- }
- // GetTask will return the task with the name matching the given call from the taskfile.
- // If no task is found, it will search for tasks with a matching alias.
- // If multiple tasks contain the same alias or no matches are found an error is returned.
- func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
- // Search for a matching task
- matchingTasks := e.Taskfile.Tasks.FindMatchingTasks(call)
- switch len(matchingTasks) {
- case 0: // Carry on
- case 1:
- if call.Vars == nil {
- call.Vars = &ast.Vars{}
- }
- call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards})
- return matchingTasks[0].Task, nil
- default:
- taskNames := make([]string, len(matchingTasks))
- for i, matchingTask := range matchingTasks {
- taskNames[i] = matchingTask.Task.Task
- }
- return nil, &errors.TaskNameConflictError{
- Call: call.Task,
- TaskNames: taskNames,
- }
- }
- // If didn't find one, search for a task with a matching alias
- var matchingTask *ast.Task
- var aliasedTasks []string
- for _, task := range e.Taskfile.Tasks.Values() {
- if slices.Contains(task.Aliases, call.Task) {
- aliasedTasks = append(aliasedTasks, task.Task)
- matchingTask = task
- }
- }
- // If we found multiple tasks
- if len(aliasedTasks) > 1 {
- return nil, &errors.TaskNameConflictError{
- Call: call.Task,
- TaskNames: aliasedTasks,
- }
- }
- // If we found no tasks
- if len(aliasedTasks) == 0 {
- didYouMean := ""
- if e.fuzzyModel != nil {
- didYouMean = e.fuzzyModel.SpellCheck(call.Task)
- }
- return nil, &errors.TaskNotFoundError{
- TaskName: call.Task,
- DidYouMean: didYouMean,
- }
- }
- return matchingTask, nil
- }
- type FilterFunc func(task *ast.Task) bool
- func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
- tasks := make([]*ast.Task, 0, e.Taskfile.Tasks.Len())
- // Create an error group to wait for each task to be compiled
- var g errgroup.Group
- // Filter tasks based on the given filter functions
- for _, task := range e.Taskfile.Tasks.Values() {
- var shouldFilter bool
- for _, filter := range filters {
- if filter(task) {
- shouldFilter = true
- }
- }
- if !shouldFilter {
- tasks = append(tasks, task)
- }
- }
- // Compile the list of tasks
- for i := range tasks {
- g.Go(func() error {
- compiledTask, err := e.FastCompiledTask(&ast.Call{Task: tasks[i].Task})
- if err != nil {
- return err
- }
- tasks[i] = compiledTask
- return nil
- })
- }
- // Wait for all the go routines to finish
- if err := g.Wait(); err != nil {
- return nil, err
- }
- // Sort the tasks
- if e.TaskSorter == nil {
- e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
- }
- e.TaskSorter.Sort(tasks)
- return tasks, nil
- }
- // FilterOutNoDesc removes all tasks that do not contain a description.
- func FilterOutNoDesc(task *ast.Task) bool {
- return task.Desc == ""
- }
- // FilterOutInternal removes all tasks that are marked as internal.
- func FilterOutInternal(task *ast.Task) bool {
- return task.Internal
- }
- func shouldRunOnCurrentPlatform(platforms []*ast.Platform) bool {
- if len(platforms) == 0 {
- return true
- }
- for _, p := range platforms {
- if (p.OS == "" || p.OS == runtime.GOOS) && (p.Arch == "" || p.Arch == runtime.GOARCH) {
- return true
- }
- }
- return false
- }
|