diff --git a/bake/bake.go b/bake/bake.go index 3c8d90b7ee65..3dfc459caba9 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -21,6 +21,7 @@ import ( "github.com/docker/buildx/bake/hclparser" "github.com/docker/buildx/build" "github.com/docker/buildx/util/buildflags" + "github.com/docker/buildx/util/pathutil" "github.com/docker/buildx/util/platformutil" "github.com/docker/buildx/util/progress" "github.com/docker/cli/cli/config" @@ -815,7 +816,76 @@ var ( _ hclparser.WithGetName = &Group{} ) +// expandPaths expands tilde in all path fields of the target +func (t *Target) expandPaths() { + // Expand context path + if t.Context != nil { + expanded := pathutil.ExpandTilde(*t.Context) + t.Context = &expanded + } + + // Expand dockerfile path + if t.Dockerfile != nil { + expanded := pathutil.ExpandTilde(*t.Dockerfile) + t.Dockerfile = &expanded + } + + // Expand named contexts + if t.Contexts != nil { + for k, v := range t.Contexts { + t.Contexts[k] = pathutil.ExpandTilde(v) + } + } + + // Expand secret file paths + for _, s := range t.Secrets { + if s.FilePath != "" { + s.FilePath = pathutil.ExpandTilde(s.FilePath) + } + } + + // Expand SSH key paths + for _, s := range t.SSH { + if len(s.Paths) > 0 { + s.Paths = pathutil.ExpandTildePaths(s.Paths) + } + } + + // Expand cache paths if they're local + for _, c := range t.CacheFrom { + if c.Type == "local" && c.Attrs != nil { + if src, ok := c.Attrs["src"]; ok { + c.Attrs["src"] = pathutil.ExpandTilde(src) + } + } + } + for _, c := range t.CacheTo { + if c.Type == "local" && c.Attrs != nil { + if dest, ok := c.Attrs["dest"]; ok { + c.Attrs["dest"] = pathutil.ExpandTilde(dest) + } + } + } + + // Expand output paths + for _, o := range t.Outputs { + // Expand the Destination field + if o.Destination != "" { + o.Destination = pathutil.ExpandTilde(o.Destination) + } + // Also expand dest in Attrs if present + if o.Attrs != nil { + if dest, ok := o.Attrs["dest"]; ok { + o.Attrs["dest"] = pathutil.ExpandTilde(dest) + } + } + } +} + func (t *Target) normalize() { + // Expand tilde in all path fields + t.expandPaths() + t.Annotations = removeDupesStr(t.Annotations) t.Attest = t.Attest.Normalize() t.Tags = removeDupesStr(t.Tags) diff --git a/util/pathutil/resolve.go b/util/pathutil/resolve.go new file mode 100644 index 000000000000..02736240541c --- /dev/null +++ b/util/pathutil/resolve.go @@ -0,0 +1,68 @@ +package pathutil + +import ( + "os" + "os/user" + "path/filepath" + "runtime" + "strings" +) + +// ExpandTilde expands tilde in paths +// - ~ expands to current user's home directory +// - ~username expands to username's home directory (Unix/macOS only) +// Returns original path if expansion fails or path doesn't start with ~ +func ExpandTilde(path string) string { + if !strings.HasPrefix(path, "~") { + return path + } + + // Handle ~/path or just ~ + if path == "~" || strings.HasPrefix(path, "~/") { + home, err := os.UserHomeDir() + if err != nil { + return path + } + if path == "~" { + return home + } + return filepath.Join(home, path[2:]) + } + + // Handle ~username/path (not supported on Windows) + if runtime.GOOS == "windows" { + return path + } + + var username string + var rest string + + if idx := strings.Index(path, "/"); idx > 1 { + username = path[1:idx] + rest = path[idx+1:] + } else { + username = path[1:] + } + + u, err := user.Lookup(username) + if err != nil { + return path + } + + if rest == "" { + return u.HomeDir + } + return filepath.Join(u.HomeDir, rest) +} + +// ExpandTildePaths expands tilde in a slice of paths +func ExpandTildePaths(paths []string) []string { + if paths == nil { + return nil + } + expanded := make([]string, len(paths)) + for i, p := range paths { + expanded[i] = ExpandTilde(p) + } + return expanded +} diff --git a/util/pathutil/resolve_test.go b/util/pathutil/resolve_test.go new file mode 100644 index 000000000000..0b399d275dd8 --- /dev/null +++ b/util/pathutil/resolve_test.go @@ -0,0 +1,148 @@ +package pathutil + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +func expectedRootPath(subpath string) string { + switch runtime.GOOS { + case "windows": + // Windows doesn't support ~username expansion + return "~root/" + subpath + case "darwin": + return filepath.Join("/var/root", subpath) + default: + return filepath.Join("/root", subpath) + } +} + +func TestExpandTilde(t *testing.T) { + // Get current user's home directory for testing + home, err := os.UserHomeDir() + if err != nil { + t.Fatalf("Failed to get home directory: %v", err) + } + + tests := []struct { + name string + input string + expected string + }{ + { + name: "no tilde", + input: "/absolute/path", + expected: "/absolute/path", + }, + { + name: "relative path no tilde", + input: "relative/path", + expected: "relative/path", + }, + { + name: "just tilde", + input: "~", + expected: home, + }, + { + name: "tilde with path", + input: "~/projects/test", + expected: filepath.Join(home, "projects/test"), + }, + { + name: "tilde with dotfile", + input: "~/.npmrc", + expected: filepath.Join(home, ".npmrc"), + }, + { + name: "invalid username", + input: "~nonexistentuser99999/path", + expected: "~nonexistentuser99999/path", // Should return original + }, + { + name: "root user home", + input: "~root/foo", + expected: expectedRootPath("foo"), + }, + { + name: "empty path", + input: "", + expected: "", + }, + { + name: "special prefixes not affected", + input: "docker-image://something", + expected: "docker-image://something", + }, + { + name: "git url not affected", + input: "git@github.com:user/repo.git", + expected: "git@github.com:user/repo.git", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExpandTilde(tt.input) + if result != tt.expected { + t.Errorf("ExpandTilde(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestExpandTildePaths(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil { + t.Fatalf("Failed to get home directory: %v", err) + } + + tests := []struct { + name string + input []string + expected []string + }{ + { + name: "nil input", + input: nil, + expected: nil, + }, + { + name: "empty slice", + input: []string{}, + expected: []string{}, + }, + { + name: "mixed paths", + input: []string{"~/path1", "/absolute/path", "relative/path", "~/.ssh/id_rsa"}, + expected: []string{filepath.Join(home, "path1"), "/absolute/path", "relative/path", filepath.Join(home, ".ssh/id_rsa")}, + }, + { + name: "all tildes", + input: []string{"~/a", "~/b", "~/c"}, + expected: []string{filepath.Join(home, "a"), filepath.Join(home, "b"), filepath.Join(home, "c")}, + }, + { + name: "with invalid usernames", + input: []string{"~/valid", "~invaliduser/path", "~/another"}, + expected: []string{filepath.Join(home, "valid"), "~invaliduser/path", filepath.Join(home, "another")}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExpandTildePaths(tt.input) + if len(result) != len(tt.expected) { + t.Errorf("ExpandTildePaths(%v) returned %d items, want %d", tt.input, len(result), len(tt.expected)) + return + } + for i := range result { + if result[i] != tt.expected[i] { + t.Errorf("ExpandTildePaths[%d] = %q, want %q", i, result[i], tt.expected[i]) + } + } + }) + } +}