Skip to content

Commit b8c7fdd

Browse files
committed
cmd/go: use os.Executable to find GOROOT
Before this change, building a GOROOT using make.bash, and then moving the entire to a new path confused the go tool. Correct operation of the go tool under these conditions required either running make.bash again (not always possible if the new location was owned by a different system user) or setting the GOROOT environment variable. Setting GOROOT is unfortunate and discouraged, as it makes it too easy to use the go tool from one GOROOT and the compiler from another GOROOT. With this change, the go tool finds its GOROOT relative to its own location, using os.Executable. It checks it is in a GOROOT by searching for the GOROOT/pkg/tool directory, to avoid two plausible situations: ln -s $GOROOT/bin/go /usr/local/bin/go and PATH=$HOME/bin:$PATH GOPATH=$HOME ln -s $GOROOT/bin/go $HOME/bin/go Additionally, if the current executable path is not in a GOROOT, the tool will follow any symlinks for the executable and check to see if its original path is a GOROOT. Fixes #18678 Change-Id: I151d7d449d213164f98193cc176b616849e6332c Reviewed-on: https://go-review.googlesource.com/42533 Run-TryBot: David Crawshaw <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent ec0ee7d commit b8c7fdd

File tree

2 files changed

+158
-7
lines changed

2 files changed

+158
-7
lines changed

src/cmd/go/go_test.go

+122-6
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ func init() {
7373
}
7474
}
7575

76+
// testGOROOT is the GOROOT to use when running testgo, a cmd/go binary
77+
// build from this process's current GOROOT, but run from a different
78+
// (temp) directory.
79+
var testGOROOT string
80+
7681
// The TestMain function creates a go command for testing purposes and
7782
// deletes it after the tests have been run.
7883
func TestMain(m *testing.M) {
@@ -87,6 +92,13 @@ func TestMain(m *testing.M) {
8792
os.Exit(2)
8893
}
8994

95+
out, err = exec.Command("go", "env", "GOROOT").CombinedOutput()
96+
if err != nil {
97+
fmt.Fprintf(os.Stderr, "could not find testing GOROOT: %v\n%s", err, out)
98+
os.Exit(2)
99+
}
100+
testGOROOT = strings.TrimSpace(string(out))
101+
90102
if out, err := exec.Command("./testgo"+exeSuffix, "env", "CGO_ENABLED").Output(); err != nil {
91103
fmt.Fprintf(os.Stderr, "running testgo failed: %v\n", err)
92104
canRun = false
@@ -253,6 +265,13 @@ func (tg *testgoData) unsetenv(name string) {
253265
}
254266
}
255267

268+
func (tg *testgoData) goTool() string {
269+
if tg.wd == "" {
270+
return "./testgo" + exeSuffix
271+
}
272+
return filepath.Join(tg.wd, "testgo"+exeSuffix)
273+
}
274+
256275
// doRun runs the test go command, recording stdout and stderr and
257276
// returning exit status.
258277
func (tg *testgoData) doRun(args []string) error {
@@ -266,13 +285,20 @@ func (tg *testgoData) doRun(args []string) error {
266285
}
267286
}
268287
}
269-
tg.t.Logf("running testgo %v", args)
270-
var prog string
271-
if tg.wd == "" {
272-
prog = "./testgo" + exeSuffix
273-
} else {
274-
prog = filepath.Join(tg.wd, "testgo"+exeSuffix)
288+
289+
hasGoroot := false
290+
for _, v := range tg.env {
291+
if strings.HasPrefix(v, "GOROOT=") {
292+
hasGoroot = true
293+
break
294+
}
295+
}
296+
prog := tg.goTool()
297+
if !hasGoroot {
298+
tg.setenv("GOROOT", testGOROOT)
275299
}
300+
301+
tg.t.Logf("running testgo %v", args)
276302
cmd := exec.Command(prog, args...)
277303
tg.stdout.Reset()
278304
tg.stderr.Reset()
@@ -3897,3 +3923,93 @@ func TestBuildTagsNoComma(t *testing.T) {
38973923
tg.runFail("build", "-tags", "tag1,tag2", "math")
38983924
tg.grepBoth("space-separated list contains comma", "-tags with a comma-separated list didn't error")
38993925
}
3926+
3927+
func copyFile(src, dst string, perm os.FileMode) error {
3928+
sf, err := os.Open(src)
3929+
if err != nil {
3930+
return err
3931+
}
3932+
defer sf.Close()
3933+
3934+
df, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
3935+
if err != nil {
3936+
return err
3937+
}
3938+
3939+
_, err = io.Copy(df, sf)
3940+
err2 := df.Close()
3941+
if err != nil {
3942+
return err
3943+
}
3944+
return err2
3945+
}
3946+
3947+
func TestExecutableGOROOT(t *testing.T) {
3948+
if runtime.GOOS == "openbsd" {
3949+
t.Skipf("test case does not work on %s, missing os.Executable", runtime.GOOS)
3950+
}
3951+
3952+
tg := testgo(t)
3953+
defer tg.cleanup()
3954+
3955+
tg.makeTempdir()
3956+
tg.tempDir("newgoroot/bin")
3957+
newGoTool := tg.path("newgoroot/bin/go" + exeSuffix)
3958+
err := copyFile(tg.goTool(), newGoTool, 0775)
3959+
if err != nil {
3960+
t.Fatalf("error copying go tool %v", err)
3961+
}
3962+
3963+
goroot := func(goTool string) string {
3964+
cmd := exec.Command(goTool, "env", "GOROOT")
3965+
cmd.Env = os.Environ()
3966+
for i, val := range cmd.Env {
3967+
if strings.HasPrefix(val, "GOROOT=") {
3968+
cmd.Env = append(cmd.Env[:i], cmd.Env[i+1:]...)
3969+
break
3970+
}
3971+
}
3972+
out, err := cmd.CombinedOutput()
3973+
if err != nil {
3974+
t.Fatalf("copied go tool failed %v: %s", err, out)
3975+
return ""
3976+
}
3977+
return strings.TrimSpace(string(out))
3978+
}
3979+
3980+
// macOS uses a symlink for /tmp.
3981+
resolvedTestGOROOT, err := filepath.EvalSymlinks(testGOROOT)
3982+
if err != nil {
3983+
t.Fatalf("could not eval testgoroot symlinks: %v", err)
3984+
}
3985+
3986+
// Missing GOROOT/pkg/tool, the go tool should fall back to
3987+
// its default path.
3988+
if got, want := goroot(newGoTool), resolvedTestGOROOT; got != want {
3989+
t.Fatalf("%s env GOROOT = %q, want %q", newGoTool, got, want)
3990+
}
3991+
3992+
// Now the executable's path looks like a GOROOT.
3993+
tg.tempDir("newgoroot/pkg/tool")
3994+
if got, want := goroot(newGoTool), tg.path("newgoroot"); got != want {
3995+
t.Fatalf("%s env GOROOT = %q with pkg/tool, want %q", newGoTool, got, want)
3996+
}
3997+
3998+
switch runtime.GOOS {
3999+
case "plan9", "windows":
4000+
t.Skipf("skipping symlink test on %s", runtime.GOOS)
4001+
}
4002+
4003+
tg.tempDir("notgoroot/bin")
4004+
symGoTool := tg.path("notgoroot/bin/go" + exeSuffix)
4005+
tg.must(os.Symlink(newGoTool, symGoTool))
4006+
4007+
resolvedNewGOROOT, err := filepath.EvalSymlinks(tg.path("newgoroot"))
4008+
if err != nil {
4009+
t.Fatalf("could not eval newgoroot symlinks: %v", err)
4010+
}
4011+
4012+
if got, want := goroot(symGoTool), resolvedNewGOROOT; got != want {
4013+
t.Fatalf("%s env GOROOT = %q, want %q", symGoTool, got, want)
4014+
}
4015+
}

src/cmd/go/internal/cfg/cfg.go

+36-1
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,44 @@ var (
6464
)
6565

6666
var (
67-
GOROOT = filepath.Clean(runtime.GOROOT())
67+
GOROOT = findGOROOT()
6868
GOBIN = os.Getenv("GOBIN")
6969
GOROOTbin = filepath.Join(GOROOT, "bin")
7070
GOROOTpkg = filepath.Join(GOROOT, "pkg")
7171
GOROOTsrc = filepath.Join(GOROOT, "src")
7272
)
73+
74+
func findGOROOT() string {
75+
if env := os.Getenv("GOROOT"); env != "" {
76+
return filepath.Clean(env)
77+
}
78+
exe, err := os.Executable()
79+
if err == nil {
80+
exe, err = filepath.Abs(exe)
81+
if err == nil {
82+
if dir := filepath.Join(exe, "../.."); isGOROOT(dir) {
83+
return dir
84+
}
85+
exe, err = filepath.EvalSymlinks(exe)
86+
if err == nil {
87+
if dir := filepath.Join(exe, "../.."); isGOROOT(dir) {
88+
return dir
89+
}
90+
}
91+
}
92+
}
93+
return filepath.Clean(runtime.GOROOT())
94+
}
95+
96+
// isGOROOT reports whether path looks like a GOROOT.
97+
//
98+
// It does this by looking for the path/pkg/tool directory,
99+
// which is necessary for useful operation of the cmd/go tool,
100+
// and is not typically present in a GOPATH.
101+
func isGOROOT(path string) bool {
102+
stat, err := os.Stat(filepath.Join(path, "pkg", "tool"))
103+
if err != nil {
104+
return false
105+
}
106+
return stat.IsDir()
107+
}

0 commit comments

Comments
 (0)