mirror of
https://github.com/go-task/task.git
synced 2026-09-02 04:02:08 +02:00
sprout logs its deprecation notices to stdout by default, which would corrupt the JSON and group output styles. Route them to the Executor's logger at verbose level instead, deduplicated so that a single deprecated call is not reported once per compilation pass.
317 lines
7.4 KiB
Go
317 lines
7.4 KiB
Go
package task
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/sajari/fuzzy"
|
|
|
|
"github.com/go-task/task/v3/errors"
|
|
"github.com/go-task/task/v3/internal/execext"
|
|
"github.com/go-task/task/v3/internal/filepathext"
|
|
"github.com/go-task/task/v3/internal/logger"
|
|
"github.com/go-task/task/v3/internal/output"
|
|
"github.com/go-task/task/v3/internal/templater"
|
|
"github.com/go-task/task/v3/internal/version"
|
|
"github.com/go-task/task/v3/taskfile"
|
|
"github.com/go-task/task/v3/taskfile/ast"
|
|
)
|
|
|
|
func (e *Executor) Setup() error {
|
|
e.setupLogger()
|
|
node, err := e.getRootNode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := e.setupTempDir(); err != nil {
|
|
return err
|
|
}
|
|
if err := e.readTaskfile(node); err != nil {
|
|
return err
|
|
}
|
|
e.setupStdFiles()
|
|
if err := e.setupOutput(); err != nil {
|
|
return err
|
|
}
|
|
if err := e.setupCompiler(); err != nil {
|
|
return err
|
|
}
|
|
if err := e.readDotEnvFiles(); err != nil {
|
|
return err
|
|
}
|
|
if err := e.doVersionChecks(); err != nil {
|
|
return err
|
|
}
|
|
e.setupDefaults()
|
|
e.setupConcurrencyState()
|
|
return nil
|
|
}
|
|
|
|
func (e *Executor) getRootNode() (taskfile.Node, error) {
|
|
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout,
|
|
taskfile.WithCACert(e.CACert),
|
|
taskfile.WithCert(e.Cert),
|
|
taskfile.WithCertKey(e.CertKey),
|
|
)
|
|
var taskNotFoundError errors.TaskfileNotFoundError
|
|
if errors.As(err, &taskNotFoundError) {
|
|
taskNotFoundError.AskInit = true
|
|
return nil, taskNotFoundError
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.Dir = node.Dir()
|
|
e.Entrypoint = node.Location()
|
|
return node, err
|
|
}
|
|
|
|
func (e *Executor) readTaskfile(node taskfile.Node) error {
|
|
ctx, cf := context.WithTimeout(context.Background(), e.Timeout)
|
|
defer cf()
|
|
debugFunc := func(s string) {
|
|
e.Logger.VerboseOutf(logger.Magenta, s)
|
|
}
|
|
promptFunc := func(s string) error {
|
|
return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes")
|
|
}
|
|
reader := taskfile.NewReader(
|
|
taskfile.WithInsecure(e.Insecure),
|
|
taskfile.WithDownload(e.Download),
|
|
taskfile.WithOffline(e.Offline),
|
|
taskfile.WithTrustedHosts(e.TrustedHosts),
|
|
taskfile.WithTempDir(e.TempDir.Remote),
|
|
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
|
|
taskfile.WithReaderCACert(e.CACert),
|
|
taskfile.WithReaderCert(e.Cert),
|
|
taskfile.WithReaderCertKey(e.CertKey),
|
|
taskfile.WithDebugFunc(debugFunc),
|
|
taskfile.WithPromptFunc(promptFunc),
|
|
)
|
|
graph, err := reader.Read(ctx, node)
|
|
if err != nil {
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: e.Timeout}
|
|
}
|
|
return err
|
|
}
|
|
if e.Taskfile, err = graph.Merge(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Executor) setupFuzzyModel() {
|
|
if e.Taskfile == nil {
|
|
return
|
|
}
|
|
|
|
model := fuzzy.NewModel()
|
|
model.SetThreshold(1) // because we want to build grammar based on every task name
|
|
|
|
var words []string
|
|
for name, task := range e.Taskfile.Tasks.All(nil) {
|
|
if task.Internal {
|
|
continue
|
|
}
|
|
words = append(words, name)
|
|
words = slices.Concat(words, task.Aliases)
|
|
}
|
|
|
|
model.Train(words)
|
|
e.fuzzyModel = model
|
|
}
|
|
|
|
func (e *Executor) setupTempDir() error {
|
|
if e.TempDir != (TempDir{}) {
|
|
return nil
|
|
}
|
|
|
|
// e.TempDirPath carries the resolved CLI precedence (flag > TASK_TEMP_DIR > taskrc).
|
|
tempDir := cmp.Or(e.TempDirPath, ".task")
|
|
if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
|
|
tempDir, err := execext.ExpandLiteral(tempDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
projectDir, _ := filepath.Abs(e.Dir)
|
|
projectName := filepath.Base(projectDir)
|
|
e.TempDir = TempDir{
|
|
Remote: tempDir,
|
|
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
|
|
}
|
|
|
|
} else {
|
|
e.TempDir = TempDir{
|
|
Remote: filepathext.SmartJoin(e.Dir, tempDir),
|
|
Fingerprint: filepathext.SmartJoin(e.Dir, tempDir),
|
|
}
|
|
}
|
|
|
|
// RemoteCacheDir from taskrc/env can override the remote cache directory
|
|
if e.RemoteCacheDir != "" {
|
|
if filepath.IsAbs(e.RemoteCacheDir) || strings.HasPrefix(e.RemoteCacheDir, "~") {
|
|
remoteCacheDir, err := execext.ExpandLiteral(e.RemoteCacheDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.TempDir.Remote = remoteCacheDir
|
|
} else {
|
|
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, e.RemoteCacheDir)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Executor) setupStdFiles() {
|
|
if e.Stdin == nil {
|
|
e.Stdin = os.Stdin
|
|
}
|
|
if e.Stdout == nil {
|
|
e.Stdout = os.Stdout
|
|
}
|
|
if e.Stderr == nil {
|
|
e.Stderr = os.Stderr
|
|
}
|
|
}
|
|
|
|
func (e *Executor) setupLogger() {
|
|
e.Logger = &logger.Logger{
|
|
Stdin: e.Stdin,
|
|
Stdout: e.Stdout,
|
|
Stderr: e.Stderr,
|
|
Verbose: e.Verbose,
|
|
Color: e.Color,
|
|
AssumeYes: e.AssumeYes,
|
|
AssumeTerm: e.AssumeTerm,
|
|
}
|
|
templater.SetNoticeSink(func(format string, args ...any) {
|
|
e.Logger.VerboseErrf(logger.Yellow, format, args...)
|
|
})
|
|
}
|
|
|
|
func (e *Executor) setupOutput() error {
|
|
if !e.OutputStyle.IsSet() {
|
|
e.OutputStyle = e.Taskfile.Output
|
|
}
|
|
|
|
var err error
|
|
e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger)
|
|
return err
|
|
}
|
|
|
|
func (e *Executor) setupCompiler() error {
|
|
if e.UserWorkingDir == "" {
|
|
var err error
|
|
e.UserWorkingDir, err = os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
e.Compiler = &Compiler{
|
|
Dir: e.Dir,
|
|
Entrypoint: e.Entrypoint,
|
|
UserWorkingDir: e.UserWorkingDir,
|
|
TaskfileEnv: e.Taskfile.Env,
|
|
TaskfileVars: e.Taskfile.Vars,
|
|
Logger: e.Logger,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Executor) readDotEnvFiles() error {
|
|
if e.Taskfile == nil || len(e.Taskfile.Dotenv) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if e.Taskfile.Version.LessThan(ast.V3) {
|
|
return nil
|
|
}
|
|
|
|
vars, err := e.Compiler.GetTaskfileVariables()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
env, err := taskfile.Dotenv(vars, e.Taskfile, e.Dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for k, v := range env.All() {
|
|
if _, ok := e.Taskfile.Env.Get(k); !ok {
|
|
e.Taskfile.Env.Set(k, v)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (e *Executor) setupDefaults() {
|
|
if e.Taskfile.Method == "" {
|
|
e.Taskfile.Method = "checksum"
|
|
}
|
|
if e.Taskfile.Run == "" {
|
|
e.Taskfile.Run = "always"
|
|
}
|
|
}
|
|
|
|
func (e *Executor) setupConcurrencyState() {
|
|
e.executionHashes = make(map[string]context.Context)
|
|
|
|
e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len())
|
|
e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len())
|
|
for k := range e.Taskfile.Tasks.Keys(nil) {
|
|
e.taskCallCount[k] = new(int32)
|
|
e.mkdirMutexMap[k] = &sync.Mutex{}
|
|
}
|
|
|
|
if e.Concurrency > 0 {
|
|
e.concurrencySemaphore = make(chan struct{}, e.Concurrency)
|
|
}
|
|
}
|
|
|
|
func (e *Executor) doVersionChecks() error {
|
|
if !e.EnableVersionCheck {
|
|
return nil
|
|
}
|
|
// Copy the version to avoid modifying the original
|
|
schemaVersion := &semver.Version{}
|
|
*schemaVersion = *e.Taskfile.Version
|
|
|
|
// Error if the Taskfile uses a schema version below v3
|
|
if schemaVersion.LessThan(ast.V3) {
|
|
return &errors.TaskfileVersionCheckError{
|
|
URI: e.Taskfile.Location,
|
|
SchemaVersion: schemaVersion,
|
|
Message: `no longer supported. Please use v3 or above`,
|
|
}
|
|
}
|
|
|
|
// Get the current version of Task
|
|
// If we can't parse the version (e.g. when its "devel"), then ignore the current version checks
|
|
currentVersion, err := semver.NewVersion(version.GetVersion())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Error if the Taskfile uses a schema version above the current version of Task
|
|
if schemaVersion.GreaterThan(currentVersion) {
|
|
return &errors.TaskfileVersionCheckError{
|
|
URI: e.Taskfile.Location,
|
|
SchemaVersion: schemaVersion,
|
|
Message: fmt.Sprintf(`is greater than the current version of Task (%s)`, currentVersion.String()),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|