Skip to content

Commit 2ffd2a7

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

File tree

3 files changed

+270
-0
lines changed

3 files changed

+270
-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)

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+
}

util/pathutil/resolve_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package pathutil
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestExpandTilde(t *testing.T) {
10+
// Get current user's home directory for testing
11+
home, err := os.UserHomeDir()
12+
if err != nil {
13+
t.Fatalf("Failed to get home directory: %v", err)
14+
}
15+
16+
tests := []struct {
17+
name string
18+
input string
19+
expected string
20+
}{
21+
{
22+
name: "no tilde",
23+
input: "/absolute/path",
24+
expected: "/absolute/path",
25+
},
26+
{
27+
name: "relative path no tilde",
28+
input: "relative/path",
29+
expected: "relative/path",
30+
},
31+
{
32+
name: "just tilde",
33+
input: "~",
34+
expected: home,
35+
},
36+
{
37+
name: "tilde with path",
38+
input: "~/projects/test",
39+
expected: filepath.Join(home, "projects/test"),
40+
},
41+
{
42+
name: "tilde with dotfile",
43+
input: "~/.npmrc",
44+
expected: filepath.Join(home, ".npmrc"),
45+
},
46+
{
47+
name: "invalid username",
48+
input: "~nonexistentuser99999/path",
49+
expected: "~nonexistentuser99999/path", // Should return original
50+
},
51+
{
52+
name: "invalid username no slash",
53+
input: "~nonexistentuser99999",
54+
expected: "~nonexistentuser99999", // Should return original
55+
},
56+
{
57+
name: "empty path",
58+
input: "",
59+
expected: "",
60+
},
61+
{
62+
name: "special prefixes not affected",
63+
input: "docker-image://something",
64+
expected: "docker-image://something",
65+
},
66+
{
67+
name: "git url not affected",
68+
input: "[email protected]:user/repo.git",
69+
expected: "[email protected]:user/repo.git",
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
result := ExpandTilde(tt.input)
76+
if result != tt.expected {
77+
t.Errorf("ExpandTilde(%q) = %q, want %q", tt.input, result, tt.expected)
78+
}
79+
})
80+
}
81+
}
82+
83+
func TestExpandTildePaths(t *testing.T) {
84+
home, err := os.UserHomeDir()
85+
if err != nil {
86+
t.Fatalf("Failed to get home directory: %v", err)
87+
}
88+
89+
tests := []struct {
90+
name string
91+
input []string
92+
expected []string
93+
}{
94+
{
95+
name: "nil input",
96+
input: nil,
97+
expected: nil,
98+
},
99+
{
100+
name: "empty slice",
101+
input: []string{},
102+
expected: []string{},
103+
},
104+
{
105+
name: "mixed paths",
106+
input: []string{"~/path1", "/absolute/path", "relative/path", "~/.ssh/id_rsa"},
107+
expected: []string{filepath.Join(home, "path1"), "/absolute/path", "relative/path", filepath.Join(home, ".ssh/id_rsa")},
108+
},
109+
{
110+
name: "all tildes",
111+
input: []string{"~/a", "~/b", "~/c"},
112+
expected: []string{filepath.Join(home, "a"), filepath.Join(home, "b"), filepath.Join(home, "c")},
113+
},
114+
{
115+
name: "with invalid usernames",
116+
input: []string{"~/valid", "~invaliduser/path", "~/another"},
117+
expected: []string{filepath.Join(home, "valid"), "~invaliduser/path", filepath.Join(home, "another")},
118+
},
119+
}
120+
121+
for _, tt := range tests {
122+
t.Run(tt.name, func(t *testing.T) {
123+
result := ExpandTildePaths(tt.input)
124+
if len(result) != len(tt.expected) {
125+
t.Errorf("ExpandTildePaths(%v) returned %d items, want %d", tt.input, len(result), len(tt.expected))
126+
return
127+
}
128+
for i := range result {
129+
if result[i] != tt.expected[i] {
130+
t.Errorf("ExpandTildePaths[%d] = %q, want %q", i, result[i], tt.expected[i])
131+
}
132+
}
133+
})
134+
}
135+
}

0 commit comments

Comments
 (0)