Skip to content
Merged
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
8 changes: 7 additions & 1 deletion packages/cli/internal/modules/dependencies/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,13 @@ func (s Service) prepareGo(ctx context.Context, in Input, p workspace.ManifestPr
if active != "" && active != "off" {
want := filepath.Join(in.Root, "go.work")
if filepath.Clean(active) != filepath.Clean(want) {
return fmt.Errorf("%s uses external GOWORK=%s; use %s or GOWORK=off explicitly", p.Name, active, want)
// Go may return the physical path while the workspace root uses
// a symlink (for example /var and /private/var on macOS).
activeInfo, activeErr := os.Stat(active)
wantInfo, wantErr := os.Stat(want)
if activeErr != nil || wantErr != nil || !os.SameFile(activeInfo, wantInfo) {
return fmt.Errorf("%s uses external GOWORK=%s; use %s or GOWORK=off explicitly", p.Name, active, want)
}
}
lockRoot = in.Root
}
Expand Down
52 changes: 52 additions & 0 deletions packages/cli/internal/modules/dependencies/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -136,6 +137,57 @@ func TestGoUnresolvedImportFailsWithoutTidy(t *testing.T) {
}
}

func TestGoWorkspaceAcceptsSymlinkPaths(t *testing.T) {
for _, mode := range []string{"workspace-root", "gowork"} {
t.Run(mode, func(t *testing.T) {
root := setupGo(t)
write(t, root, "go.work", "go 1.25.0\nuse ./api\n")
write(t, root, "api/go.mod", "module example.com/api\ngo 1.25.0\n")
write(t, root, "api/main.go", "package main\nfunc main(){}\n")
physicalRoot, err := filepath.EvalSymlinks(root)
if err != nil {
t.Fatal(err)
}
linkTarget := filepath.Join(physicalRoot, "go.work")
alias := filepath.Join(physicalRoot, "alias.work")
if mode == "workspace-root" {
linkTarget = physicalRoot
alias = filepath.Join(t.TempDir(), "workspace-link")
}
if err := os.Symlink(linkTarget, alias); err != nil {
if runtime.GOOS == "windows" {
t.Skipf("symlinks unavailable: %v", err)
}
t.Fatal(err)
}
active := alias
if mode == "workspace-root" {
root, active = alias, filepath.Join(physicalRoot, "go.work")
}
t.Setenv("GOWORK", active)
in := Input{Root: root, Manifest: &workspace.Manifest{Projects: []workspace.ManifestProject{project("api", "api", "go")}}}
if err := (Service{}).Prepare(context.Background(), in); err != nil {
t.Fatal(err)
}
})
}
}

func TestGoWorkspaceRejectsExternalFileWithSameContents(t *testing.T) {
root := setupGo(t)
work := "go 1.25.0\n"
write(t, root, "go.work", work)
write(t, root, "api/go.mod", "module example.com/api\ngo 1.25.0\n")
external := t.TempDir()
write(t, external, "go.work", work)
active := filepath.Join(external, "go.work")
t.Setenv("GOWORK", active)
in := Input{Root: root, Manifest: &workspace.Manifest{Projects: []workspace.ManifestProject{project("api", "api", "go")}}}
if err := (Service{}).Prepare(context.Background(), in); err == nil || !strings.Contains(err.Error(), "uses external GOWORK="+active) {
t.Fatalf("expected external workspace rejection, got %v", err)
}
}

type fakeProvider struct{ dir string }

func (p fakeProvider) Prepare(_ context.Context, c runtimeport.Command) (runtimeport.Command, error) {
Expand Down
18 changes: 17 additions & 1 deletion packages/cli/internal/transport/cobra/create/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package createcmd
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"

Expand Down Expand Up @@ -76,7 +77,7 @@ func compactHomePath(path string) string {
}

func relativeOrAbs(cwd, targetDir string, useCurrentDir bool) string {
rel, err := filepath.Rel(cwd, targetDir)
rel, err := filepath.Rel(resolveDisplayPath(cwd), resolveDisplayPath(targetDir))
if err == nil && rel != "" {
return rel
}
Expand All @@ -86,6 +87,21 @@ func relativeOrAbs(cwd, targetDir string, useCurrentDir bool) string {
return targetDir
}

// resolveDisplayPath resolves symlinks in existing ancestors, even when the
// workspace directory (and some of its parents) has not been created yet.
// Other filesystem errors leave the original path available for display.
func resolveDisplayPath(path string) string {
resolved, err := filepath.EvalSymlinks(path)
if err == nil {
return resolved
}
parent := filepath.Dir(path)
if !os.IsNotExist(err) || parent == path {
return path
}
return filepath.Join(resolveDisplayPath(parent), filepath.Base(path))
}

func parsePresetProjectNames(raw string, want int) ([]string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
Expand Down
44 changes: 44 additions & 0 deletions packages/cli/internal/transport/cobra/create/result_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package createcmd

import (
"os"
"path/filepath"
"runtime"
"testing"
)

func TestRelativeOrAbsWithSymlinkPaths(t *testing.T) {
root, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
alias := filepath.Join(t.TempDir(), "parent-link")
if err := os.Symlink(root, alias); err != nil {
if runtime.GOOS == "windows" {
t.Skipf("directory symlinks unavailable: %v", err)
}
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(root, "existing"), 0o755); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name, cwd, target, want string
inPlace bool
}{
{name: "plain", cwd: root, target: filepath.Join(root, "demo"), want: "demo"},
{name: "target-alias", cwd: root, target: filepath.Join(alias, "demo"), want: "demo"},
{name: "cwd-alias", cwd: alias, target: filepath.Join(root, "demo"), want: "demo"},
{name: "both-aliases", cwd: alias, target: filepath.Join(alias, "demo"), want: "demo"},
{name: "missing-parents", cwd: root, target: filepath.Join(alias, "new", "nested", "demo"), want: filepath.Join("new", "nested", "demo")},
{name: "existing-target", cwd: root, target: filepath.Join(alias, "existing"), want: "existing"},
{name: "in-place", cwd: root, target: alias, inPlace: true, want: "."},
{name: "sibling", cwd: filepath.Join(root, "existing"), target: filepath.Join(alias, "demo"), want: filepath.Join("..", "demo")},
} {
t.Run(tc.name, func(t *testing.T) {
if got := relativeOrAbs(tc.cwd, tc.target, tc.inPlace); got != tc.want {
t.Errorf("relativeOrAbs(%q, %q, %v) = %q; want %q", tc.cwd, tc.target, tc.inPlace, got, tc.want)
}
})
}
}
Loading