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/build"
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/osutil"
"github.com/docker/buildx/util/platformutil"
"github.com/docker/buildx/util/progress"
"github.com/docker/buildx/util/urlutil"
@@ -46,6 +47,10 @@ type File struct {
Data []byte
}
type ParseOpt struct {
FileRelativePaths bool
}
type Override struct {
Value string
ArrValue []string
@@ -197,8 +202,8 @@ func ListTargets(files []File) ([]string, error) {
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) {
c, _, err := ParseFiles(files, defaults, vars)
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, opts...)
if err != nil {
return nil, nil, err
}
@@ -337,11 +342,13 @@ func (c Config) matchNames(pattern string) ([]string, error) {
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() {
err = formatHCLError(err, files)
}()
frel := fileRelativePaths(opts)
var c Config
var composeFiles []File
var hclFiles []*hcl.File
@@ -373,6 +380,9 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
if cmperr != nil {
return nil, nil, errors.Wrap(cmperr, "failed to parse compose file")
}
if frel {
setComposeContextBase(cfg, composeFiles)
}
c = mergeConfig(c, *cfg)
c = dedupeConfig(c)
}
@@ -413,9 +423,78 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
pm = *res
}
if frel {
rebaseContextPaths(&c)
}
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 {
c2 := c
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
if t.Context == nil {
s := "."
if t.useDefaultContextBase {
s = rebaseContextPath(t.defaultContextBase, ".")
}
t.Context = &s
}
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 bool
defaultContextBase string
hasDefaultContextBase bool
useDefaultContextBase bool
contextBase string
hasContextBase bool
contextBaseRef string
contextsBase map[string]string
}
func (t *Target) MarshalJSON() ([]byte, error) {
@@ -829,10 +919,82 @@ 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 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() {
t.Annotations = removeDupesStr(t.Annotations)
t.Attest = t.Attest.Normalize()
@@ -863,8 +1025,16 @@ func (t *Target) normalize() {
}
func (t *Target) Merge(t2 *Target) {
if t2.hasDefaultContextBase {
t.defaultContextBase = t2.defaultContextBase
t.hasDefaultContextBase = true
t.useDefaultContextBase = t2.useDefaultContextBase
}
if t2.Context != nil {
t.Context = t2.Context
t.contextBase = t2.contextBase
t.hasContextBase = t2.hasContextBase
t.contextBaseRef = t2.contextBaseRef
}
if t2.Dockerfile != nil {
t.Dockerfile = t2.Dockerfile
@@ -886,6 +1056,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 {
+236
View File
@@ -763,6 +763,242 @@ func TestHCLDockerfileCwdPrefix(t *testing.T) {
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) {
fp := File{
Name: "docker-bake.hcl",
+32 -1
View File
@@ -21,11 +21,23 @@ 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) {
envs, err := composeEnv(envOverrides)
if err != nil {
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
for _, f := range fs {
cfgs = append(cfgs, composetypes.ConfigFile{
@@ -36,6 +48,25 @@ func ParseComposeFiles(fs []File, envOverrides map[string]string) (*Config, erro
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) {
cfg, err := loadComposeFiles(cfgs, envs)
if err != nil {
+39
View File
@@ -174,6 +174,45 @@ services:
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) {
dt := []byte(`
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)
}
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)
+23 -2
View File
@@ -12,6 +12,7 @@ import (
"os"
"slices"
"sort"
"strconv"
"strings"
"sync"
"text/tabwriter"
@@ -48,6 +49,7 @@ import (
const (
bakeEnvFileSeparator = "BUILDX_BAKE_PATH_SEPARATOR"
bakeEnvFilePath = "BUILDX_BAKE_FILE"
bakeEnvFileRelative = "BUILDX_BAKE_FILE_RELATIVE_PATHS"
)
type bakeOptions struct {
@@ -225,9 +227,16 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
if err != nil {
return err
}
fileRelativePaths, err := bakeFileRelativePaths()
if err != nil {
return err
}
parseOpt := bake.ParseOpt{
FileRelativePaths: fileRelativePaths,
}
if in.list != "" {
cfg, pm, err := bake.ParseFiles(files, defaults, vars)
cfg, pm, err := bake.ParseFiles(files, defaults, vars, parseOpt)
if err != nil {
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 {
return err
}
@@ -674,6 +683,18 @@ func bakeArgs(args []string) (url, cmdContext string, targets []string) {
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) {
var lnames []string // local
var rnames []string // remote
+5
View File
@@ -416,6 +416,11 @@ 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 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
$ 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
(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).
The following example builds the `db` and `webapp-release` targets that are
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,
testBakeLocal,
testBakeLocalMulti,
testBakeFileRelativePaths,
testBakeLocalExportDeleteMode,
testBakeRemote,
testBakeRemoteAuth,
@@ -674,6 +675,189 @@ services:
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) {
dockerfile := []byte(`
FROM scratch