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

502 lines
11 KiB
Go
Raw Normal View History

2017-02-27 14:48:50 +02:00
package task
2017-02-27 01:43:50 +02:00
import (
"context"
"errors"
"fmt"
"io"
2017-02-27 01:43:50 +02:00
"os"
2019-06-16 02:12:54 +02:00
"sync"
"sync/atomic"
2017-02-27 01:43:50 +02:00
"github.com/go-task/task/v3/internal/compiler"
compilerv2 "github.com/go-task/task/v3/internal/compiler/v2"
compilerv3 "github.com/go-task/task/v3/internal/compiler/v3"
"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/taskfile"
"github.com/go-task/task/v3/taskfile/read"
2017-03-12 22:18:59 +02:00
"golang.org/x/sync/errgroup"
2017-02-27 01:43:50 +02:00
)
const (
// MaximumTaskCall is the max number of times a task can be called.
// This exists to prevent infinite loops on cyclic dependencies
MaximumTaskCall = 100
)
2017-02-27 02:18:53 +02:00
// Executor executes a Taskfile
type Executor struct {
Taskfile *taskfile.Taskfile
2020-06-12 20:09:53 +02:00
Dir 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
2019-02-09 14:15:38 +02:00
Logger *logger.Logger
Compiler compiler.Compiler
Output output.Output
2019-02-05 08:42:57 +02:00
OutputStyle string
taskvars *taskfile.Vars
2017-07-06 02:03:59 +02:00
2020-06-12 20:09:53 +02:00
concurrencySemaphore chan struct{}
taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
2021-07-28 22:39:00 +02:00
executionHashes map[string]context.Context
executionHashesMutex sync.Mutex
}
2017-02-27 01:43:50 +02:00
2017-02-28 14:50:40 +02:00
// Run runs Task
2019-02-09 14:16:13 +02:00
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.PrintTasksHelp()
return &taskNotFoundError{taskName: c.Task}
}
}
2019-02-24 16:33:09 +02:00
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)
}
2019-02-24 10:24:57 +02:00
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()
}
2018-03-11 19:39:40 +02:00
// Setup setups Executor's internal state
func (e *Executor) Setup() error {
if e.Entrypoint == "" {
e.Entrypoint = "Taskfile.yml"
}
var err error
e.Taskfile, err = read.Taskfile(e.Dir, e.Entrypoint)
if err != nil {
return err
}
2020-05-17 20:34:32 +02:00
v, err := e.Taskfile.ParsedVersion()
if err != nil {
2018-03-11 19:39:40 +02:00
return err
}
2020-05-17 20:34:32 +02:00
if v < 3.0 {
e.taskvars, err = read.Taskvars(e.Dir)
if err != nil {
return err
}
}
if e.Stdin == nil {
e.Stdin = os.Stdin
}
if e.Stdout == nil {
e.Stdout = os.Stdout
}
if e.Stderr == nil {
e.Stderr = os.Stderr
}
e.Logger = &logger.Logger{
Stdout: e.Stdout,
Stderr: e.Stderr,
Verbose: e.Verbose,
Color: e.Color,
}
2019-08-18 17:37:21 +02:00
if v < 2 {
return fmt.Errorf(`task: Taskfile versions prior to v2 are not supported anymore`)
2019-08-18 17:37:21 +02:00
}
// consider as equal to the greater version if round
if v == 2.0 {
v = 2.6
}
if v == 3.0 {
v = 3.7
}
2021-07-28 21:50:29 +02:00
if v > 3.7 {
return fmt.Errorf(`task: Taskfile versions greater than v3.7 not implemented in the version of Task`)
2019-07-07 19:18:02 +02:00
}
// Color available only on v3
if v < 3 {
e.Logger.Color = false
}
if v < 3 {
e.Compiler = &compilerv2.CompilerV2{
Dir: e.Dir,
Taskvars: e.taskvars,
TaskfileVars: e.Taskfile.Vars,
Expansions: e.Taskfile.Expansions,
Logger: e.Logger,
}
} else {
e.Compiler = &compilerv3.CompilerV3{
Dir: e.Dir,
TaskfileEnv: e.Taskfile.Env,
TaskfileVars: e.Taskfile.Vars,
Logger: e.Logger,
}
}
if v >= 3.0 {
env, err := read.Dotenv(e.Compiler, e.Taskfile, e.Dir)
if err != nil {
return err
}
err = env.Range(func(key string, value taskfile.Var) error {
if _, ok := e.Taskfile.Env.Mapping[key]; !ok {
e.Taskfile.Env.Set(key, value)
}
return nil
})
if err != nil {
return err
}
}
if v < 2.1 && e.Taskfile.Output != "" {
return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
}
if v < 2.2 && e.Taskfile.Includes.Len() > 0 {
return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`)
}
if v >= 3.0 && e.Taskfile.Expansions > 2 {
return fmt.Errorf(`task: The "expansions" setting is not available anymore on v3.0`)
}
2019-02-05 08:42:57 +02:00
if e.OutputStyle != "" {
e.Taskfile.Output = e.OutputStyle
}
switch e.Taskfile.Output {
case "", "interleaved":
e.Output = output.Interleaved{}
case "group":
e.Output = output.Group{}
case "prefixed":
e.Output = output.Prefixed{}
default:
return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output)
}
if e.Taskfile.Method == "" {
if v >= 3 {
e.Taskfile.Method = "checksum"
} else {
e.Taskfile.Method = "timestamp"
}
}
if v <= 2.1 {
err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
for _, task := range e.Taskfile.Tasks {
if task.IgnoreError {
return err
}
for _, cmd := range task.Cmds {
if cmd.IgnoreError {
return err
}
}
}
}
if v < 2.6 {
for _, task := range e.Taskfile.Tasks {
if len(task.Preconditions) > 0 {
return errors.New(`task: Task option "preconditions" is only available starting on Taskfile version v2.6`)
}
}
}
if v < 3 {
err := e.Taskfile.Includes.Range(func(_ string, taskfile taskfile.IncludedTaskfile) error {
if taskfile.AdvancedImport {
return errors.New(`task: Import with additional parameters is only available starting on Taskfile version v3`)
}
return nil
})
if err != nil {
return err
}
2021-07-28 21:50:29 +02:00
}
if v < 3.7 {
if e.Taskfile.Run != "" {
return errors.New(`task: Setting the "run" type is only available starting on Taskfile version v3.7`)
}
for _, task := range e.Taskfile.Tasks {
if task.Run != "" {
2021-07-28 21:50:29 +02:00
return errors.New(`task: Setting the "run" type is only available starting on Taskfile version v3.7`)
}
}
}
2021-07-28 21:50:29 +02:00
if e.Taskfile.Run == "" {
e.Taskfile.Run = "always"
}
2021-07-28 22:39:00 +02:00
e.executionHashes = make(map[string]context.Context)
2020-08-17 21:25:17 +02:00
e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
2019-06-16 02:12:54 +02:00
e.mkdirMutexMap = make(map[string]*sync.Mutex, len(e.Taskfile.Tasks))
for k := range e.Taskfile.Tasks {
e.taskCallCount[k] = new(int32)
2019-06-16 02:12:54 +02:00
e.mkdirMutexMap[k] = &sync.Mutex{}
}
2020-06-12 20:09:53 +02:00
if e.Concurrency > 0 {
e.concurrencySemaphore = make(chan struct{}, e.Concurrency)
}
return nil
2017-02-27 02:18:53 +02:00
}
2017-02-27 01:43:50 +02:00
2017-02-28 14:50:40 +02:00
// 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}
}
2020-06-12 20:09:53 +02:00
release := e.acquireConcurrencyLimit()
defer release()
2021-07-28 22:39:00 +02:00
return e.startExecution(ctx, t, func(ctx context.Context) error {
if err := e.runDeps(ctx, t); err != nil {
return err
}
2019-05-17 22:13:47 +02:00
2021-07-28 22:39:00 +02:00
if !e.Force {
preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
if err != nil {
return err
}
2019-05-17 22:13:47 +02:00
2021-07-28 22:39:00 +02:00
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
}
}
2017-03-05 11:15:49 +02:00
2021-07-28 22:39:00 +02:00
if err := e.mkdir(t); err != nil {
e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v", t.Dir, err)
}
2021-07-28 22:39:00 +02:00
for i := range t.Cmds {
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)
}
2018-08-05 17:53:42 +02:00
2021-07-28 22:39:00 +02:00
if execext.IsExitError(err) && t.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v", err)
continue
}
2018-08-05 17:53:42 +02:00
2021-07-28 22:39:00 +02:00
return &taskRunError{t.Task, err}
}
2017-02-27 01:43:50 +02:00
}
2021-07-28 22:39:00 +02:00
return nil
})
2017-02-27 01:43:50 +02:00
}
2019-06-16 02:12:54 +02:00
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)
2017-03-16 01:19:29 +02:00
2020-06-12 20:09:53 +02:00
reacquire := e.releaseConcurrencyLimit()
defer reacquire()
2017-03-16 01:19:29 +02:00
for _, d := range t.Deps {
2017-07-02 20:30:50 +02:00
d := d
2017-03-16 01:19:29 +02:00
g.Go(func() error {
2019-05-17 22:13:47 +02:00
err := e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
if err != nil {
2019-05-28 22:02:59 +02:00
return err
2019-05-17 22:13:47 +02:00
}
return nil
})
2017-03-16 01:19:29 +02:00
}
2017-07-08 20:08:44 +02:00
return g.Wait()
2017-03-16 01:19:29 +02:00
}
func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfile.Call, i int) error {
2017-07-02 20:30:50 +02:00
cmd := t.Cmds[i]
2018-06-24 15:29:46 +02:00
switch {
case cmd.Task != "":
2020-06-12 20:09:53 +02:00
reacquire := e.releaseConcurrencyLimit()
defer reacquire()
2019-05-17 22:13:47 +02:00
err := e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
if err != nil {
2019-05-28 22:02:59 +02:00
return err
2019-05-17 22:13:47 +02:00
}
return nil
2018-06-24 15:29:46 +02:00
case cmd.Cmd != "":
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
2020-11-05 22:27:42 +02:00
e.Logger.Errf(logger.Green, "task: [%s] %s", t.Name(), cmd.Cmd)
2018-06-24 15:29:46 +02:00
}
2017-03-25 20:26:42 +02:00
2018-08-05 16:28:02 +02:00
if e.Dry {
return nil
}
2018-06-24 15:29:46 +02:00
stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix)
stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix)
defer func() {
if _, ok := stdOut.(*os.File); !ok {
if closer, ok := stdOut.(io.Closer); ok {
closer.Close()
}
}
if _, ok := stdErr.(*os.File); !ok {
if closer, ok := stdErr.(io.Closer); ok {
closer.Close()
}
}
}()
2018-06-24 15:29:46 +02:00
2018-09-01 16:02:23 +02:00
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.Cmd,
Dir: t.Dir,
Env: getEnviron(t),
Stdin: e.Stdin,
Stdout: stdOut,
Stderr: stdErr,
2018-06-24 15:29:46 +02:00
})
2018-09-01 16:02:23 +02:00
if execext.IsExitError(err) && cmd.IgnoreError {
2020-11-05 22:27:42 +02:00
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v", t.Name(), err)
return nil
}
return err
2018-06-24 15:29:46 +02:00
default:
return nil
2017-09-16 19:05:07 +02:00
}
}
func getEnviron(t *taskfile.Task) []string {
if t.Env == nil {
return nil
}
environ := os.Environ()
2019-08-25 22:16:59 +02:00
for k, v := range t.Env.ToCacheMap() {
str, isString := v.(string)
if !isString {
continue
}
if _, alreadySet := os.LookupEnv(k); alreadySet {
continue
2019-09-14 22:54:41 +02:00
}
environ = append(environ, fmt.Sprintf("%s=%s", k, str))
}
return environ
}
2020-08-17 21:25:17 +02:00
2021-07-28 22:39:00 +02:00
func (e *Executor) startExecution(ctx context.Context, t *taskfile.Task, execute func(ctx context.Context) error) error {
2020-08-17 21:25:17 +02:00
h, err := e.GetHash(t)
if err != nil {
2021-07-28 22:39:00 +02:00
return err
2020-08-17 21:25:17 +02:00
}
if h == "" {
2021-07-28 22:39:00 +02:00
return execute(ctx)
2020-08-17 21:25:17 +02:00
}
2021-07-28 22:39:00 +02:00
e.executionHashesMutex.Lock()
otherExecutionCtx, ok := e.executionHashes[h]
2020-08-17 21:25:17 +02:00
if ok {
e.executionHashesMutex.Unlock()
e.Logger.VerboseErrf(logger.Magenta, "task: skipping execution of task: %s", h)
2021-07-28 22:39:00 +02:00
<-otherExecutionCtx.Done()
return nil
2020-08-17 21:25:17 +02:00
}
2021-07-28 22:39:00 +02:00
ctx, cancel := context.WithCancel(ctx)
defer cancel()
2021-07-28 22:39:00 +02:00
e.executionHashes[h] = ctx
e.executionHashesMutex.Unlock()
2021-07-28 22:39:00 +02:00
return execute(ctx)
2020-08-17 21:25:17 +02:00
}