bake: add file-relative path opt-in

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
committed by CrazyMax
parent 9618d82eff
commit b38d1004e3
6 changed files with 250 additions and 5 deletions
+53 -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,7 +342,7 @@ 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)
}() }()
@@ -413,9 +418,54 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
pm = *res pm = *res
} }
for _, opt := range opts {
if opt.FileRelativePaths {
rebaseContextPaths(&c, files)
break
}
}
return &c, &pm, nil return &c, &pm, nil
} }
func rebaseContextPaths(c *Config, files []File) {
base, ok := firstLocalFileDir(files)
if !ok {
return
}
for _, t := range c.Targets {
if t.Context != nil {
contextPath := rebaseContextPath(base, *t.Context)
t.Context = &contextPath
}
for k, v := range t.Contexts {
t.Contexts[k] = rebaseContextPath(base, v)
}
}
}
func firstLocalFileDir(files []File) (string, bool) {
if len(files) == 0 || files[0].Name == "-" || urlutil.IsRemoteURL(files[0].Name) {
return "", false
}
return filepath.Dir(files[0].Name), true
}
func rebaseContextPath(base, p string) string {
if 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))
+93
View File
@@ -763,6 +763,99 @@ 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 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
`),
}
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"debian"}, 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"])
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)
}
func TestOverrideMerge(t *testing.T) { func TestOverrideMerge(t *testing.T) {
fp := File{ fp := File{
Name: "docker-bake.hcl", Name: "docker-bake.hcl",
+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
+4
View File
@@ -416,6 +416,10 @@ 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 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.
```console ```console
$ docker buildx bake --print -f - <<< 'target "default" {}' $ docker buildx bake --print -f - <<< 'target "default" {}'
+6
View File
@@ -153,6 +153,12 @@ 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 directory of the first Bake file, set
`BUILDX_BAKE_FILE_RELATIVE_PATHS=1`. 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:
+71
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,76 @@ 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("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