Commit b8c7fddd authored by David Crawshaw's avatar David Crawshaw

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 <crawshaw@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarIan Lance Taylor <iant@golang.org>
parent ec0ee7d3
......@@ -73,6 +73,11 @@ func init() {
}
}
// testGOROOT is the GOROOT to use when running testgo, a cmd/go binary
// build from this process's current GOROOT, but run from a different
// (temp) directory.
var testGOROOT string
// The TestMain function creates a go command for testing purposes and
// deletes it after the tests have been run.
func TestMain(m *testing.M) {
......@@ -87,6 +92,13 @@ func TestMain(m *testing.M) {
os.Exit(2)
}
out, err = exec.Command("go", "env", "GOROOT").CombinedOutput()
if err != nil {
fmt.Fprintf(os.Stderr, "could not find testing GOROOT: %v\n%s", err, out)
os.Exit(2)
}
testGOROOT = strings.TrimSpace(string(out))
if out, err := exec.Command("./testgo"+exeSuffix, "env", "CGO_ENABLED").Output(); err != nil {
fmt.Fprintf(os.Stderr, "running testgo failed: %v\n", err)
canRun = false
......@@ -253,6 +265,13 @@ func (tg *testgoData) unsetenv(name string) {
}
}
func (tg *testgoData) goTool() string {
if tg.wd == "" {
return "./testgo" + exeSuffix
}
return filepath.Join(tg.wd, "testgo"+exeSuffix)
}
// doRun runs the test go command, recording stdout and stderr and
// returning exit status.
func (tg *testgoData) doRun(args []string) error {
......@@ -266,13 +285,20 @@ func (tg *testgoData) doRun(args []string) error {
}
}
}
tg.t.Logf("running testgo %v", args)
var prog string
if tg.wd == "" {
prog = "./testgo" + exeSuffix
} else {
prog = filepath.Join(tg.wd, "testgo"+exeSuffix)
hasGoroot := false
for _, v := range tg.env {
if strings.HasPrefix(v, "GOROOT=") {
hasGoroot = true
break
}
}
prog := tg.goTool()
if !hasGoroot {
tg.setenv("GOROOT", testGOROOT)
}
tg.t.Logf("running testgo %v", args)
cmd := exec.Command(prog, args...)
tg.stdout.Reset()
tg.stderr.Reset()
......@@ -3897,3 +3923,93 @@ func TestBuildTagsNoComma(t *testing.T) {
tg.runFail("build", "-tags", "tag1,tag2", "math")
tg.grepBoth("space-separated list contains comma", "-tags with a comma-separated list didn't error")
}
func copyFile(src, dst string, perm os.FileMode) error {
sf, err := os.Open(src)
if err != nil {
return err
}
defer sf.Close()
df, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
_, err = io.Copy(df, sf)
err2 := df.Close()
if err != nil {
return err
}
return err2
}
func TestExecutableGOROOT(t *testing.T) {
if runtime.GOOS == "openbsd" {
t.Skipf("test case does not work on %s, missing os.Executable", runtime.GOOS)
}
tg := testgo(t)
defer tg.cleanup()
tg.makeTempdir()
tg.tempDir("newgoroot/bin")
newGoTool := tg.path("newgoroot/bin/go" + exeSuffix)
err := copyFile(tg.goTool(), newGoTool, 0775)
if err != nil {
t.Fatalf("error copying go tool %v", err)
}
goroot := func(goTool string) string {
cmd := exec.Command(goTool, "env", "GOROOT")
cmd.Env = os.Environ()
for i, val := range cmd.Env {
if strings.HasPrefix(val, "GOROOT=") {
cmd.Env = append(cmd.Env[:i], cmd.Env[i+1:]...)
break
}
}
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("copied go tool failed %v: %s", err, out)
return ""
}
return strings.TrimSpace(string(out))
}
// macOS uses a symlink for /tmp.
resolvedTestGOROOT, err := filepath.EvalSymlinks(testGOROOT)
if err != nil {
t.Fatalf("could not eval testgoroot symlinks: %v", err)
}
// Missing GOROOT/pkg/tool, the go tool should fall back to
// its default path.
if got, want := goroot(newGoTool), resolvedTestGOROOT; got != want {
t.Fatalf("%s env GOROOT = %q, want %q", newGoTool, got, want)
}
// Now the executable's path looks like a GOROOT.
tg.tempDir("newgoroot/pkg/tool")
if got, want := goroot(newGoTool), tg.path("newgoroot"); got != want {
t.Fatalf("%s env GOROOT = %q with pkg/tool, want %q", newGoTool, got, want)
}
switch runtime.GOOS {
case "plan9", "windows":
t.Skipf("skipping symlink test on %s", runtime.GOOS)
}
tg.tempDir("notgoroot/bin")
symGoTool := tg.path("notgoroot/bin/go" + exeSuffix)
tg.must(os.Symlink(newGoTool, symGoTool))
resolvedNewGOROOT, err := filepath.EvalSymlinks(tg.path("newgoroot"))
if err != nil {
t.Fatalf("could not eval newgoroot symlinks: %v", err)
}
if got, want := goroot(symGoTool), resolvedNewGOROOT; got != want {
t.Fatalf("%s env GOROOT = %q, want %q", symGoTool, got, want)
}
}
......@@ -64,9 +64,44 @@ var (
)
var (
GOROOT = filepath.Clean(runtime.GOROOT())
GOROOT = findGOROOT()
GOBIN = os.Getenv("GOBIN")
GOROOTbin = filepath.Join(GOROOT, "bin")
GOROOTpkg = filepath.Join(GOROOT, "pkg")
GOROOTsrc = filepath.Join(GOROOT, "src")
)
func findGOROOT() string {
if env := os.Getenv("GOROOT"); env != "" {
return filepath.Clean(env)
}
exe, err := os.Executable()
if err == nil {
exe, err = filepath.Abs(exe)
if err == nil {
if dir := filepath.Join(exe, "../.."); isGOROOT(dir) {
return dir
}
exe, err = filepath.EvalSymlinks(exe)
if err == nil {
if dir := filepath.Join(exe, "../.."); isGOROOT(dir) {
return dir
}
}
}
}
return filepath.Clean(runtime.GOROOT())
}
// isGOROOT reports whether path looks like a GOROOT.
//
// It does this by looking for the path/pkg/tool directory,
// which is necessary for useful operation of the cmd/go tool,
// and is not typically present in a GOPATH.
func isGOROOT(path string) bool {
stat, err := os.Stat(filepath.Join(path, "pkg", "tool"))
if err != nil {
return false
}
return stat.IsDir()
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment