From 8f0d3cef548b584d8b24edab8edea6c4c9f534b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?okhowang=28=E7=8E=8B=E6=B2=9B=E6=96=87=29?= Date: Tue, 4 Aug 2026 16:55:34 +0800 Subject: [PATCH] build: resolve target links for call methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Targets referencing each other through a "target:" named context are resolved on the client: the linked target is solved first and its result is then rewritten into an "input:" context by waitContextDeps. A call method such as check or outline replies with the metadata of the subrequest and does not return a build result, so nothing can be linked into the targets depending on it. The "target:" context is then left untouched and reaches the frontend, which rejects it: $ docker buildx bake -f providers.hcl -f build.hcl app --check app error: unsupported context source target for base base error: unsupported context source target for builder Solve the build request separately when a target using a call method is linked to by another one, and register that result for them. It is never evaluated, so the definition is only resolved and nothing is built. Checks are skipped for that request as they are already reported by the call method, otherwise a Dockerfile asking for check violations to be errors would fail it before the report is produced. Fixes #3343 Signed-off-by: okhowang(王沛文) --- build/build.go | 46 ++++++++++++++++++++++++++++++++------- tests/bake.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/build/build.go b/build/build.go index 7e354ec8a316..daf690c04b37 100644 --- a/build/build.go +++ b/build/build.go @@ -607,16 +607,45 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[ // Capture the error from this build function. defer catchFrontendError(&retErr, &frontendErr) + rKey := resultKey(dp, k) + children := childTargets[rKey] + + // A call method replies with the metadata of the subrequest + // instead of a build result, so there would be nothing to link + // into the targets using this one as a named context. Solve the + // build request on its own in that case and register its result + // for them. It is never evaluated, so this only resolves the + // definition and does not build anything. + linked := false + if opt.CallFunc != nil && len(children) > 0 { + linkReq := req + linkReq.FrontendOpt = maps.Clone(req.FrontendOpt) + // checks are reported by the call method below, skip them + // here so that this request is not rejected by a Dockerfile + // that asks for check violations to be errors + linkReq.FrontendOpt["build-arg:BUILDKIT_DOCKERFILE_CHECK"] = "skip=all;error=false" + res, err := solve(ctx, c, linkReq) + if err != nil { + return nil, err + } + results.Set(rKey, res) + linked = true + } + + solveReq := req if opt.CallFunc != nil { - if _, ok := req.FrontendOpt["frontend.caps"]; !ok { - req.FrontendOpt["frontend.caps"] = "moby.buildkit.frontend.subrequests+forward" + // keep the initial request untouched, it may have already + // been sent for the linked result above + solveReq.FrontendOpt = maps.Clone(req.FrontendOpt) + if _, ok := solveReq.FrontendOpt["frontend.caps"]; !ok { + solveReq.FrontendOpt["frontend.caps"] = "moby.buildkit.frontend.subrequests+forward" } else { - req.FrontendOpt["frontend.caps"] += ",moby.buildkit.frontend.subrequests+forward" + solveReq.FrontendOpt["frontend.caps"] += ",moby.buildkit.frontend.subrequests+forward" } - req.FrontendOpt["requestid"] = "frontend." + opt.CallFunc.Name + solveReq.FrontendOpt["requestid"] = "frontend." + opt.CallFunc.Name } - res, err := solve(ctx, c, req) + res, err := solve(ctx, c, solveReq) if err != nil { return nil, err } @@ -625,11 +654,12 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[ callRes = res.Metadata } - rKey := resultKey(dp, k) - results.Set(rKey, res) + if !linked { + results.Set(rKey, res) + } forceEval := false - if children := childTargets[rKey]; len(children) > 0 { + if len(children) > 0 { // wait for the child targets to register their LLB before evaluating _, err := results.Get(ctx, children...) if err != nil { diff --git a/tests/bake.go b/tests/bake.go index ae26e64bf4b2..b850b0fca2c3 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -83,6 +83,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakeListVariables, testBakeListTypedVariables, testBakeCallCheck, + testBakeCallCheckLinkedTargets, testBakeCallCheckFlag, testBakeCallMetadata, testBakeMultiPlatform, @@ -2450,6 +2451,63 @@ target "validate" { require.Contains(t, out, "ConsistentInstructionCasing") } +func testBakeCallCheckLinkedTargets(t *testing.T, sb integration.Sandbox) { + dockerfileBuilder := []byte(` +FROM scratch +COPY foo /foo + `) + dockerfileBase := []byte(` +FROM builder +COPY foo /bar + `) + dockerfileApp := []byte(` +FROM base +COPy foo /baz + `) + bakefile := []byte(` +target "builder" { + dockerfile = "builder.Dockerfile" +} + +target "base" { + dockerfile = "base.Dockerfile" + contexts = { + builder = "target:builder" + } +} + +target "app" { + dockerfile = "app.Dockerfile" + contexts = { + base = "target:base" + } +} +`) + dir := tmpdir( + t, + fstest.CreateFile("docker-bake.hcl", bakefile, 0600), + fstest.CreateFile("builder.Dockerfile", dockerfileBuilder, 0600), + fstest.CreateFile("base.Dockerfile", dockerfileBase, 0600), + fstest.CreateFile("app.Dockerfile", dockerfileApp, 0600), + fstest.CreateFile("foo", []byte("foo"), 0600), + ) + + out, err := bakeCmd( + sb, + withDir(dir), + withArgs("app", "--check"), + ) + require.Error(t, err, out) + + // the "target:" contexts must be resolved for the call method instead of + // being forwarded to the frontend + require.NotContains(t, out, "unsupported context source target") + + // the requested target and the ones it links to are all checked + require.Equal(t, 3, strings.Count(out, "Check complete"), out) + require.Contains(t, out, "ConsistentInstructionCasing") +} + func testBakeCallCheckFlag(t *testing.T, sb integration.Sandbox) { dockerfile := []byte(` FROM scratch