1
0
mirror of https://github.com/go-task/task.git synced 2024-12-12 10:45:49 +02:00
task/task.go

328 lines
7.2 KiB
Go

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, 0755); 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)
}