diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index 3213e7a5151b..082c6c1cb3e4 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -2603,8 +2603,20 @@ func buildMetaArgs(args *llb.EnvList, shlex *shell.Lex, argCommands []instructio if err != nil { return nil, nil, parser.WithLocation(err, cmd.Location()) } + kp.Value = &result.Result info.deps = result.Matched + if _, ok := result.Matched[kp.Key]; ok { + delete(info.deps, kp.Key) + if old, ok := allArgs[kp.Key]; ok { + for k := range old.deps { + if info.deps == nil { + info.deps = make(map[string]struct{}) + } + info.deps[k] = struct{}{} + } + } + } } } else { kp.Value = &v diff --git a/frontend/dockerfile/dockerfile2llb/outline.go b/frontend/dockerfile/dockerfile2llb/outline.go index b3d80999c413..72593af289a1 100644 --- a/frontend/dockerfile/dockerfile2llb/outline.go +++ b/frontend/dockerfile/dockerfile2llb/outline.go @@ -52,17 +52,21 @@ func (o outlineCapture) clone() outlineCapture { } } -func (o outlineCapture) markAllUsed(in map[string]struct{}) { +func (o outlineCapture) markAllUsed(in map[string]struct{}, visited map[string]struct{}) { for k := range in { + if _, ok := visited[k]; ok { + continue + } + visited[k] = struct{}{} if a, ok := o.allArgs[k]; ok { - o.markAllUsed(a.deps) + o.markAllUsed(a.deps, visited) } o.usedArgs[k] = struct{}{} } } func (ds *dispatchState) args(visited map[string]struct{}) []outline.Arg { - ds.outline.markAllUsed(ds.outline.usedArgs) + ds.outline.markAllUsed(ds.outline.usedArgs, map[string]struct{}{}) args := make([]outline.Arg, 0, len(ds.outline.usedArgs)) for k := range ds.outline.usedArgs { diff --git a/frontend/dockerfile/dockerfile_outline_test.go b/frontend/dockerfile/dockerfile_outline_test.go index 79afed412b44..985ad66ac6b4 100644 --- a/frontend/dockerfile/dockerfile_outline_test.go +++ b/frontend/dockerfile/dockerfile_outline_test.go @@ -23,6 +23,7 @@ var outlineTests = integration.TestFuncs( testOutlineArgs, testOutlineSecrets, testOutlineDescribeDefinition, + testOutlineRecursiveArgs, ) func testOutlineArgs(t *testing.T, sb integration.Sandbox) { @@ -268,6 +269,86 @@ FROM second require.True(t, called) } +func testOutlineRecursiveArgs(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + workers.CheckFeatureCompat(t, sb, workers.FeatureFrontendOutline) + f := getFrontend(t, sb) + if _, ok := f.(*clientFrontend); !ok { + t.Skip("only test with client frontend") + } + + dockerfile := []byte(` +ARG FOO=123 +ARG ABC=abc +ARG DEF=def +ARG FOO=${FOO}${ABC} +ARG BAR=${FOO}456 +ARG FOO=${FOO}456${BAR} +FROM scratch +ARG FOO +ARG INFOO=123 +ARG INBAR=${INFOO}456 +ARG INFOO=${INFOO}456${INBAR} +`) + + dir := integration.Tmpdir( + t, + fstest.CreateFile("Dockerfile", []byte(dockerfile), 0600), + ) + + c, err := client.New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + destDir, err := os.MkdirTemp("", "buildkit") + require.NoError(t, err) + defer os.RemoveAll(destDir) + + called := false + frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res, err := c.Solve(ctx, gateway.SolveRequest{ + FrontendOpt: map[string]string{ + "frontend.caps": "moby.buildkit.frontend.subrequests", + "requestid": "frontend.outline", + }, + Frontend: "dockerfile.v0", + }) + require.NoError(t, err) + + outline, err := unmarshalOutline(res) + require.NoError(t, err) + + require.Len(t, outline.Args, 5) + + require.Equal(t, "ABC", outline.Args[0].Name) + require.Equal(t, "abc", outline.Args[0].Value) + + require.Equal(t, "BAR", outline.Args[1].Name) + require.Equal(t, "123abc456", outline.Args[1].Value) + + require.Equal(t, "FOO", outline.Args[2].Name) + require.Equal(t, "123abc456123abc456", outline.Args[2].Value) + + require.Equal(t, "INBAR", outline.Args[3].Name) + require.Equal(t, "123456", outline.Args[3].Value) + + require.Equal(t, "INFOO", outline.Args[4].Name) + require.Equal(t, "123456123456", outline.Args[4].Value) + + called = true + return nil, nil + } + + _, err = c.Build(sb.Context(), client.SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir, + }, + }, "", frontend, nil) + require.NoError(t, err) + + require.True(t, called) +} + func testOutlineDescribeDefinition(t *testing.T, sb integration.Sandbox) { workers.CheckFeatureCompat(t, sb, workers.FeatureFrontendOutline) f := getFrontend(t, sb)