Skip to content

Commit 67b4fbe

Browse files
committed
bake: handle tilde expansion in filepaths
Signed-off-by: David Karlsson <[email protected]>
1 parent 70487be commit 67b4fbe

File tree

4 files changed

+389
-0
lines changed

4 files changed

+389
-0
lines changed

bake/bake.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/docker/buildx/bake/hclparser"
2222
"github.com/docker/buildx/build"
2323
"github.com/docker/buildx/util/buildflags"
24+
"github.com/docker/buildx/util/pathutil"
2425
"github.com/docker/buildx/util/platformutil"
2526
"github.com/docker/buildx/util/progress"
2627
"github.com/docker/cli/cli/config"
@@ -815,7 +816,76 @@ var (
815816
_ hclparser.WithGetName = &Group{}
816817
)
817818

819+
// expandPaths expands tilde in all path fields of the target
820+
func (t *Target) expandPaths() {
821+
// Expand context path
822+
if t.Context != nil {
823+
expanded := pathutil.ExpandTilde(*t.Context)
824+
t.Context = &expanded
825+
}
826+
827+
// Expand dockerfile path
828+
if t.Dockerfile != nil {
829+
expanded := pathutil.ExpandTilde(*t.Dockerfile)
830+
t.Dockerfile = &expanded
831+
}
832+
833+
// Expand named contexts
834+
if t.Contexts != nil {
835+
for k, v := range t.Contexts {
836+
t.Contexts[k] = pathutil.ExpandTilde(v)
837+
}
838+
}
839+
840+
// Expand secret file paths
841+
for _, s := range t.Secrets {
842+
if s.FilePath != "" {
843+
s.FilePath = pathutil.ExpandTilde(s.FilePath)
844+
}
845+
}
846+
847+
// Expand SSH key paths
848+
for _, s := range t.SSH {
849+
if len(s.Paths) > 0 {
850+
s.Paths = pathutil.ExpandTildePaths(s.Paths)
851+
}
852+
}
853+
854+
// Expand cache paths if they're local
855+
for _, c := range t.CacheFrom {
856+
if c.Type == "local" && c.Attrs != nil {
857+
if src, ok := c.Attrs["src"]; ok {
858+
c.Attrs["src"] = pathutil.ExpandTilde(src)
859+
}
860+
}
861+
}
862+
for _, c := range t.CacheTo {
863+
if c.Type == "local" && c.Attrs != nil {
864+
if dest, ok := c.Attrs["dest"]; ok {
865+
c.Attrs["dest"] = pathutil.ExpandTilde(dest)
866+
}
867+
}
868+
}
869+
870+
// Expand output paths
871+
for _, o := range t.Outputs {
872+
// Expand the Destination field
873+
if o.Destination != "" {
874+
o.Destination = pathutil.ExpandTilde(o.Destination)
875+
}
876+
// Also expand dest in Attrs if present
877+
if o.Attrs != nil {
878+
if dest, ok := o.Attrs["dest"]; ok {
879+
o.Attrs["dest"] = pathutil.ExpandTilde(dest)
880+
}
881+
}
882+
}
883+
}
884+
818885
func (t *Target) normalize() {
886+
// Expand tilde in all path fields
887+
t.expandPaths()
888+
819889
t.Annotations = removeDupesStr(t.Annotations)
820890
t.Attest = t.Attest.Normalize()
821891
t.Tags = removeDupesStr(t.Tags)

bake/tilde_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package bake
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/docker/buildx/util/buildflags"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestTargetExpandPaths(t *testing.T) {
13+
home, err := os.UserHomeDir()
14+
require.NoError(t, err)
15+
16+
// Create a target with tilde paths
17+
contextPath := "~/projects/test"
18+
dockerfilePath := "~/dockerfiles/app.Dockerfile"
19+
20+
target := &Target{
21+
Context: &contextPath,
22+
Dockerfile: &dockerfilePath,
23+
Contexts: map[string]string{
24+
"shared": "~/shared-configs",
25+
"libs": "/absolute/libs",
26+
},
27+
Secrets: []*buildflags.Secret{
28+
{
29+
ID: "npm",
30+
FilePath: "~/.npmrc",
31+
},
32+
{
33+
ID: "aws",
34+
FilePath: "~/.aws/credentials",
35+
},
36+
},
37+
SSH: []*buildflags.SSH{
38+
{
39+
ID: "default",
40+
Paths: []string{"~/.ssh/id_rsa", "~/.ssh/id_ed25519"},
41+
},
42+
},
43+
CacheFrom: []*buildflags.CacheOptionsEntry{
44+
{
45+
Type: "local",
46+
Attrs: map[string]string{
47+
"src": "~/.docker/cache",
48+
},
49+
},
50+
},
51+
CacheTo: []*buildflags.CacheOptionsEntry{
52+
{
53+
Type: "local",
54+
Attrs: map[string]string{
55+
"dest": "~/.docker/cache",
56+
},
57+
},
58+
},
59+
Outputs: []*buildflags.ExportEntry{
60+
{
61+
Type: "local",
62+
Destination: "~/builds/output",
63+
},
64+
},
65+
}
66+
67+
// Apply normalization which includes tilde expansion
68+
target.normalize()
69+
70+
// Verify expansions
71+
require.Equal(t, filepath.Join(home, "projects/test"), *target.Context)
72+
require.Equal(t, filepath.Join(home, "dockerfiles/app.Dockerfile"), *target.Dockerfile)
73+
74+
// Check named contexts
75+
require.Equal(t, filepath.Join(home, "shared-configs"), target.Contexts["shared"])
76+
require.Equal(t, "/absolute/libs", target.Contexts["libs"]) // Should remain unchanged
77+
78+
// Check secrets
79+
require.Equal(t, filepath.Join(home, ".npmrc"), target.Secrets[0].FilePath)
80+
require.Equal(t, filepath.Join(home, ".aws/credentials"), target.Secrets[1].FilePath)
81+
82+
// Check SSH paths
83+
require.Equal(t, filepath.Join(home, ".ssh/id_rsa"), target.SSH[0].Paths[0])
84+
require.Equal(t, filepath.Join(home, ".ssh/id_ed25519"), target.SSH[0].Paths[1])
85+
86+
// Check cache paths
87+
require.Equal(t, filepath.Join(home, ".docker/cache"), target.CacheFrom[0].Attrs["src"])
88+
require.Equal(t, filepath.Join(home, ".docker/cache"), target.CacheTo[0].Attrs["dest"])
89+
90+
// Check output paths (stored in Destination field, not Attrs)
91+
require.Equal(t, filepath.Join(home, "builds/output"), target.Outputs[0].Destination)
92+
}
93+
94+
func TestTargetExpandPathsSpecialCases(t *testing.T) {
95+
// Test that special prefixes and URLs are not expanded
96+
gitURL := "[email protected]:user/repo.git"
97+
dockerImage := "docker-image://myimage:latest"
98+
cwdPath := "cwd://relative/path"
99+
100+
target := &Target{
101+
Context: &gitURL,
102+
Dockerfile: &dockerImage,
103+
Contexts: map[string]string{
104+
"remote": "https://example.com/context.tar.gz",
105+
"cwd": cwdPath,
106+
"target": "target:other-target",
107+
},
108+
}
109+
110+
// Apply normalization
111+
target.normalize()
112+
113+
// Verify no expansion for special cases
114+
require.Equal(t, gitURL, *target.Context)
115+
require.Equal(t, dockerImage, *target.Dockerfile)
116+
require.Equal(t, "https://example.com/context.tar.gz", target.Contexts["remote"])
117+
require.Equal(t, cwdPath, target.Contexts["cwd"])
118+
require.Equal(t, "target:other-target", target.Contexts["target"])
119+
}

util/pathutil/resolve.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package pathutil
2+
3+
import (
4+
"os"
5+
"os/user"
6+
"path/filepath"
7+
"strings"
8+
)
9+
10+
// ExpandTilde expands tilde in paths (~/ or ~username/)
11+
// Returns original path if expansion fails or path doesn't start with ~
12+
func ExpandTilde(path string) string {
13+
if !strings.HasPrefix(path, "~") {
14+
return path
15+
}
16+
17+
// Handle ~/path or just ~
18+
if path == "~" || strings.HasPrefix(path, "~/") {
19+
home, err := os.UserHomeDir()
20+
if err != nil {
21+
return path
22+
}
23+
if path == "~" {
24+
return home
25+
}
26+
return filepath.Join(home, path[2:])
27+
}
28+
29+
// Handle ~username/path
30+
var username string
31+
var rest string
32+
33+
if idx := strings.Index(path, "/"); idx > 1 {
34+
username = path[1:idx]
35+
rest = path[idx+1:]
36+
} else {
37+
username = path[1:]
38+
}
39+
40+
u, err := user.Lookup(username)
41+
if err != nil {
42+
// If the user doesn't exist, return the unresolved path.
43+
// Matches shell behavior:
44+
// $ echo ~nonexistentuser/path
45+
// ~nonexistentuser/path
46+
return path
47+
}
48+
49+
if rest == "" {
50+
return u.HomeDir
51+
}
52+
return filepath.Join(u.HomeDir, rest)
53+
}
54+
55+
// ExpandTildePaths expands tilde in a slice of paths
56+
func ExpandTildePaths(paths []string) []string {
57+
if paths == nil {
58+
return nil
59+
}
60+
expanded := make([]string, len(paths))
61+
for i, p := range paths {
62+
expanded[i] = ExpandTilde(p)
63+
}
64+
return expanded
65+
}

0 commit comments

Comments
 (0)