From f7885caedf72a4011001b59619076d0b39c96dfa Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Fri, 10 Jul 2026 16:21:50 +0200 Subject: [PATCH 1/4] bake: add file-relative path opt-in Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- bake/bake.go | 56 +++++++++++++++++++-- bake/bake_test.go | 93 +++++++++++++++++++++++++++++++++++ commands/bake.go | 25 +++++++++- docs/bake-reference.md | 4 ++ docs/reference/buildx_bake.md | 6 +++ tests/bake.go | 71 ++++++++++++++++++++++++++ 6 files changed, 250 insertions(+), 5 deletions(-) diff --git a/bake/bake.go b/bake/bake.go index 66ef0a1d55b4..3a9e63abdf20 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -23,6 +23,7 @@ import ( "github.com/docker/buildx/bake/hclparser" "github.com/docker/buildx/build" "github.com/docker/buildx/util/buildflags" + "github.com/docker/buildx/util/osutil" "github.com/docker/buildx/util/platformutil" "github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/urlutil" @@ -46,6 +47,10 @@ type File struct { Data []byte } +type ParseOpt struct { + FileRelativePaths bool +} + type Override struct { Value string ArrValue []string @@ -197,8 +202,8 @@ func ListTargets(files []File) ([]string, error) { return dedupSlice(targets), nil } -func ReadTargets(ctx context.Context, files []File, targets, overrides []string, defaults, vars map[string]string, ent *EntitlementConf) (map[string]*Target, map[string]*Group, error) { - c, _, err := ParseFiles(files, defaults, vars) +func ReadTargets(ctx context.Context, files []File, targets, overrides []string, defaults, vars map[string]string, ent *EntitlementConf, opts ...ParseOpt) (map[string]*Target, map[string]*Group, error) { + c, _, err := ParseFiles(files, defaults, vars, opts...) if err != nil { return nil, nil, err } @@ -337,7 +342,7 @@ func (c Config) matchNames(pattern string) ([]string, error) { return names, nil } -func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *hclparser.ParseMeta, err error) { +func ParseFiles(files []File, defaults, vars map[string]string, opts ...ParseOpt) (_ *Config, _ *hclparser.ParseMeta, err error) { defer func() { err = formatHCLError(err, files) }() @@ -413,9 +418,54 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h pm = *res } + for _, opt := range opts { + if opt.FileRelativePaths { + rebaseContextPaths(&c, files) + break + } + } + return &c, &pm, nil } +func rebaseContextPaths(c *Config, files []File) { + base, ok := firstLocalFileDir(files) + if !ok { + return + } + for _, t := range c.Targets { + if t.Context != nil { + contextPath := rebaseContextPath(base, *t.Context) + t.Context = &contextPath + } + for k, v := range t.Contexts { + t.Contexts[k] = rebaseContextPath(base, v) + } + } +} + +func firstLocalFileDir(files []File) (string, bool) { + if len(files) == 0 || files[0].Name == "-" || urlutil.IsRemoteURL(files[0].Name) { + return "", false + } + return filepath.Dir(files[0].Name), true +} + +func rebaseContextPath(base, p string) string { + if p == "" || isSpecialContextPath(p) || filepath.IsAbs(p) { + return p + } + return osutil.SanitizePath(filepath.Join(base, filepath.FromSlash(p))) +} + +func isSpecialContextPath(p string) bool { + return strings.HasPrefix(p, "cwd://") || + strings.HasPrefix(p, "target:") || + strings.HasPrefix(p, "docker-image:") || + strings.HasPrefix(p, "oci-layout://") || + urlutil.IsRemoteURL(p) +} + func dedupeConfig(c Config) Config { c2 := c c2.Groups = make([]*Group, 0, len(c2.Groups)) diff --git a/bake/bake_test.go b/bake/bake_test.go index de3fb9636e06..379830c6f579 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -763,6 +763,99 @@ func TestHCLDockerfileCwdPrefix(t *testing.T) { assert.Equal(t, ".", bo["app"].Inputs.ContextPath) } +func TestContextPathRebase(t *testing.T) { + fp := File{ + Name: filepath.Join("subdir", "docker-bake.hcl"), + Data: []byte(` +target "base" { + context = "base" +} + +target "app" { + context = "." + dockerfile = "Dockerfile.app" + contexts = { + shared = "../shared" + cwd = "cwd://local" + linked = "target:base" + image = "docker-image://alpine:latest" + layout = "oci-layout://layout" + } +}`), + } + + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{ + FileRelativePaths: true, + }) + require.NoError(t, err) + + require.Equal(t, filepath.ToSlash(filepath.Clean("subdir")), *m["app"].Context) + require.Equal(t, "Dockerfile.app", *m["app"].Dockerfile) + require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), m["app"].Contexts["shared"]) + require.Equal(t, "cwd://local", m["app"].Contexts["cwd"]) + require.Equal(t, "target:base", m["app"].Contexts["linked"]) + require.Equal(t, "docker-image://alpine:latest", m["app"].Contexts["image"]) + require.Equal(t, "oci-layout://layout", m["app"].Contexts["layout"]) + require.Equal(t, filepath.ToSlash(filepath.Clean("subdir/base")), *m["base"].Context) + + bo, err := TargetsToBuildOpt(m, &Input{}) + require.NoError(t, err) + require.Equal(t, filepath.ToSlash(filepath.Clean("subdir")), bo["app"].Inputs.ContextPath) + require.Equal(t, filepath.Join("subdir", "Dockerfile.app"), bo["app"].Inputs.DockerfilePath) + require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), bo["app"].Inputs.NamedContexts["shared"].Path) +} + +func TestOverridesNotRebased(t *testing.T) { + fp := File{ + Name: filepath.Join("subdir", "docker-bake.hcl"), + Data: []byte(` +target "app" { + context = "." + contexts = { + shared = "../shared" + } +}`), + } + + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{ + "app.context=override", + "app.contexts.shared=override-shared", + }, nil, nil, &EntitlementConf{}, ParseOpt{ + FileRelativePaths: true, + }) + require.NoError(t, err) + + require.Equal(t, "override", *m["app"].Context) + require.Equal(t, "override-shared", m["app"].Contexts["shared"]) +} + +func TestComposePathRebase(t *testing.T) { + fp := File{ + Name: filepath.Join("tests", "docker-compose.yml"), + Data: []byte(` +services: + debian: + build: + context: ./dockerfiles/debian + additional_contexts: + shared: ../shared +`), + } + + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"debian"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{ + FileRelativePaths: true, + }) + require.NoError(t, err) + + require.Equal(t, filepath.ToSlash(filepath.Clean("tests/dockerfiles/debian")), *m["debian"].Context) + require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), m["debian"].Contexts["shared"]) + + bo, err := TargetsToBuildOpt(m, &Input{}) + require.NoError(t, err) + require.Equal(t, filepath.ToSlash(filepath.Clean("tests/dockerfiles/debian")), bo["debian"].Inputs.ContextPath) + require.Equal(t, filepath.Join("tests", "dockerfiles", "debian", "Dockerfile"), bo["debian"].Inputs.DockerfilePath) +} + func TestOverrideMerge(t *testing.T) { fp := File{ Name: "docker-bake.hcl", diff --git a/commands/bake.go b/commands/bake.go index 76a077525593..6fcde330d1fd 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -12,6 +12,7 @@ import ( "os" "slices" "sort" + "strconv" "strings" "sync" "text/tabwriter" @@ -48,6 +49,7 @@ import ( const ( bakeEnvFileSeparator = "BUILDX_BAKE_PATH_SEPARATOR" bakeEnvFilePath = "BUILDX_BAKE_FILE" + bakeEnvFileRelative = "BUILDX_BAKE_FILE_RELATIVE_PATHS" ) type bakeOptions struct { @@ -225,9 +227,16 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if err != nil { return err } + fileRelativePaths, err := bakeFileRelativePaths() + if err != nil { + return err + } + parseOpt := bake.ParseOpt{ + FileRelativePaths: fileRelativePaths, + } if in.list != "" { - cfg, pm, err := bake.ParseFiles(files, defaults, vars) + cfg, pm, err := bake.ParseFiles(files, defaults, vars, parseOpt) if err != nil { return err } @@ -246,7 +255,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba } } - tgts, grps, err := bake.ReadTargets(ctx, files, targets, overrides, defaults, vars, &ent) + tgts, grps, err := bake.ReadTargets(ctx, files, targets, overrides, defaults, vars, &ent, parseOpt) if err != nil { return err } @@ -674,6 +683,18 @@ func bakeArgs(args []string) (url, cmdContext string, targets []string) { return url, cmdContext, targets } +func bakeFileRelativePaths() (bool, error) { + v := strings.TrimSpace(os.Getenv(bakeEnvFileRelative)) + if v == "" { + return false, nil + } + enabled, err := strconv.ParseBool(v) + if err != nil { + return false, errors.Wrapf(err, "failed to parse %s value %q", bakeEnvFileRelative, v) + } + return enabled, nil +} + func readBakeFiles(ctx context.Context, nodes []builder.Node, url string, names []string, stdin io.Reader, pw progress.Writer, filesFromEnv bool) (files []bake.File, inp *bake.Input, err error) { var lnames []string // local var rnames []string // remote diff --git a/docs/bake-reference.md b/docs/bake-reference.md index e473b2d1ba30..832d28f312d7 100644 --- a/docs/bake-reference.md +++ b/docs/bake-reference.md @@ -416,6 +416,10 @@ target "app" { ``` This resolves to the current working directory (`"."`) by default. +Set `BUILDX_BAKE_FILE_RELATIVE_PATHS=1` to resolve local directory paths in +`target.context` and `target.contexts` relative to the directory of the first +Bake file. Use `cwd://` for paths that should remain relative to the current +working directory when this opt-in is enabled. ```console $ docker buildx bake --print -f - <<< 'target "default" {}' diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 8c8633e322e8..30102a41a830 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -153,6 +153,12 @@ This is mutually exclusive with `-f` / `--file`; if both are specified, the envi Multiple definitions can be specified by separating them with the system's path separator (typically `;` on Windows and `:` elsewhere), but can be changed with `BUILDX_BAKE_PATH_SEPARATOR`. +By default, local directory build contexts in Bake files are resolved from the +current working directory. To opt in to resolving local directory build contexts +from the directory of the first Bake file, set +`BUILDX_BAKE_FILE_RELATIVE_PATHS=1`. Use the `cwd://` prefix for paths that +should remain relative to the current working directory. + You can pass the names of the targets to build, to build only specific target(s). The following example builds the `db` and `webapp-release` targets that are defined in the `docker-bake.dev.hcl` file: diff --git a/tests/bake.go b/tests/bake.go index c8a1d1ee4dd4..17ae3810c507 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -45,6 +45,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakePrintRemoteContextSubdir, testBakeLocal, testBakeLocalMulti, + testBakeFileRelativePaths, testBakeLocalExportDeleteMode, testBakeRemote, testBakeRemoteAuth, @@ -674,6 +675,76 @@ services: require.FileExists(t, filepath.Join(dirDest2, "foo")) } +func testBakeFileRelativePaths(t *testing.T, sb integration.Sandbox) { + t.Run("compose context", func(t *testing.T) { + dockerfile := []byte(` +FROM scratch +COPY marker /marker +COPY --from=shared shared-marker /shared-marker +`) + composefile := []byte(` +services: + debian: + build: + context: ./dockerfiles/debian + additional_contexts: + shared: ../shared +`) + + dir := tmpdir( + t, + fstest.CreateDir("tests", 0700), + fstest.CreateDir("tests/dockerfiles", 0700), + fstest.CreateDir("tests/dockerfiles/debian", 0700), + fstest.CreateDir("shared", 0700), + fstest.CreateFile("tests/docker-compose.yml", composefile, 0600), + fstest.CreateFile("tests/dockerfiles/debian/Dockerfile", dockerfile, 0600), + fstest.CreateFile("tests/dockerfiles/debian/marker", []byte("marker"), 0600), + fstest.CreateFile("shared/shared-marker", []byte("shared"), 0600), + ) + dirDest := t.TempDir() + + out, err := bakeCmd( + sb, + withDir(dir), + withArgs("--file", "tests/docker-compose.yml", "--set", "*.output=type=local,dest="+dirDest), + withEnv("BUILDX_BAKE_FILE_RELATIVE_PATHS=1"), + ) + require.NoError(t, err, out) + require.FileExists(t, filepath.Join(dirDest, "marker")) + require.FileExists(t, filepath.Join(dirDest, "shared-marker")) + }) + + t.Run("cwd prefix", func(t *testing.T) { + bakefile := []byte(` +target "default" { + context = "cwd://." + dockerfile-inline = < Date: Tue, 7 Jul 2026 09:01:09 +0200 Subject: [PATCH 2/4] bake: make file-relative paths definition-scoped Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- bake/bake.go | 104 ++++++++++++++++++++++++++++------ bake/bake_test.go | 72 ++++++++++++++++++++++- bake/compose.go | 46 ++++++++++++++- bake/hclparser/hclparser.go | 8 +++ docs/bake-reference.md | 6 +- docs/reference/buildx_bake.md | 2 +- tests/bake.go | 28 +++++++++ 7 files changed, 242 insertions(+), 24 deletions(-) diff --git a/bake/bake.go b/bake/bake.go index 3a9e63abdf20..f9c1976fa068 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -347,6 +347,8 @@ func ParseFiles(files []File, defaults, vars map[string]string, opts ...ParseOpt err = formatHCLError(err, files) }() + frel := fileRelativePaths(opts) + var c Config var composeFiles []File var hclFiles []*hcl.File @@ -374,7 +376,7 @@ func ParseFiles(files []File, defaults, vars map[string]string, opts ...ParseOpt } if len(composeFiles) > 0 { - cfg, cmperr := ParseComposeFiles(composeFiles, vars) + cfg, cmperr := parseComposeFilesWithBase(composeFiles, vars, frel) if cmperr != nil { return nil, nil, errors.Wrap(cmperr, "failed to parse compose file") } @@ -418,41 +420,51 @@ func ParseFiles(files []File, defaults, vars map[string]string, opts ...ParseOpt pm = *res } - for _, opt := range opts { - if opt.FileRelativePaths { - rebaseContextPaths(&c, files) - break - } + if frel { + rebaseContextPaths(&c) } return &c, &pm, nil } -func rebaseContextPaths(c *Config, files []File) { - base, ok := firstLocalFileDir(files) - if !ok { - return - } +func fileRelativePaths(opts []ParseOpt) bool { + return slices.ContainsFunc(opts, func(opt ParseOpt) bool { + return opt.FileRelativePaths + }) +} + +func rebaseContextPaths(c *Config) { for _, t := range c.Targets { - if t.Context != nil { - contextPath := rebaseContextPath(base, *t.Context) + t.rebaseContextPaths() + } +} + +func (t *Target) rebaseContextPaths() { + if t.Context != nil { + if t.hasContextBase { + contextPath := rebaseContextPath(t.contextBase, *t.Context) t.Context = &contextPath } - for k, v := range t.Contexts { + } else if t.hasDefaultContextBase { + contextPath := rebaseContextPath(t.defaultContextBase, ".") + t.Context = &contextPath + } + for k, v := range t.Contexts { + if base, ok := t.contextsBase[k]; ok { t.Contexts[k] = rebaseContextPath(base, v) } } } -func firstLocalFileDir(files []File) (string, bool) { - if len(files) == 0 || files[0].Name == "-" || urlutil.IsRemoteURL(files[0].Name) { +func localFileDir(name string) (string, bool) { + if name == "" || name == "-" || urlutil.IsRemoteURL(name) { return "", false } - return filepath.Dir(files[0].Name), true + return filepath.Dir(name), true } func rebaseContextPath(base, p string) string { - if p == "" || isSpecialContextPath(p) || filepath.IsAbs(p) { + if base == "" || p == "" || isSpecialContextPath(p) || filepath.IsAbs(p) { return p } return osutil.SanitizePath(filepath.Join(base, filepath.FromSlash(p))) @@ -831,6 +843,12 @@ type Target struct { // linked is a private field to mark a target used as a linked one linked bool + + defaultContextBase string + hasDefaultContextBase bool + contextBase string + hasContextBase bool + contextsBase map[string]string } func (t *Target) MarshalJSON() ([]byte, error) { @@ -879,10 +897,46 @@ func (t *Target) MarshalJSON() ([]byte, error) { var ( _ hclparser.WithEvalContexts = &Target{} _ hclparser.WithGetName = &Target{} + _ hclparser.WithBlockSource = &Target{} _ hclparser.WithEvalContexts = &Group{} _ hclparser.WithGetName = &Group{} ) +func (t *Target) SetBlockSource(block *hcl.Block) { + base, _ := localFileDir(block.DefRange.Filename) + t.defaultContextBase = base + t.hasDefaultContextBase = true + + content, _, diags := block.Body.PartialContent(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "context"}, + {Name: "contexts"}, + }, + }) + if diags.HasErrors() { + return + } + if _, ok := content.Attributes["context"]; ok { + t.contextBase = base + t.hasContextBase = true + } + if _, ok := content.Attributes["contexts"]; ok { + t.setContextsBase(base) + } +} + +func (t *Target) setContextsBase(base string) { + if len(t.Contexts) == 0 { + return + } + if t.contextsBase == nil { + t.contextsBase = map[string]string{} + } + for k := range t.Contexts { + t.contextsBase[k] = base + } +} + func (t *Target) normalize() { t.Annotations = removeDupesStr(t.Annotations) t.Attest = t.Attest.Normalize() @@ -913,8 +967,14 @@ func (t *Target) normalize() { } func (t *Target) Merge(t2 *Target) { + if t2.hasDefaultContextBase { + t.defaultContextBase = t2.defaultContextBase + t.hasDefaultContextBase = true + } if t2.Context != nil { t.Context = t2.Context + t.contextBase = t2.contextBase + t.hasContextBase = t2.hasContextBase } if t2.Dockerfile != nil { t.Dockerfile = t2.Dockerfile @@ -936,6 +996,14 @@ func (t *Target) Merge(t2 *Target) { t.Contexts = map[string]string{} } t.Contexts[k] = v + if t.contextsBase == nil { + t.contextsBase = map[string]string{} + } + if base, ok := t2.contextsBase[k]; ok { + t.contextsBase[k] = base + } else { + delete(t.contextsBase, k) + } } for k, v := range t2.Labels { if v == nil { diff --git a/bake/bake_test.go b/bake/bake_test.go index 379830c6f579..3e3b516b11ee 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -805,6 +805,70 @@ target "app" { require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), bo["app"].Inputs.NamedContexts["shared"].Path) } +func TestDefaultContextRebase(t *testing.T) { + fp := File{ + Name: filepath.Join("definitions", "docker-bake.hcl"), + Data: []byte(` +target "app" { + dockerfile-inline = < 0 { + var c Config + for _, f := range fs { + cfg, err := parseComposeFiles([]File{f}, envs) + if err != nil { + return nil, err + } + setComposeContextBase(cfg, f.Name) + c = mergeConfig(c, *cfg) + c = dedupeConfig(c) + } + return &c, nil + } + + return parseComposeFiles(fs, envs) +} + +func parseComposeFiles(fs []File, envs map[string]string) (*Config, error) { var cfgs []composetypes.ConfigFile for _, f := range fs { cfgs = append(cfgs, composetypes.ConfigFile{ @@ -36,6 +67,19 @@ func ParseComposeFiles(fs []File, envOverrides map[string]string) (*Config, erro return ParseCompose(cfgs, envs) } +func setComposeContextBase(c *Config, name string) { + base, _ := localFileDir(name) + for _, t := range c.Targets { + t.defaultContextBase = base + t.hasDefaultContextBase = true + if t.Context != nil { + t.contextBase = base + t.hasContextBase = true + } + t.setContextsBase(base) + } +} + func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Config, error) { cfg, err := loadComposeFiles(cfgs, envs) if err != nil { diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go index 6b62efad0521..ec7bf19036e0 100644 --- a/bake/hclparser/hclparser.go +++ b/bake/hclparser/hclparser.go @@ -91,6 +91,10 @@ type WithGetName interface { GetName(ectx *hcl.EvalContext, block *hcl.Block, loadDeps func(hcl.Expression) hcl.Diagnostics) (string, error) } +type WithBlockSource interface { + SetBlockSource(block *hcl.Block) +} + // errUndefined is returned when a variable or function is not defined. type errUndefined struct{} @@ -944,6 +948,10 @@ func Parse(b hcl.Body, opt Opt, val any) (*ParseMeta, hcl.Diagnostics) { vvs := p.blockValues[b] for _, vv := range vvs { + if v, ok := vv.Interface().(WithBlockSource); ok { + v.SetBlockSource(b) + } + t := types[b.Type] lblIndex, lblExists := getNameIndex(vv) lblName, _ := getName(vv) diff --git a/docs/bake-reference.md b/docs/bake-reference.md index 832d28f312d7..07594d2a77dc 100644 --- a/docs/bake-reference.md +++ b/docs/bake-reference.md @@ -417,9 +417,9 @@ target "app" { This resolves to the current working directory (`"."`) by default. Set `BUILDX_BAKE_FILE_RELATIVE_PATHS=1` to resolve local directory paths in -`target.context` and `target.contexts` relative to the directory of the first -Bake file. Use `cwd://` for paths that should remain relative to the current -working directory when this opt-in is enabled. +`target.context` and `target.contexts` relative to the Bake or Compose file +that defines each path. Use `cwd://` for paths that should remain relative to +the current working directory when this opt-in is enabled. ```console $ docker buildx bake --print -f - <<< 'target "default" {}' diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 30102a41a830..5edb3fdb92a0 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -155,7 +155,7 @@ Multiple definitions can be specified by separating them with the system's path By default, local directory build contexts in Bake files are resolved from the current working directory. To opt in to resolving local directory build contexts -from the directory of the first Bake file, set +from the Bake or Compose file that defines each path, set `BUILDX_BAKE_FILE_RELATIVE_PATHS=1`. Use the `cwd://` prefix for paths that should remain relative to the current working directory. diff --git a/tests/bake.go b/tests/bake.go index 17ae3810c507..90faaac6ae8e 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -715,6 +715,34 @@ services: require.FileExists(t, filepath.Join(dirDest, "shared-marker")) }) + t.Run("default context", func(t *testing.T) { + bakefile := []byte(` +target "default" { + dockerfile-inline = < Date: Fri, 10 Jul 2026 16:28:14 +0200 Subject: [PATCH 3/4] bake: align compose path base with compose Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- bake/bake.go | 5 +++- bake/compose.go | 41 ++++++++++++--------------------- bake/compose_test.go | 39 +++++++++++++++++++++++++++++++ docs/bake-reference.md | 7 +++--- docs/reference/buildx_bake.md | 8 ++++--- tests/bake.go | 43 +++++++++++++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 34 deletions(-) diff --git a/bake/bake.go b/bake/bake.go index f9c1976fa068..e50fda4dcc5b 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -376,10 +376,13 @@ func ParseFiles(files []File, defaults, vars map[string]string, opts ...ParseOpt } if len(composeFiles) > 0 { - cfg, cmperr := parseComposeFilesWithBase(composeFiles, vars, frel) + cfg, cmperr := ParseComposeFiles(composeFiles, vars) if cmperr != nil { return nil, nil, errors.Wrap(cmperr, "failed to parse compose file") } + if frel { + setComposeContextBase(cfg, composeFiles) + } c = mergeConfig(c, *cfg) c = dedupeConfig(c) } diff --git a/bake/compose.go b/bake/compose.go index 0a604849e748..c6eabcb31a4c 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -22,38 +22,19 @@ import ( ) func ParseComposeFiles(fs []File, envOverrides map[string]string, opts ...ParseOpt) (*Config, error) { - frel := fileRelativePaths(opts) - cfg, err := parseComposeFilesWithBase(fs, envOverrides, frel) + envs, err := composeEnv(envOverrides) if err != nil { return nil, err } - if frel { - rebaseContextPaths(cfg) - } - return cfg, nil -} - -func parseComposeFilesWithBase(fs []File, envOverrides map[string]string, withBase bool) (*Config, error) { - envs, err := composeEnv(envOverrides) + cfg, err := parseComposeFiles(fs, envs) if err != nil { return nil, err } - - if withBase && len(fs) > 0 { - var c Config - for _, f := range fs { - cfg, err := parseComposeFiles([]File{f}, envs) - if err != nil { - return nil, err - } - setComposeContextBase(cfg, f.Name) - c = mergeConfig(c, *cfg) - c = dedupeConfig(c) - } - return &c, nil + if fileRelativePaths(opts) { + setComposeContextBase(cfg, fs) + rebaseContextPaths(cfg) } - - return parseComposeFiles(fs, envs) + return cfg, nil } func parseComposeFiles(fs []File, envs map[string]string) (*Config, error) { @@ -67,8 +48,14 @@ func parseComposeFiles(fs []File, envs map[string]string) (*Config, error) { return ParseCompose(cfgs, envs) } -func setComposeContextBase(c *Config, name string) { - base, _ := localFileDir(name) +func setComposeContextBase(c *Config, files []File) { + if len(files) == 0 { + return + } + base, ok := localFileDir(files[0].Name) + if !ok { + return + } for _, t := range c.Targets { t.defaultContextBase = base t.hasDefaultContextBase = true diff --git a/bake/compose_test.go b/bake/compose_test.go index da8ba2cfa9f1..3782e2eeaaec 100644 --- a/bake/compose_test.go +++ b/bake/compose_test.go @@ -174,6 +174,45 @@ services: require.Equal(t, "webapp", *c.Targets[1].Target) } +func TestComposeProjectBase(t *testing.T) { + fp := File{ + Name: filepath.Join("project", "compose.yml"), + Data: []byte(` +services: + app: + build: + context: ./app +`), + } + fp2 := File{ + Name: filepath.Join("overrides", "compose.yml"), + Data: []byte(` +services: + app: + build: + additional_contexts: + shared: ./shared + other: + build: + context: ./other +`), + } + + c, err := ParseComposeFiles([]File{fp, fp2}, nil, ParseOpt{ + FileRelativePaths: true, + }) + require.NoError(t, err) + + targets := map[string]*Target{} + for _, t := range c.Targets { + targets[t.Name] = t + } + + require.Equal(t, filepath.ToSlash(filepath.Clean("project/app")), *targets["app"].Context) + require.Equal(t, filepath.ToSlash(filepath.Clean("project/shared")), targets["app"].Contexts["shared"]) + require.Equal(t, filepath.ToSlash(filepath.Clean("project/other")), *targets["other"].Context) +} + func TestBuildArgEnvCompose(t *testing.T) { dt := []byte(` version: "3.8" diff --git a/docs/bake-reference.md b/docs/bake-reference.md index 07594d2a77dc..800e0abbc383 100644 --- a/docs/bake-reference.md +++ b/docs/bake-reference.md @@ -417,9 +417,10 @@ target "app" { This resolves to the current working directory (`"."`) by default. Set `BUILDX_BAKE_FILE_RELATIVE_PATHS=1` to resolve local directory paths in -`target.context` and `target.contexts` relative to the Bake or Compose file -that defines each path. Use `cwd://` for paths that should remain relative to -the current working directory when this opt-in is enabled. +`target.context` and `target.contexts` relative to the Bake file that defines +each path. Compose files use the first Compose file directory as the base, which +matches Compose project directory semantics. Use `cwd://` for paths that should +remain relative to the current working directory when this opt-in is enabled. ```console $ docker buildx bake --print -f - <<< 'target "default" {}' diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 5edb3fdb92a0..683a06c6fb74 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -155,9 +155,11 @@ Multiple definitions can be specified by separating them with the system's path By default, local directory build contexts in Bake files are resolved from the current working directory. To opt in to resolving local directory build contexts -from the Bake or Compose file that defines each path, set -`BUILDX_BAKE_FILE_RELATIVE_PATHS=1`. Use the `cwd://` prefix for paths that -should remain relative to the current working directory. +from the Bake file that defines each path, set +`BUILDX_BAKE_FILE_RELATIVE_PATHS=1`. Compose files use the first Compose file +directory as the base, which matches Compose project directory semantics. Use +the `cwd://` prefix for paths that should remain relative to the current working +directory. You can pass the names of the targets to build, to build only specific target(s). The following example builds the `db` and `webapp-release` targets that are diff --git a/tests/bake.go b/tests/bake.go index 90faaac6ae8e..032d962c148c 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -715,6 +715,49 @@ services: require.FileExists(t, filepath.Join(dirDest, "shared-marker")) }) + t.Run("compose project", func(t *testing.T) { + composefile := []byte(` +services: + app: + build: + context: ./app + dockerfile_inline: | + FROM scratch + COPY marker /marker + COPY --from=shared shared-marker /shared-marker +`) + overridefile := []byte(` +services: + app: + build: + additional_contexts: + shared: ./shared +`) + + dir := tmpdir( + t, + fstest.CreateDir("project", 0700), + fstest.CreateDir("project/app", 0700), + fstest.CreateDir("project/shared", 0700), + fstest.CreateDir("overrides", 0700), + fstest.CreateFile("project/compose.yml", composefile, 0600), + fstest.CreateFile("project/app/marker", []byte("marker"), 0600), + fstest.CreateFile("project/shared/shared-marker", []byte("shared"), 0600), + fstest.CreateFile("overrides/compose.yml", overridefile, 0600), + ) + dirDest := t.TempDir() + + out, err := bakeCmd( + sb, + withDir(dir), + withArgs("--file", "project/compose.yml", "--file", "overrides/compose.yml", "--set", "app.output=type=local,dest="+dirDest), + withEnv("BUILDX_BAKE_FILE_RELATIVE_PATHS=1"), + ) + require.NoError(t, err, out) + require.FileExists(t, filepath.Join(dirDest, "marker")) + require.FileExists(t, filepath.Join(dirDest, "shared-marker")) + }) + t.Run("default context", func(t *testing.T) { bakefile := []byte(` target "default" { From 1591d9d0801fca93550453f5209c4756512ae9fe Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:21:00 +0200 Subject: [PATCH 4/4] bake: fix inherited file-relative contexts Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- bake/bake.go | 8 ++++++-- bake/bake_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/bake/bake.go b/bake/bake.go index e50fda4dcc5b..2ccb66e95899 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -449,8 +449,7 @@ func (t *Target) rebaseContextPaths() { t.Context = &contextPath } } else if t.hasDefaultContextBase { - contextPath := rebaseContextPath(t.defaultContextBase, ".") - t.Context = &contextPath + t.useDefaultContextBase = true } for k, v := range t.Contexts { if base, ok := t.contextsBase[k]; ok { @@ -755,6 +754,9 @@ func (c Config) ResolveTarget(name string, overrides map[string]map[string]Overr t.Inherits = nil if t.Context == nil { s := "." + if t.useDefaultContextBase { + s = rebaseContextPath(t.defaultContextBase, ".") + } t.Context = &s } if t.Dockerfile == nil || (t.Dockerfile != nil && *t.Dockerfile == "") { @@ -849,6 +851,7 @@ type Target struct { defaultContextBase string hasDefaultContextBase bool + useDefaultContextBase bool contextBase string hasContextBase bool contextsBase map[string]string @@ -973,6 +976,7 @@ func (t *Target) Merge(t2 *Target) { if t2.hasDefaultContextBase { t.defaultContextBase = t2.defaultContextBase t.hasDefaultContextBase = true + t.useDefaultContextBase = t2.useDefaultContextBase } if t2.Context != nil { t.Context = t2.Context diff --git a/bake/bake_test.go b/bake/bake_test.go index 3e3b516b11ee..49cd0e242e12 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -869,6 +869,55 @@ target "other" { require.Equal(t, filepath.ToSlash(filepath.Clean("two")), *m["other"].Context) } +func TestInheritedContextRebase(t *testing.T) { + t.Run("same file", func(t *testing.T) { + fp := File{ + Name: filepath.Join("subdir", "docker-bake.hcl"), + Data: []byte(` +target "base" { + context = "basectx" +} + +target "app" { + inherits = ["base"] + tags = ["app:latest"] +}`), + } + + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{ + FileRelativePaths: true, + }) + require.NoError(t, err) + + require.Equal(t, filepath.ToSlash(filepath.Clean("subdir/basectx")), *m["app"].Context) + }) + + t.Run("cross file", func(t *testing.T) { + fp1 := File{ + Name: filepath.Join("one", "docker-bake.hcl"), + Data: []byte(` +target "base" { + context = "basectx" +}`), + } + fp2 := File{ + Name: filepath.Join("two", "docker-bake.hcl"), + Data: []byte(` +target "app" { + inherits = ["base"] + tags = ["app:latest"] +}`), + } + + m, _, err := ReadTargets(context.TODO(), []File{fp1, fp2}, []string{"app"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{ + FileRelativePaths: true, + }) + require.NoError(t, err) + + require.Equal(t, filepath.ToSlash(filepath.Clean("one/basectx")), *m["app"].Context) + }) +} + func TestOverridesNotRebased(t *testing.T) { fp := File{ Name: filepath.Join("subdir", "docker-bake.hcl"),