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)
}()
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 {
+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)