From cbcebdef5df323c7059b3acb723d893c9d9b1c17 Mon Sep 17 00:00:00 2001 From: lawrence3699 Date: Sun, 19 Apr 2026 19:44:08 +1000 Subject: [PATCH] fix: refresh for-loop commands after deps --- executor_test.go | 41 ++++++++++++++++++ task.go | 12 ++++++ variables.go | 107 ++++++++++++++++++++++++++--------------------- 3 files changed, 112 insertions(+), 48 deletions(-) diff --git a/executor_test.go b/executor_test.go index d963fa6d3f..92a16bda15 100644 --- a/executor_test.go +++ b/executor_test.go @@ -894,6 +894,47 @@ func TestForCmds(t *testing.T) { } } +func TestForCmdsRecompileAfterDeps(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte(` +version: '3' + +tasks: + create-deps: + cmds: + - mkdir -p generated + - printf 'a\n' > generated/a.dep + - printf 'b\n' > generated/b.dep + - printf 'c\n' > generated/c.dep + + use-deps: + deps: + - create-deps + sources: + - generated/*.dep + cmds: + - for: sources + cmd: cat "{{.ITEM}}" +`), 0o644)) + + var buffer SyncBuffer + e := task.NewExecutor( + task.WithDir(dir), + task.WithSilent(true), + task.WithStdout(&buffer), + task.WithStderr(&buffer), + ) + require.NoError(t, e.Setup()) + require.NoError(t, e.Run(t.Context(), &task.Call{ + Task: "use-deps", + Vars: ast.NewVars(), + })) + + require.Equal(t, "a\nb\nc\n", string(PPSortedLines(t, buffer.buf.Bytes()))) +} + func TestForDeps(t *testing.T) { t.Parallel() diff --git a/task.go b/task.go index 54cda92762..ef8240d1ce 100644 --- a/task.go +++ b/task.go @@ -209,6 +209,18 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { if err := e.runDeps(ctx, t); err != nil { return err } + if len(t.Deps) > 0 { + // Dependencies can materialize files that `for: sources` / `for: generates` + // loops expand over, so refresh the compiled command list after deps run. + origTask, err := e.GetTask(call) + if err != nil { + return err + } + cache := &templater.Cache{Vars: t.Vars} + if err := compileCmds(origTask, t, t.Vars, cache); err != nil { + return err + } + } skipFingerprinting := e.ForceAll || (!call.Indirect && e.Force) if !skipFingerprinting { diff --git a/variables.go b/variables.go index c7c6cc8493..b28d257272 100644 --- a/variables.go +++ b/variables.go @@ -212,54 +212,8 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err cache.ResetCache() } - if len(origTask.Cmds) > 0 { - new.Cmds = make([]*ast.Cmd, 0, len(origTask.Cmds)) - for _, cmd := range origTask.Cmds { - if cmd == nil { - continue - } - if cmd.For != nil { - list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache) - if err != nil { - return nil, err - } - // Name the iterator variable - var as string - if cmd.For.As != "" { - as = cmd.For.As - } else { - as = "ITEM" - } - // Create a new command for each item in the list - for i, loopValue := range list { - extra := map[string]any{ - as: loopValue, - } - if len(keys) > 0 { - extra["KEY"] = keys[i] - } - newCmd := cmd.DeepCopy() - newCmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra) - newCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra) - newCmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra) - newCmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra) - new.Cmds = append(new.Cmds, newCmd) - } - continue - } - // Defer commands are replaced in a lazy manner because - // we need to include EXIT_CODE. - if cmd.Defer { - new.Cmds = append(new.Cmds, cmd.DeepCopy()) - continue - } - newCmd := cmd.DeepCopy() - newCmd.Cmd = templater.Replace(cmd.Cmd, cache) - newCmd.Task = templater.Replace(cmd.Task, cache) - newCmd.If = templater.Replace(cmd.If, cache) - newCmd.Vars = templater.ReplaceVars(cmd.Vars, cache) - new.Cmds = append(new.Cmds, newCmd) - } + if err := compileCmds(origTask, &new, vars, cache); err != nil { + return nil, err } if len(origTask.Deps) > 0 { new.Deps = make([]*ast.Dep, 0, len(origTask.Deps)) @@ -334,6 +288,63 @@ func asAnySlice[T any](slice []T) []any { return ret } +func compileCmds(origTask *ast.Task, new *ast.Task, vars *ast.Vars, cache *templater.Cache) error { + if len(origTask.Cmds) == 0 { + new.Cmds = nil + return nil + } + + new.Cmds = make([]*ast.Cmd, 0, len(origTask.Cmds)) + for _, cmd := range origTask.Cmds { + if cmd == nil { + continue + } + if cmd.For != nil { + list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache) + if err != nil { + return err + } + // Name the iterator variable + var as string + if cmd.For.As != "" { + as = cmd.For.As + } else { + as = "ITEM" + } + // Create a new command for each item in the list + for i, loopValue := range list { + extra := map[string]any{ + as: loopValue, + } + if len(keys) > 0 { + extra["KEY"] = keys[i] + } + newCmd := cmd.DeepCopy() + newCmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra) + newCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra) + newCmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra) + newCmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra) + new.Cmds = append(new.Cmds, newCmd) + } + continue + } + // Defer commands are replaced in a lazy manner because + // we need to include EXIT_CODE. + if cmd.Defer { + new.Cmds = append(new.Cmds, cmd.DeepCopy()) + continue + } + newCmd := cmd.DeepCopy() + newCmd.Cmd = templater.Replace(cmd.Cmd, cache) + newCmd.Task = templater.Replace(cmd.Task, cache) + newCmd.If = templater.Replace(cmd.If, cache) + newCmd.Vars = templater.ReplaceVars(cmd.Vars, cache) + new.Cmds = append(new.Cmds, newCmd) + } + + return nil +} + func itemsFromFor( f *ast.For, dir string,