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
+84 -16
View File
@@ -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 {
t.rebaseContextPaths()
}
}
func (t *Target) rebaseContextPaths() {
if t.Context != nil {
contextPath := rebaseContextPath(base, *t.Context)
if t.hasContextBase {
contextPath := rebaseContextPath(t.contextBase, *t.Context)
t.Context = &contextPath
}
} 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 {
+71 -1
View File
@@ -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 = <<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) {
fp := File{
Name: filepath.Join("subdir", "docker-bake.hcl"),
@@ -839,21 +903,27 @@ services:
context: ./dockerfiles/debian
additional_contexts:
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,
})
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"])
require.Equal(t, filepath.ToSlash(filepath.Clean("tests")), *m["implicit"].Context)
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)
require.Equal(t, filepath.ToSlash(filepath.Clean("tests")), bo["implicit"].Inputs.ContextPath)
}
func TestOverrideMerge(t *testing.T) {
+45 -1
View File
@@ -21,11 +21,42 @@ import (
"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)
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
}
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 {
+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)
}
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)
+3 -3
View File
@@ -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" {}'
+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
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.
+28
View File
@@ -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 = <<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) {
bakefile := []byte(`
target "default" {