bake: make file-relative paths definition-scoped

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2026-07-21 11:25:55 +02:00
parent b38d1004e3
commit 1dc55abd89
7 changed files with 242 additions and 24 deletions
+86 -18
View File
@@ -347,6 +347,8 @@ func ParseFiles(files []File, defaults, vars map[string]string, opts ...ParseOpt
err = formatHCLError(err, files) err = formatHCLError(err, files)
}() }()
frel := fileRelativePaths(opts)
var c Config var c Config
var composeFiles []File var composeFiles []File
var hclFiles []*hcl.File var hclFiles []*hcl.File
@@ -374,7 +376,7 @@ func ParseFiles(files []File, defaults, vars map[string]string, opts ...ParseOpt
} }
if len(composeFiles) > 0 { if len(composeFiles) > 0 {
cfg, cmperr := ParseComposeFiles(composeFiles, vars) cfg, cmperr := parseComposeFilesWithBase(composeFiles, vars, frel)
if cmperr != nil { if cmperr != nil {
return nil, nil, errors.Wrap(cmperr, "failed to parse compose file") 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 pm = *res
} }
for _, opt := range opts { if frel {
if opt.FileRelativePaths { rebaseContextPaths(&c)
rebaseContextPaths(&c, files)
break
}
} }
return &c, &pm, nil return &c, &pm, nil
} }
func rebaseContextPaths(c *Config, files []File) { func fileRelativePaths(opts []ParseOpt) bool {
base, ok := firstLocalFileDir(files) return slices.ContainsFunc(opts, func(opt ParseOpt) bool {
if !ok { return opt.FileRelativePaths
return })
} }
func rebaseContextPaths(c *Config) {
for _, t := range c.Targets { for _, t := range c.Targets {
if t.Context != nil { t.rebaseContextPaths()
contextPath := rebaseContextPath(base, *t.Context) }
}
func (t *Target) rebaseContextPaths() {
if t.Context != nil {
if t.hasContextBase {
contextPath := rebaseContextPath(t.contextBase, *t.Context)
t.Context = &contextPath 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) t.Contexts[k] = rebaseContextPath(base, v)
} }
} }
} }
func firstLocalFileDir(files []File) (string, bool) { func localFileDir(name string) (string, bool) {
if len(files) == 0 || files[0].Name == "-" || urlutil.IsRemoteURL(files[0].Name) { if name == "" || name == "-" || urlutil.IsRemoteURL(name) {
return "", false return "", false
} }
return filepath.Dir(files[0].Name), true return filepath.Dir(name), true
} }
func rebaseContextPath(base, p string) string { func rebaseContextPath(base, p string) string {
if p == "" || isSpecialContextPath(p) || filepath.IsAbs(p) { if base == "" || p == "" || isSpecialContextPath(p) || filepath.IsAbs(p) {
return p return p
} }
return osutil.SanitizePath(filepath.Join(base, filepath.FromSlash(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 is a private field to mark a target used as a linked one
linked bool linked bool
defaultContextBase string
hasDefaultContextBase bool
contextBase string
hasContextBase bool
contextsBase map[string]string
} }
func (t *Target) MarshalJSON() ([]byte, error) { func (t *Target) MarshalJSON() ([]byte, error) {
@@ -879,10 +897,46 @@ func (t *Target) MarshalJSON() ([]byte, error) {
var ( var (
_ hclparser.WithEvalContexts = &Target{} _ hclparser.WithEvalContexts = &Target{}
_ hclparser.WithGetName = &Target{} _ hclparser.WithGetName = &Target{}
_ hclparser.WithBlockSource = &Target{}
_ hclparser.WithEvalContexts = &Group{} _ hclparser.WithEvalContexts = &Group{}
_ hclparser.WithGetName = &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() { func (t *Target) normalize() {
t.Annotations = removeDupesStr(t.Annotations) t.Annotations = removeDupesStr(t.Annotations)
t.Attest = t.Attest.Normalize() t.Attest = t.Attest.Normalize()
@@ -913,8 +967,14 @@ func (t *Target) normalize() {
} }
func (t *Target) Merge(t2 *Target) { func (t *Target) Merge(t2 *Target) {
if t2.hasDefaultContextBase {
t.defaultContextBase = t2.defaultContextBase
t.hasDefaultContextBase = true
}
if t2.Context != nil { if t2.Context != nil {
t.Context = t2.Context t.Context = t2.Context
t.contextBase = t2.contextBase
t.hasContextBase = t2.hasContextBase
} }
if t2.Dockerfile != nil { if t2.Dockerfile != nil {
t.Dockerfile = t2.Dockerfile t.Dockerfile = t2.Dockerfile
@@ -936,6 +996,14 @@ func (t *Target) Merge(t2 *Target) {
t.Contexts = map[string]string{} t.Contexts = map[string]string{}
} }
t.Contexts[k] = v 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 { for k, v := range t2.Labels {
if v == nil { if v == nil {
+71 -1
View File
@@ -805,6 +805,70 @@ target "app" {
require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), bo["app"].Inputs.NamedContexts["shared"].Path) 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 = <<EOT
FROM scratch
EOT
}`),
}
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("definitions")), *m["app"].Context)
bo, err := TargetsToBuildOpt(m, &Input{})
require.NoError(t, err)
require.Equal(t, filepath.ToSlash(filepath.Clean("definitions")), bo["app"].Inputs.ContextPath)
}
func TestDefinitionPathBase(t *testing.T) {
fp1 := File{
Name: filepath.Join("one", "docker-bake.hcl"),
Data: []byte(`
target "app" {
context = "."
contexts = {
shared = "../shared"
}
}
target "implicit" {
dockerfile-inline = <<EOT
FROM scratch
EOT
}`),
}
fp2 := File{
Name: filepath.Join("two", "docker-bake.hcl"),
Data: []byte(`
target "app" {
tags = ["app:latest"]
}
target "other" {
context = "."
}`),
}
m, _, err := ReadTargets(context.TODO(), []File{fp1, fp2}, []string{"app", "implicit", "other"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{
FileRelativePaths: true,
})
require.NoError(t, err)
require.Equal(t, filepath.ToSlash(filepath.Clean("one")), *m["app"].Context)
require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), m["app"].Contexts["shared"])
require.Equal(t, []string{"app:latest"}, m["app"].Tags)
require.Equal(t, filepath.ToSlash(filepath.Clean("one")), *m["implicit"].Context)
require.Equal(t, filepath.ToSlash(filepath.Clean("two")), *m["other"].Context)
}
func TestOverridesNotRebased(t *testing.T) { func TestOverridesNotRebased(t *testing.T) {
fp := File{ fp := File{
Name: filepath.Join("subdir", "docker-bake.hcl"), Name: filepath.Join("subdir", "docker-bake.hcl"),
@@ -839,21 +903,27 @@ services:
context: ./dockerfiles/debian context: ./dockerfiles/debian
additional_contexts: additional_contexts:
shared: ../shared shared: ../shared
implicit:
build:
dockerfile_inline: |
FROM scratch
`), `),
} }
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"debian"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{ m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"debian", "implicit"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{
FileRelativePaths: true, FileRelativePaths: true,
}) })
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, filepath.ToSlash(filepath.Clean("tests/dockerfiles/debian")), *m["debian"].Context) 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"]) require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), m["debian"].Contexts["shared"])
require.Equal(t, filepath.ToSlash(filepath.Clean("tests")), *m["implicit"].Context)
bo, err := TargetsToBuildOpt(m, &Input{}) bo, err := TargetsToBuildOpt(m, &Input{})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, filepath.ToSlash(filepath.Clean("tests/dockerfiles/debian")), bo["debian"].Inputs.ContextPath) 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) require.Equal(t, filepath.Join("tests", "dockerfiles", "debian", "Dockerfile"), bo["debian"].Inputs.DockerfilePath)
require.Equal(t, filepath.ToSlash(filepath.Clean("tests")), bo["implicit"].Inputs.ContextPath)
} }
func TestOverrideMerge(t *testing.T) { func TestOverrideMerge(t *testing.T) {
+45 -1
View File
@@ -21,11 +21,42 @@ import (
"go.yaml.in/yaml/v3" "go.yaml.in/yaml/v3"
) )
func ParseComposeFiles(fs []File, envOverrides map[string]string) (*Config, error) { func ParseComposeFiles(fs []File, envOverrides map[string]string, opts ...ParseOpt) (*Config, error) {
frel := fileRelativePaths(opts)
cfg, err := parseComposeFilesWithBase(fs, envOverrides, frel)
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) envs, err := composeEnv(envOverrides)
if err != nil { if err != nil {
return nil, err 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
}
return parseComposeFiles(fs, envs)
}
func parseComposeFiles(fs []File, envs map[string]string) (*Config, error) {
var cfgs []composetypes.ConfigFile var cfgs []composetypes.ConfigFile
for _, f := range fs { for _, f := range fs {
cfgs = append(cfgs, composetypes.ConfigFile{ cfgs = append(cfgs, composetypes.ConfigFile{
@@ -36,6 +67,19 @@ func ParseComposeFiles(fs []File, envOverrides map[string]string) (*Config, erro
return ParseCompose(cfgs, envs) 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) { func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Config, error) {
cfg, err := loadComposeFiles(cfgs, envs) cfg, err := loadComposeFiles(cfgs, envs)
if err != nil { if err != nil {
+8
View File
@@ -91,6 +91,10 @@ type WithGetName interface {
GetName(ectx *hcl.EvalContext, block *hcl.Block, loadDeps func(hcl.Expression) hcl.Diagnostics) (string, error) 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. // errUndefined is returned when a variable or function is not defined.
type errUndefined struct{} type errUndefined struct{}
@@ -944,6 +948,10 @@ func Parse(b hcl.Body, opt Opt, val any) (*ParseMeta, hcl.Diagnostics) {
vvs := p.blockValues[b] vvs := p.blockValues[b]
for _, vv := range vvs { for _, vv := range vvs {
if v, ok := vv.Interface().(WithBlockSource); ok {
v.SetBlockSource(b)
}
t := types[b.Type] t := types[b.Type]
lblIndex, lblExists := getNameIndex(vv) lblIndex, lblExists := getNameIndex(vv)
lblName, _ := getName(vv) lblName, _ := getName(vv)
+3 -3
View File
@@ -417,9 +417,9 @@ target "app" {
This resolves to the current working directory (`"."`) by default. This resolves to the current working directory (`"."`) by default.
Set `BUILDX_BAKE_FILE_RELATIVE_PATHS=1` to resolve local directory paths in 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 `target.context` and `target.contexts` relative to the Bake or Compose file
Bake file. Use `cwd://` for paths that should remain relative to the current that defines each path. Use `cwd://` for paths that should remain relative to
working directory when this opt-in is enabled. the current working directory when this opt-in is enabled.
```console ```console
$ docker buildx bake --print -f - <<< 'target "default" {}' $ docker buildx bake --print -f - <<< 'target "default" {}'
+1 -1
View File
@@ -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 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 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 `BUILDX_BAKE_FILE_RELATIVE_PATHS=1`. Use the `cwd://` prefix for paths that
should remain relative to the current working directory. should remain relative to the current working directory.
+28
View File
@@ -715,6 +715,34 @@ services:
require.FileExists(t, filepath.Join(dirDest, "shared-marker")) require.FileExists(t, filepath.Join(dirDest, "shared-marker"))
}) })
t.Run("default context", func(t *testing.T) {
bakefile := []byte(`
target "default" {
dockerfile-inline = <<EOT
FROM scratch
COPY marker /marker
EOT
}
`)
dir := tmpdir(
t,
fstest.CreateDir("definitions", 0700),
fstest.CreateFile("definitions/docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("definitions/marker", []byte("marker"), 0600),
)
dirDest := t.TempDir()
out, err := bakeCmd(
sb,
withDir(dir),
withArgs("--file", "definitions/docker-bake.hcl", "--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"))
})
t.Run("cwd prefix", func(t *testing.T) { t.Run("cwd prefix", func(t *testing.T) {
bakefile := []byte(` bakefile := []byte(`
target "default" { target "default" {