package task import ( "context" "fmt" "io" "os" "sync" "sync/atomic" "github.com/go-task/task/v3/internal/compiler" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/summary" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile" "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 = 100 ) // Executor executes a Taskfile type Executor struct { Taskfile *taskfile.Taskfile Dir string TempDir string Entrypoint string Force bool Watch bool Verbose bool Silent bool Dry bool Summary bool Parallel bool Color bool Concurrency int Stdin io.Reader Stdout io.Writer Stderr io.Writer Logger *logger.Logger Compiler compiler.Compiler Output output.Output OutputStyle taskfile.Output taskvars *taskfile.Vars 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 ...taskfile.Call) error { // check if given tasks exist for _, c := range calls { if _, ok := e.Taskfile.Tasks[c.Task]; !ok { // FIXME: move to the main package e.ListTasksWithDesc() return &taskNotFoundError{taskName: c.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 } if e.Watch { return e.watchTasks(calls...) } g, ctx := errgroup.WithContext(ctx) for _, c := range calls { 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 } } } return g.Wait() } // RunTask runs a task by its name func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { t, err := e.CompiledTask(call) if err != nil { return err } if !e.Watch && atomic.AddInt32(e.taskCallCount[call.Task], 1) >= MaximumTaskCall { return &MaximumTaskCallExceededError{task: call.Task} } release := e.acquireConcurrencyLimit() defer release() return e.startExecution(ctx, t, func(ctx context.Context) error { e.Logger.VerboseErrf(logger.Magenta, `task: "%s" started`, call.Task) if err := e.runDeps(ctx, t); err != nil { return err } if !e.Force { if err := ctx.Err(); err != nil { return err } preCondMet, err := e.areTaskPreconditionsMet(ctx, t) if err != nil { return err } upToDate, err := e.isTaskUpToDate(ctx, t) if err != nil { return err } if upToDate && preCondMet { if !e.Silent { e.Logger.Errf(logger.Magenta, `task: Task "%s" is up to date`, t.Name()) } return nil } } if err := e.mkdir(t); err != nil { e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v", t.Dir, err) } for i := range t.Cmds { if t.Cmds[i].Defer { defer e.runDeferred(t, call, i) 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", err2) } if execext.IsExitError(err) && t.IgnoreError { e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v", err) continue } return &TaskRunError{t.Task, err} } } e.Logger.VerboseErrf(logger.Magenta, `task: "%s" finished`, call.Task) return nil }) } func (e *Executor) mkdir(t *taskfile.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 *taskfile.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, taskfile.Call{Task: d.Task, Vars: d.Vars}) if err != nil { return err } return nil }) } return g.Wait() } func (e *Executor) runDeferred(t *taskfile.Task, call taskfile.Call, i int) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() if err := e.runCommand(ctx, t, call, i); err != nil { e.Logger.VerboseErrf(logger.Yellow, `task: ignored error in deferred cmd: %s`, err.Error()) } } func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfile.Call, i int) error { cmd := t.Cmds[i] switch { case cmd.Task != "": reacquire := e.releaseConcurrencyLimit() defer reacquire() err := e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars}) if err != nil { return err } return nil case cmd.Cmd != "": if e.Verbose || (!cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) { e.Logger.Errf(logger.Green, "task: [%s] %s", 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.Templater{Vars: vars, RemoveNoValue: true} 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) defer func() { if err := close(); err != nil { e.Logger.Errf(logger.Red, "task: unable to close writter: %v", err) } }() err = execext.RunCommand(ctx, &execext.RunCommandOptions{ Command: cmd.Cmd, Dir: t.Dir, Env: getEnviron(t), Stdin: e.Stdin, Stdout: stdOut, Stderr: stdErr, }) if execext.IsExitError(err) && cmd.IgnoreError { e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v", t.Name(), err) return nil } return err default: return nil } } func getEnviron(t *taskfile.Task) []string { if t.Env == nil { return nil } environ := os.Environ() for k, v := range t.Env.ToCacheMap() { str, isString := v.(string) if !isString { continue } if _, alreadySet := os.LookupEnv(k); alreadySet { continue } environ = append(environ, fmt.Sprintf("%s=%s", k, str)) } return environ } func (e *Executor) startExecution(ctx context.Context, t *taskfile.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() otherExecutionCtx, ok := e.executionHashes[h] if ok { e.executionHashesMutex.Unlock() e.Logger.VerboseErrf(logger.Magenta, "task: skipping execution of task: %s", h) <-otherExecutionCtx.Done() return nil } ctx, cancel := context.WithCancel(ctx) defer cancel() e.executionHashes[h] = ctx e.executionHashesMutex.Unlock() return execute(ctx) }