Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 162 additions & 81 deletions compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"sync"

Expand All @@ -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 "<nil>"
}
return t.Name()
}(),
func() string {
if call == nil {
return "<nil>"
}
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 "<nil>"
}
return t.Name()
}(),
func() string {
if call == nil {
return "<nil>"
}
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
}

Expand Down Expand Up @@ -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),
Expand All @@ -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
}
4 changes: 4 additions & 0 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions testdata/compiler/debug_compiler/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -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}}"
Loading