Files
task/setup.go
Valentin Maerten 9732f7e08b feat(executor): store TaskfileGraph for lazy resolution
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.
2026-01-25 19:49:16 +01:00

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
}