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/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,7 +342,7 @@ 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)
}()
@@ -413,9 +418,54 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
pm = *res
}
for _, opt := range opts {
if opt.FileRelativePaths {
rebaseContextPaths(&c, files)
break
}
}
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 {
c2 := c
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)
}
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) {
fp := File{
Name: "docker-bake.hcl",
+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
+4
View File
@@ -416,6 +416,10 @@ 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.
```console
$ 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
(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).
The following example builds the `db` and `webapp-release` targets that are
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,
testBakeLocal,
testBakeLocalMulti,
testBakeFileRelativePaths,
testBakeLocalExportDeleteMode,
testBakeRemote,
testBakeRemoteAuth,
@@ -674,6 +675,76 @@ 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("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