Merge pull request #3935 from crazy-max/bake-file-relative-paths

bake: add file-relative path opt-in
This commit is contained in:
Tõnis Tiigi
2026-07-21 18:44:27 -07:00
committed by GitHub
9 changed files with 716 additions and 6 deletions
+181 -3
View File
@@ -23,6 +23,7 @@ import (
"github.com/docker/buildx/bake/hclparser" "github.com/docker/buildx/bake/hclparser"
"github.com/docker/buildx/build" "github.com/docker/buildx/build"
"github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/osutil"
"github.com/docker/buildx/util/platformutil" "github.com/docker/buildx/util/platformutil"
"github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/progress"
"github.com/docker/buildx/util/urlutil" "github.com/docker/buildx/util/urlutil"
@@ -46,6 +47,10 @@ type File struct {
Data []byte Data []byte
} }
type ParseOpt struct {
FileRelativePaths bool
}
type Override struct { type Override struct {
Value string Value string
ArrValue []string ArrValue []string
@@ -197,8 +202,8 @@ func ListTargets(files []File) ([]string, error) {
return dedupSlice(targets), nil 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) { 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) c, _, err := ParseFiles(files, defaults, vars, opts...)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -337,11 +342,13 @@ func (c Config) matchNames(pattern string) ([]string, error) {
return names, nil 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() { defer func() {
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
@@ -373,6 +380,9 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
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")
} }
if frel {
setComposeContextBase(cfg, composeFiles)
}
c = mergeConfig(c, *cfg) c = mergeConfig(c, *cfg)
c = dedupeConfig(c) c = dedupeConfig(c)
} }
@@ -413,9 +423,78 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
pm = *res pm = *res
} }
if frel {
rebaseContextPaths(&c)
}
return &c, &pm, nil return &c, &pm, nil
} }
func fileRelativePaths(opts []ParseOpt) bool {
return slices.ContainsFunc(opts, func(opt ParseOpt) bool {
return opt.FileRelativePaths
})
}
func rebaseContextPaths(c *Config) {
targets := make(map[string]*Target, len(c.Targets))
for _, t := range c.Targets {
targets[t.Name] = t
}
for _, t := range c.Targets {
if ref := targets[t.contextBaseRef]; ref != nil {
switch {
case ref.hasContextBase:
t.contextBase = ref.contextBase
t.hasContextBase = true
case ref.hasDefaultContextBase:
t.contextBase = ref.defaultContextBase
t.hasContextBase = true
}
}
t.rebaseContextPaths()
}
}
func (t *Target) rebaseContextPaths() {
if t.Context != nil {
if t.hasContextBase {
contextPath := rebaseContextPath(t.contextBase, *t.Context)
t.Context = &contextPath
}
} else if t.hasDefaultContextBase {
t.useDefaultContextBase = true
}
for k, v := range t.Contexts {
if base, ok := t.contextsBase[k]; ok {
t.Contexts[k] = rebaseContextPath(base, v)
}
}
}
func localFileDir(name string) (string, bool) {
if name == "" || name == "-" || urlutil.IsRemoteURL(name) {
return "", false
}
return filepath.Dir(name), true
}
func rebaseContextPath(base, p string) string {
if base == "" || 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 { func dedupeConfig(c Config) Config {
c2 := c c2 := c
c2.Groups = make([]*Group, 0, len(c2.Groups)) c2.Groups = make([]*Group, 0, len(c2.Groups))
@@ -690,6 +769,9 @@ func (c Config) ResolveTarget(name string, overrides map[string]map[string]Overr
t.Inherits = nil t.Inherits = nil
if t.Context == nil { if t.Context == nil {
s := "." s := "."
if t.useDefaultContextBase {
s = rebaseContextPath(t.defaultContextBase, ".")
}
t.Context = &s t.Context = &s
} }
if t.Dockerfile == nil || (t.Dockerfile != nil && *t.Dockerfile == "") { if t.Dockerfile == nil || (t.Dockerfile != nil && *t.Dockerfile == "") {
@@ -781,6 +863,14 @@ 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
useDefaultContextBase bool
contextBase string
hasContextBase bool
contextBaseRef string
contextsBase map[string]string
} }
func (t *Target) MarshalJSON() ([]byte, error) { func (t *Target) MarshalJSON() ([]byte, error) {
@@ -829,10 +919,82 @@ 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 attr, ok := content.Attributes["context"]; ok {
t.contextBase = base
t.hasContextBase = true
t.contextBaseRef = targetContextRef(attr.Expr)
}
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 targetContextRef(expr hcl.Expression) string {
traversal, diags := hcl.AbsTraversalForExpr(expr)
if diags.HasErrors() || len(traversal) != 3 {
return ""
}
root, ok := traversal[0].(hcl.TraverseRoot)
if !ok || root.Name != "target" {
return ""
}
target, ok := traversalStepName(traversal[1])
if !ok {
return ""
}
field, ok := traversal[2].(hcl.TraverseAttr)
if !ok || field.Name != "context" {
return ""
}
return target
}
func traversalStepName(step hcl.Traverser) (string, bool) {
switch step := step.(type) {
case hcl.TraverseAttr:
return step.Name, true
case hcl.TraverseIndex:
key, err := convert.Convert(step.Key, cty.String)
if err != nil || key.IsNull() || !key.IsKnown() {
return "", false
}
return key.AsString(), true
default:
return "", false
}
}
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()
@@ -863,8 +1025,16 @@ 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
t.useDefaultContextBase = t2.useDefaultContextBase
}
if t2.Context != nil { if t2.Context != nil {
t.Context = t2.Context t.Context = t2.Context
t.contextBase = t2.contextBase
t.hasContextBase = t2.hasContextBase
t.contextBaseRef = t2.contextBaseRef
} }
if t2.Dockerfile != nil { if t2.Dockerfile != nil {
t.Dockerfile = t2.Dockerfile t.Dockerfile = t2.Dockerfile
@@ -886,6 +1056,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 {
+236
View File
@@ -763,6 +763,242 @@ func TestHCLDockerfileCwdPrefix(t *testing.T) {
assert.Equal(t, ".", bo["app"].Inputs.ContextPath) 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 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 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 TestTargetReferenceContextRebase(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" {
context = target.base.context
}`),
}
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"),
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
implicit:
build:
dockerfile_inline: |
FROM scratch
`),
}
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) { func TestOverrideMerge(t *testing.T) {
fp := File{ fp := File{
Name: "docker-bake.hcl", Name: "docker-bake.hcl",
+32 -1
View File
@@ -21,11 +21,23 @@ 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) {
envs, err := composeEnv(envOverrides) envs, err := composeEnv(envOverrides)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg, err := parseComposeFiles(fs, envs)
if err != nil {
return nil, err
}
if fileRelativePaths(opts) {
setComposeContextBase(cfg, fs)
rebaseContextPaths(cfg)
}
return cfg, nil
}
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 +48,25 @@ func ParseComposeFiles(fs []File, envOverrides map[string]string) (*Config, erro
return ParseCompose(cfgs, envs) return ParseCompose(cfgs, envs)
} }
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
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 {
+39
View File
@@ -174,6 +174,45 @@ services:
require.Equal(t, "webapp", *c.Targets[1].Target) 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) { func TestBuildArgEnvCompose(t *testing.T) {
dt := []byte(` dt := []byte(`
version: "3.8" version: "3.8"
+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)
+23 -2
View File
@@ -12,6 +12,7 @@ import (
"os" "os"
"slices" "slices"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"text/tabwriter" "text/tabwriter"
@@ -48,6 +49,7 @@ import (
const ( const (
bakeEnvFileSeparator = "BUILDX_BAKE_PATH_SEPARATOR" bakeEnvFileSeparator = "BUILDX_BAKE_PATH_SEPARATOR"
bakeEnvFilePath = "BUILDX_BAKE_FILE" bakeEnvFilePath = "BUILDX_BAKE_FILE"
bakeEnvFileRelative = "BUILDX_BAKE_FILE_RELATIVE_PATHS"
) )
type bakeOptions struct { type bakeOptions struct {
@@ -225,9 +227,16 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
if err != nil { if err != nil {
return err return err
} }
fileRelativePaths, err := bakeFileRelativePaths()
if err != nil {
return err
}
parseOpt := bake.ParseOpt{
FileRelativePaths: fileRelativePaths,
}
if in.list != "" { if in.list != "" {
cfg, pm, err := bake.ParseFiles(files, defaults, vars) cfg, pm, err := bake.ParseFiles(files, defaults, vars, parseOpt)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@@ -674,6 +683,18 @@ func bakeArgs(args []string) (url, cmdContext string, targets []string) {
return url, cmdContext, targets 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) { 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 lnames []string // local
var rnames []string // remote var rnames []string // remote
+5
View File
@@ -416,6 +416,11 @@ 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
`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 ```console
$ docker buildx bake --print -f - <<< 'target "default" {}' $ docker buildx bake --print -f - <<< 'target "default" {}'
+8
View File
@@ -153,6 +153,14 @@ 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 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`. (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 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). 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 The following example builds the `db` and `webapp-release` targets that are
defined in the `docker-bake.dev.hcl` file: defined in the `docker-bake.dev.hcl` file:
+184
View File
@@ -45,6 +45,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
testBakePrintRemoteContextSubdir, testBakePrintRemoteContextSubdir,
testBakeLocal, testBakeLocal,
testBakeLocalMulti, testBakeLocalMulti,
testBakeFileRelativePaths,
testBakeLocalExportDeleteMode, testBakeLocalExportDeleteMode,
testBakeRemote, testBakeRemote,
testBakeRemoteAuth, testBakeRemoteAuth,
@@ -674,6 +675,189 @@ services:
require.FileExists(t, filepath.Join(dirDest2, "foo")) 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("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" {
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("hcl target reference", func(t *testing.T) {
baseBakefile := []byte(`
target "base" {
context = "basectx"
}
`)
appBakefile := []byte(`
target "app" {
context = target.base.context
dockerfile-inline = <<EOT
FROM scratch
COPY marker /marker
EOT
}
`)
dir := tmpdir(
t,
fstest.CreateDir("one", 0700),
fstest.CreateDir("one/basectx", 0700),
fstest.CreateDir("two", 0700),
fstest.CreateDir("two/basectx", 0700),
fstest.CreateFile("one/docker-bake.hcl", baseBakefile, 0600),
fstest.CreateFile("one/basectx/marker", []byte("source-file"), 0600),
fstest.CreateFile("two/docker-bake.hcl", appBakefile, 0600),
fstest.CreateFile("two/basectx/marker", []byte("consumer-file"), 0600),
)
dirDest := t.TempDir()
out, err := bakeCmd(
sb,
withDir(dir),
withArgs("--file", "one/docker-bake.hcl", "--file", "two/docker-bake.hcl", "--set", "app.output=type=local,dest="+dirDest, "app"),
withEnv("BUILDX_BAKE_FILE_RELATIVE_PATHS=1"),
)
require.NoError(t, err, out)
dt, err := os.ReadFile(filepath.Join(dirDest, "marker"))
require.NoError(t, err)
require.Equal(t, "source-file", string(dt))
})
t.Run("cwd prefix", func(t *testing.T) {
bakefile := []byte(`
target "default" {
context = "cwd://."
dockerfile-inline = <<EOT
FROM scratch
COPY root-marker /root-marker
EOT
}
`)
dir := tmpdir(
t,
fstest.CreateDir("definitions", 0700),
fstest.CreateFile("definitions/docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("root-marker", []byte("root"), 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, "root-marker"))
})
}
func testBakeLocalExportDeleteMode(t *testing.T, sb integration.Sandbox) { func testBakeLocalExportDeleteMode(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(` dockerfile := []byte(`
FROM scratch FROM scratch