mirror of
https://github.com/go-task/task.git
synced 2026-02-25 04:33:43 +01:00
Store the TaskfileGraph in the Executor so it can be used for lazy variable resolution when SCOPED_INCLUDES experiment is enabled. The graph is now preserved after reading, before merging into the final Taskfile. This allows traversing the DAG at runtime to resolve variables from the correct scope.
321 lines
7.5 KiB
Go
321 lines
7.5 KiB
Go
package task
|
|
|
|
import (
|
|
"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/env"
|
|
"github.com/go-task/task/v3/internal/execext"
|
|
"github.com/go-task/task/v3/internal/filepathext"
|
|
"github.com/go-task/task/v3/internal/fsext"
|
|
"github.com/go-task/task/v3/internal/logger"
|
|
"github.com/go-task/task/v3/internal/output"
|
|
"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),
|
|
)
|
|
if os.IsNotExist(err) {
|
|
return nil, errors.TaskfileNotFoundError{
|
|
URI: fsext.DefaultDir(e.Entrypoint, e.Dir),
|
|
Walk: true,
|
|
AskInit: true,
|
|
}
|
|
}
|
|
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
|
|
}
|
|
e.Graph = graph
|
|
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
|
|
}
|
|
|
|
tempDir := env.GetTaskEnv("TEMP_DIR")
|
|
if tempDir == "" {
|
|
e.TempDir = TempDir{
|
|
Remote: filepathext.SmartJoin(e.Dir, ".task"),
|
|
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
|
|
}
|
|
} else 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,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|