From 1dc55abd89fee8d59a53032bf1336369c2aa3c56 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:01:09 +0200 Subject: [PATCH] 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 3a9e63abd..f9c1976fa 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 379830c6f..3e3b516b1 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 6b62efad0..ec7bf1903 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 832d28f31..07594d2a7 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 30102a41a..5edb3fdb9 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 17ae3810c..90faaac6a 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 = <