From c600bf571c4f903c72a06629c4b63c968e39c6cd Mon Sep 17 00:00:00 2001 From: Timothy Rule <34501912+trulede@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:45:08 +0100 Subject: [PATCH] Refactor compiler and add variable logging. --- compiler.go | 243 ++++++++++++------ internal/logger/logger.go | 4 + testdata/compiler/debug_compiler/Taskfile.yml | 46 ++++ 3 files changed, 212 insertions(+), 81 deletions(-) create mode 100644 testdata/compiler/debug_compiler/Taskfile.yml diff --git a/compiler.go b/compiler.go index 311fd58423..f1c63e9d9a 100644 --- a/compiler.go +++ b/compiler.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "strings" "sync" @@ -32,116 +33,192 @@ type Compiler struct { muDynamicCache sync.Mutex } +type mergeProc func() error + +type mergeVars func() *ast.Vars + +type mergeItem struct { + name string // Name of the merge item, for logging. + cond bool // Indicates if this mergeItem should be processed. + vars mergeVars // Variables to be merged (overwrite existing). + dir *string // Directory used when evaluating variables. + proc mergeProc // Called to modify state between merge items. +} + +var ( + enableDebug = os.Getenv("TASK_DEBUG_COMPILER") + entryOsEnv = env.GetEnviron() +) + +func (c *Compiler) logf(s string, args ...any) { + if enableDebug != "" { + c.Logger.VerboseErrf(logger.Grey, s, args...) + } +} + func (c *Compiler) GetTaskfileVariables() (*ast.Vars, error) { + c.logf("GetTaskfileVariables:\n") return c.getVariables(nil, nil, true) } func (c *Compiler) GetVariables(t *ast.Task, call *Call) (*ast.Vars, error) { + c.logf("GetVariables: task=%s, call=%s\n", + func() string { + if t == nil { + return "" + } + return t.Name() + }(), + func() string { + if call == nil { + return "" + } + return call.Task + }(), + ) return c.getVariables(t, call, true) } func (c *Compiler) FastGetVariables(t *ast.Task, call *Call) (*ast.Vars, error) { + c.logf("FastGetVariables: task=%s, call=%s\n", + func() string { + if t == nil { + return "" + } + return t.Name() + }(), + func() string { + if call == nil { + return "" + } + return call.Task + }(), + ) return c.getVariables(t, call, false) } -func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) { - result := env.GetEnviron() - specialVars, err := c.getSpecialVars(t, call) - if err != nil { - return nil, err - } - for k, v := range specialVars { - result.Set(k, ast.Var{Value: v}) - } +func (c *Compiler) resolveAndSetVar(result *ast.Vars, k string, v ast.Var, dir string, evaluateSh bool) error { + cache := &templater.Cache{Vars: result} + newVar := templater.ReplaceVar(v, cache) - getRangeFunc := func(dir string) func(k string, v ast.Var) error { - return func(k string, v ast.Var) error { - cache := &templater.Cache{Vars: result} - // Replace values - newVar := templater.ReplaceVar(v, cache) - // If the variable should not be evaluated, but is nil, set it to an empty string - // This stops empty interface errors when using the templater to replace values later - // Preserve the Sh field so it can be displayed in summary - if !evaluateShVars && newVar.Value == nil { - result.Set(k, ast.Var{Value: "", Sh: newVar.Sh}) - return nil - } - // If the variable should not be evaluated and it is set, we can set it and return - if !evaluateShVars { - result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh}) - return nil - } - // Now we can check for errors since we've handled all the cases when we don't want to evaluate - if err := cache.Err(); err != nil { - return err - } - // If the variable is already set, we can set it and return - if newVar.Value != nil || newVar.Sh == nil { - result.Set(k, ast.Var{Value: newVar.Value}) - return nil - } - // If the variable is dynamic, we need to resolve it first - static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result)) - if err != nil { - return err + set := func(key string, value ast.Var) { + result.Set(key, value) + if enableDebug != "" { + _v, found := entryOsEnv.Get(key) + if !found || !reflect.DeepEqual(_v, value) { + valStr := fmt.Sprintf("%v", value.Value) + if strings.Contains(valStr, "\n") { + indent := strings.Repeat(" ", 6) + valStr = strings.ReplaceAll("\n"+valStr, "\n", "\n"+indent) + } + c.logf(" %s <-- %v\n", k, valStr) } - result.Set(k, ast.Var{Value: static}) - return nil } } - rangeFunc := getRangeFunc(c.Dir) - var taskRangeFunc func(k string, v ast.Var) error - if t != nil { - // NOTE(@andreynering): We're manually joining these paths here because - // this is the raw task, not the compiled one. - cache := &templater.Cache{Vars: result} - dir := templater.Replace(t.Dir, cache) - if err := cache.Err(); err != nil { - return nil, err + // Templating only (no shell evaluation). + if !evaluateSh { + if newVar.Value == nil { + // If the variable should not be evaluated, but is nil, set it to an empty string. + newVar.Value = "" } - dir = filepathext.SmartJoin(c.Dir, dir) - taskRangeFunc = getRangeFunc(dir) + set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh}) + return nil + } + // Check cache error condition before continuing. + if err := cache.Err(); err != nil { + return err + } + // Variable already set, use use its value. + if newVar.Value != nil || newVar.Sh == nil { + set(k, ast.Var{Value: newVar.Value}) + return nil } + // Resolve the variable. + c.logf(" --> %s (v.Dir=%s, dir=%s)\n", *newVar.Sh, newVar.Dir, dir) + if static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result)); err == nil { + set(k, ast.Var{Value: static}) + } else { + return err + } + return nil +} - for k, v := range c.TaskfileEnv.All() { - if err := rangeFunc(k, v); err != nil { - return nil, err - } +func (c *Compiler) mergeVars(dest *ast.Vars, source *ast.Vars, dir string, evaluateShVars bool) error { + if source == nil || dest == nil { + return nil } - for k, v := range c.TaskfileVars.All() { - if err := rangeFunc(k, v); err != nil { - return nil, err + for k, v := range source.All() { + if err := c.resolveAndSetVar(dest, k, v, dir, evaluateShVars); err != nil { + return err } } - if t != nil { - for k, v := range t.IncludeVars.All() { - if err := rangeFunc(k, v); err != nil { - return nil, err + return nil +} + +func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) { + result := ast.NewVars() + taskdir := "" + taskOnly := (t != nil) + taskCall := (t != nil && call != nil) + + processMergeItem := func(items []mergeItem) error { + for _, m := range items { + if m.proc != nil { + if err := m.proc(); err != nil { + return err + } } - } - for k, v := range t.IncludedTaskfileVars.All() { - if err := taskRangeFunc(k, v); err != nil { - return nil, err + if !m.cond { + continue + } + c.logf(" compiler: variable merge: %s\n", m.name) + if m.vars == nil { + continue + } + dir := c.Dir + if m.dir != nil { + dir = *m.dir + } + evalSh := evaluateShVars + if m.name == "SpecialVars" || m.name == "OS.Env" { + evalSh = false + } + if err := c.mergeVars(result, m.vars(), dir, evalSh); err != nil { + return err } } + return nil } - - if t == nil || call == nil { - return result, nil - } - - for k, v := range call.Vars.All() { - if err := rangeFunc(k, v); err != nil { - return nil, err + updateTaskdir := func() error { + if t != nil { + cache := &templater.Cache{Vars: result} + dir := templater.Replace(t.Dir, cache) + if err := cache.Err(); err != nil { + return err + } + taskdir = filepathext.SmartJoin(c.Dir, dir) } + return nil } - for k, v := range t.Vars.All() { - if err := taskRangeFunc(k, v); err != nil { - return nil, err - } + resolveGlobalVarRefs := func() error { + return nil } + if err := processMergeItem([]mergeItem{ + {"OS.Env", true, func() *ast.Vars { return env.GetEnviron() }, nil, nil}, + {"SpecialVars", true, func() *ast.Vars { return c.getSpecialVars(t, call) }, nil, nil}, + {proc: updateTaskdir}, + {"Taskfile.Env", true, func() *ast.Vars { return c.TaskfileEnv }, nil, nil}, + {"Taskfile.Vars", true, func() *ast.Vars { return c.TaskfileVars }, nil, nil}, + {proc: resolveGlobalVarRefs}, + {"Inc.Vars", taskOnly, func() *ast.Vars { return t.IncludeVars }, nil, nil}, + {"IncTaskfile.Vars", taskOnly, func() *ast.Vars { return t.IncludedTaskfileVars }, &taskdir, nil}, + {"Call.Vars", taskCall, func() *ast.Vars { return call.Vars }, nil, nil}, + {"Task.Vars", taskCall, func() *ast.Vars { return t.Vars }, &taskdir, nil}, + }); err != nil { + return nil, err + } return result, nil } @@ -197,7 +274,7 @@ func (c *Compiler) ResetCache() { c.dynamicCache = nil } -func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, error) { +func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) *ast.Vars { allVars := map[string]string{ "TASK_EXE": filepath.ToSlash(os.Args[0]), "ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint), @@ -222,5 +299,9 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e allVars["ALIAS"] = "" } - return allVars, nil + vars := ast.NewVars() + for k, v := range allVars { + vars.Set(k, ast.Var{Value: v}) + } + return vars } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 2c962f4ea3..b7b3c7fd82 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -100,6 +100,10 @@ func BrightRed() PrintFunc { return color.New(attrsFgHiRed...).FprintfFunc() } +func Grey() PrintFunc { + return color.RGB(128, 128, 128).FprintfFunc() +} + func envColor(name string, defaultColor color.Attribute) []color.Attribute { // Fetch the environment variable override := env.GetTaskEnv(name) diff --git a/testdata/compiler/debug_compiler/Taskfile.yml b/testdata/compiler/debug_compiler/Taskfile.yml new file mode 100644 index 0000000000..5ff5842790 --- /dev/null +++ b/testdata/compiler/debug_compiler/Taskfile.yml @@ -0,0 +1,46 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json + +version: '3' + +silent: true +env: + TASKFILE_ENV: taskfile_env +vars: + TASKFILE_VAR: taskfile_var + +tasks: + default: + deps: + - task: dep + vars: + DEP_VAR: dep_var + env: + TASK_ENV: task_env + vars: + TASK_VAR: task_var + cmds: + - echo "{{.TASK}} > TASKFILE_ENV={{.TASKFILE_ENV}}" + - echo "{{.TASK}} > TASKFILE_VAR={{.TASKFILE_VAR}}" + - echo "{{.TASK}} > TASK_ENV=$TASK_ENV" + - echo "{{.TASK}} > TASK_VAR={{.TASK_VAR}}" + - echo "{{.TASK}} > CALL_VAR={{.CALL_VAR}}" + - echo "{{.TASK}} > DEP_VAR={{.DEP_VAR}}" + - task: call + vars: + CALL_VAR: call_var + call: + cmds: + - echo "{{.TASK}} > TASKFILE_ENV={{.TASKFILE_ENV}}" + - echo "{{.TASK}} > TASKFILE_VAR={{.TASKFILE_VAR}}" + - echo "{{.TASK}} > TASK_ENV=$TASK_ENV" + - echo "{{.TASK}} > TASK_VAR={{.TASK_VAR}}" + - echo "{{.TASK}} > CALL_VAR={{.CALL_VAR}}" + - echo "{{.TASK}} > DEP_VAR={{.DEP_VAR}}" + dep: + cmds: + - echo "{{.TASK}} > TASKFILE_ENV={{.TASKFILE_ENV}}" + - echo "{{.TASK}} > TASKFILE_VAR={{.TASKFILE_VAR}}" + - echo "{{.TASK}} > TASK_ENV=$TASK_ENV" + - echo "{{.TASK}} > TASK_VAR={{.TASK_VAR}}" + - echo "{{.TASK}} > CALL_VAR={{.CALL_VAR}}" + - echo "{{.TASK}} > DEP_VAR={{.DEP_VAR}}"