Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions bake/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
68 changes: 68 additions & 0 deletions util/pathutil/resolve.go
Original file line number Diff line number Diff line change
@@ -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
}
148 changes: 148 additions & 0 deletions util/pathutil/resolve_test.go
Original file line number Diff line number Diff line change
@@ -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: "[email protected]:user/repo.git",
expected: "[email protected]: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])
}
}
})
}
}