这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
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
2 changes: 1 addition & 1 deletion compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (c *Compiler) FastGetVariables(t *ast.Task, call *Call) (*ast.Vars, error)

func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
result := env.GetEnviron()
evaluator := hclext.NewHCLEvaluator(result)
evaluator := hclext.NewHCLEvaluator(result, result, nil)
specialVars, err := c.getSpecialVars(t, call)
if err != nil {
return nil, err
Expand Down
21 changes: 21 additions & 0 deletions executor_call_task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package task

import (
"bytes"
"context"
"io"
"strings"

"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/taskfile/ast"
)

func (e *Executor) callTask(name string, vars *ast.Vars) (string, error) {
buf := &bytes.Buffer{}
origStdout, origStderr, origLogger := e.Stdout, e.Stderr, e.Logger
e.Stdout, e.Stderr = buf, buf
e.Logger = &logger.Logger{Stdout: io.Discard, Stderr: io.Discard}
err := e.RunTask(context.Background(), &Call{Task: name, Vars: vars, Silent: true, Indirect: true})
e.Stdout, e.Stderr, e.Logger = origStdout, origStderr, origLogger
return strings.TrimSpace(buf.String()), err
}
35 changes: 35 additions & 0 deletions hcl_e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package task

import (
"os/exec"
"path/filepath"
"strings"
"testing"
)

func TestHCLE2E(t *testing.T) {
cmd := exec.Command("go", "run", "./cmd/task", "-t", filepath.Join("testdata", "HCLE2ETest", "Taskfile.hcl"), "all")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("task run failed: %v\n%s", err, out)
}
output := string(out)
if !strings.Contains(output, "BUILD:1.2.3") {
t.Fatalf("missing build output: %s", output)
}
one := strings.Index(output, "ONE-DONE")
two := strings.Index(output, "TWO-DONE")
if one == -1 || two == -1 || one > two {
t.Fatalf("dependency order wrong: %s", output)
}
if !strings.Contains(output, "LINT MODE fast") {
t.Fatalf("missing lint output: %s", output)
}
if !strings.Contains(output, "FINAL foo") {
t.Fatalf("missing final output: %s", output)
}
idx := strings.Index(output, "PATH=")
if idx == -1 || idx+5 >= len(output) || output[idx+5] == '\n' {
t.Fatalf("missing path output: %s", output)
}
}
104 changes: 92 additions & 12 deletions internal/hclext/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package hclext
import (
"fmt"
"os"
"os/exec"
"strings"

"github.com/hashicorp/hcl/v2"
Expand All @@ -12,33 +13,56 @@ import (
"github.com/go-task/task/v3/taskfile/ast"
)

type TaskRunner func(name string, vars *ast.Vars) (string, error)

type HCLEvaluator struct {
EvalCtx *hcl.EvalContext
vars map[string]cty.Value
env map[string]cty.Value
}

func NewHCLEvaluator(vars *ast.Vars) *HCLEvaluator {
ctx := &hcl.EvalContext{
Variables: map[string]cty.Value{},
Functions: builtinFunctions(),
}
func NewHCLEvaluator(vars, env *ast.Vars, runner TaskRunner) *HCLEvaluator {
varVals := map[string]cty.Value{}
if vars != nil {
for k, v := range vars.All() {
if v.Value != nil {
ctx.Variables[k] = cty.StringVal(fmt.Sprint(v.Value))
varVals[k] = cty.StringVal(fmt.Sprint(v.Value))
}
}
}
envVals := map[string]cty.Value{}
if env != nil {
for k, v := range env.All() {
if v.Value != nil {
envVals[k] = cty.StringVal(fmt.Sprint(v.Value))
}
}
}
return &HCLEvaluator{EvalCtx: ctx}
ctx := &hcl.EvalContext{
Variables: map[string]cty.Value{
"vars": cty.ObjectVal(varVals),
"env": cty.ObjectVal(envVals),
},
Functions: builtinFunctions(runner),
}
return &HCLEvaluator{EvalCtx: ctx, vars: varVals, env: envVals}
}

func builtinFunctions() map[string]function.Function {
return map[string]function.Function{
func builtinFunctions(runner TaskRunner) map[string]function.Function {
funcs := map[string]function.Function{
"upper": stringFunc(strings.ToUpper),
"lower": stringFunc(strings.ToLower),
"join": joinFunc(),
"split": splitFunc(),
"env": envFunc(),
"sh": shellFunc("/bin/sh"),
"bash": shellFunc("/bin/bash"),
"zsh": shellFunc("/bin/zsh"),
}
if runner != nil {
funcs["task"] = taskFunc(runner)
}
return funcs
}

func stringFunc(fn func(string) string) function.Function {
Expand Down Expand Up @@ -99,11 +123,67 @@ func envFunc() function.Function {
})
}

func shellFunc(shell string) function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{{Name: "cmd", Type: cty.String}},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
cmd := exec.Command(shell, "-c", args[0].AsString())
out, err := cmd.CombinedOutput()
if err != nil {
return cty.NilVal, fmt.Errorf("%s: %w", shell, err)
}
return cty.StringVal(strings.TrimSpace(string(out))), nil
},
})
}

func taskFunc(runner TaskRunner) function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{{Name: "name", Type: cty.String}},
VarParam: &function.Parameter{Name: "vars", Type: cty.DynamicPseudoType},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
name := args[0].AsString()
var depVars *ast.Vars
if len(args) > 1 && args[1].Type().IsObjectType() {
depVars = ast.NewVars()
for k := range args[1].Type().AttributeTypes() {
val := args[1].GetAttr(k)
var depVal string
switch {
case val.Type() == cty.String:
depVal = val.AsString()
case val.Type() == cty.Number:
bf := val.AsBigFloat()
depVal = bf.Text('f', -1)
case val.Type() == cty.Bool:
if val.True() {
depVal = "true"
} else {
depVal = "false"
}
default:
depVal = val.GoString()
}
depVars.Set(k, ast.Var{Value: depVal})
}
}
out, err := runner(name, depVars)
if err != nil {
return cty.NilVal, err
}
return cty.StringVal(out), nil
},
})
}

func (e *HCLEvaluator) SetVar(name, value string) {
if e.EvalCtx.Variables == nil {
e.EvalCtx.Variables = map[string]cty.Value{}
if e.vars == nil {
e.vars = map[string]cty.Value{}
}
e.EvalCtx.Variables[name] = cty.StringVal(value)
e.vars[name] = cty.StringVal(value)
e.EvalCtx.Variables["vars"] = cty.ObjectVal(e.vars)
}

func (e *HCLEvaluator) EvalString(expr hcl.Expression) (string, error) {
Expand Down
19 changes: 6 additions & 13 deletions task.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,23 +260,15 @@ func (e *Executor) mkdir(t *ast.Task) error {
}

func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
g, ctx := errgroup.WithContext(ctx)

reacquire := e.releaseConcurrencyLimit()
defer reacquire()

for _, d := range t.Deps {
d := d
g.Go(func() error {
err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
if err != nil {
return err
}
return nil
})
if err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true}); err != nil {
return err
}
}

return g.Wait()
return nil
}

func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode *uint8) {
Expand All @@ -291,7 +283,8 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode
cmd := t.Cmds[i]
vars, _ := e.Compiler.GetVariables(origTask, call)
cache := &templater.Cache{Vars: vars}
hclEval := hclext.NewHCLEvaluator(vars)
runtimeEnv := env.GetEnviron()
hclEval := hclext.NewHCLEvaluator(vars, runtimeEnv, e.callTask)
extra := map[string]any{}

if deferredExitCode != nil && *deferredExitCode > 0 {
Expand Down
7 changes: 4 additions & 3 deletions taskfile/hcl_evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/stretchr/testify/require"

"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/hclext"
"github.com/go-task/task/v3/taskfile/ast"
)
Expand All @@ -15,15 +16,15 @@ func TestHCLEvaluatorExpressions(t *testing.T) {
t.Setenv("HOME", "/home/test")
vars := ast.NewVars()
vars.Set("FOO", ast.Var{Value: "bar"})
eval := hclext.NewHCLEvaluator(vars)
eval := hclext.NewHCLEvaluator(vars, env.GetEnviron(), nil)

expr1, diags := hclsyntax.ParseTemplate([]byte("${FOO}"), "test.hcl", hcl.InitialPos)
expr1, diags := hclsyntax.ParseTemplate([]byte("${vars.FOO}"), "test.hcl", hcl.InitialPos)
require.False(t, diags.HasErrors())
v, err := eval.EvalString(expr1)
require.NoError(t, err)
require.Equal(t, "bar", v)

expr2, diags := hclsyntax.ParseTemplate([]byte("${upper(FOO)}"), "test.hcl", hcl.InitialPos)
expr2, diags := hclsyntax.ParseTemplate([]byte("${upper(vars.FOO)}"), "test.hcl", hcl.InitialPos)
require.False(t, diags.HasErrors())
v, err = eval.EvalString(expr2)
require.NoError(t, err)
Expand Down
6 changes: 3 additions & 3 deletions taskfile/hcl_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ func TestHCLLoader(t *testing.T) {
data := []byte(`version = "3"
task "build" {
desc = "Build the project"
cmds = ["echo hello ${USER}"]
vars = { USER = "world" }
env = { GREETING = "hi" }
vars { USER = "world" }
env { GREETING = "hi" }
cmds = ["echo hello ${vars.USER}"]
}
`)

Expand Down
59 changes: 59 additions & 0 deletions taskfile/hcl_runtime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package taskfile

import (
"testing"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/stretchr/testify/require"

"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/hclext"
"github.com/go-task/task/v3/taskfile/ast"
)

func TestBlockSyntaxEnforced(t *testing.T) {
data := []byte(`version = "3"
vars = { FOO = "bar" }
`)
loader := HCLLoader{}
_, err := loader.Load(data, "Taskfile.hcl")
require.Error(t, err)
}

func TestShFunctionSuccess(t *testing.T) {
eval := hclext.NewHCLEvaluator(nil, env.GetEnviron(), nil)
expr, diags := hclsyntax.ParseExpression([]byte(`sh("echo hi")`), "test.hcl", hcl.InitialPos)
require.False(t, diags.HasErrors())
v, err := eval.EvalString(expr)
require.NoError(t, err)
require.Equal(t, "hi", v)
}

func TestShFunctionFail(t *testing.T) {
eval := hclext.NewHCLEvaluator(nil, env.GetEnviron(), nil)
expr, diags := hclsyntax.ParseExpression([]byte(`sh("exit 1")`), "test.hcl", hcl.InitialPos)
require.False(t, diags.HasErrors())
_, err := eval.EvalString(expr)
require.Error(t, err)
}

func TestTaskFunctionStdoutCapture(t *testing.T) {
runner := func(name string, vars *ast.Vars) (string, error) {
return "output", nil
}
eval := hclext.NewHCLEvaluator(nil, env.GetEnviron(), runner)
expr, diags := hclsyntax.ParseExpression([]byte(`task("build")`), "test.hcl", hcl.InitialPos)
require.False(t, diags.HasErrors())
v, err := eval.EvalString(expr)
require.NoError(t, err)
require.Equal(t, "output", v)
}

func TestInvalidReference(t *testing.T) {
eval := hclext.NewHCLEvaluator(ast.NewVars(), env.GetEnviron(), nil)
expr, diags := hclsyntax.ParseExpression([]byte(`FOO`), "test.hcl", hcl.InitialPos)
require.False(t, diags.HasErrors())
_, err := eval.EvalString(expr)
require.Error(t, err)
}
Loading