diff --git a/src/cmd/go/gotoolchain.go b/src/cmd/go/gotoolchain.go index ef1b531313..b66561cadc 100644 --- a/src/cmd/go/gotoolchain.go +++ b/src/cmd/go/gotoolchain.go @@ -7,10 +7,12 @@ package main import ( + "bytes" "cmd/go/internal/base" "cmd/go/internal/cfg" "cmd/go/internal/modcmd" "cmd/go/internal/modload" + "cmd/go/internal/work" "context" "fmt" "internal/godebug" @@ -61,19 +63,36 @@ func switchGoToolchain() { gotoolchain := cfg.Getenv("GOTOOLCHAIN") if gotoolchain == "" { - if strings.HasPrefix(runtime.Version(), "go") { - gotoolchain = "local" // TODO: set to "auto" once auto is implemented below - } else { - gotoolchain = "local" - } - } - env := gotoolchain - if gotoolchain == "auto" || gotoolchain == "path" { - // TODO: Locate and read go.mod or go.work. - base.Fatalf("GOTOOLCHAIN=auto not yet implemented") + gotoolchain = "auto" } - if gotoolchain == "local" || gotoolchain == runtime.Version() { + gotoolchain, min, haveMin := strings.Cut(gotoolchain, "+") + if haveMin { + if gotoolchain != "auto" && gotoolchain != "path" { + base.Fatalf("invalid GOTOOLCHAIN %q: only auto and path can use +version", gotoolchain) + } + if !strings.HasPrefix(min, "go1") { + base.Fatalf("invalid GOTOOLCHAIN %q: invalid minimum version %q", gotoolchain, min) + } + } else { + min = work.RuntimeVersion + } + + pathOnly := gotoolchain == "path" + if gotoolchain == "auto" || gotoolchain == "path" { + // Locate and read go.mod or go.work. + goVers, toolchain := modGoToolchain() + if toolchain != "" { + // toolchain line wins by itself + gotoolchain = toolchain + } else if goVers != "" { + gotoolchain = toolchainMax(min, "go"+goVers) + } else { + gotoolchain = min + } + } + + if gotoolchain == "local" || gotoolchain == work.RuntimeVersion { // Let the current binary handle the command. return } @@ -95,7 +114,7 @@ func switchGoToolchain() { // GOTOOLCHAIN=auto looks in PATH and then falls back to download. // GOTOOLCHAIN=path only looks in PATH. - if env == "path" { + if pathOnly { base.Fatalf("cannot find %q in PATH", gotoolchain) } @@ -208,3 +227,39 @@ func execGoToolchain(gotoolchain, dir, exe string) { err := syscall.Exec(exe, os.Args, os.Environ()) base.Fatalf("exec %s: %v", gotoolchain, err) } + +// modGoToolchain finds the enclosing go.work or go.mod file +// and returns the go version and toolchain lines from the file. +// The toolchain line overrides the version line +func modGoToolchain() (goVers, toolchain string) { + wd := base.UncachedCwd() + file := modload.FindGoWork(wd) + // $GOWORK can be set to a file that does not yet exist, if we are running 'go work init'. + // Do not try to load the file in that case + if _, err := os.Stat(file); err != nil { + file = "" + } + if file == "" { + file = modload.FindGoMod(wd) + } + if file == "" { + return "", "" + } + + data, err := os.ReadFile(file) + if err != nil { + base.Fatalf("%v", err) + } + for len(data) > 0 { + var line []byte + line, data, _ = bytes.Cut(data, nl) + line = bytes.TrimSpace(line) + if goVers == "" { + goVers = parseKey(line, goKey) + } + if toolchain == "" { + toolchain = parseKey(line, toolchainKey) + } + } + return +} diff --git a/src/cmd/go/gotoolchain_port.go b/src/cmd/go/gotoolchain_port.go new file mode 100644 index 0000000000..a530059a99 --- /dev/null +++ b/src/cmd/go/gotoolchain_port.go @@ -0,0 +1,117 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bytes" + "strings" +) + +var ( + nl = []byte("\n") + comment = []byte("//") + goKey = []byte("go") + toolchainKey = []byte("toolchain") +) + +// parseKey checks whether line begings with key ("go" or "toolchain"). +// If so, it returns the remainder of the line (the argument). +func parseKey(line, key []byte) string { + if !bytes.HasPrefix(line, key) { + return "" + } + line = bytes.TrimPrefix(line, key) + if len(line) == 0 || (line[0] != ' ' && line[0] != '\t') { + return "" + } + line, _, _ = bytes.Cut(line, comment) // strip comments + return string(bytes.TrimSpace(line)) +} + +// toolchainMax returns the max of x and y as toolchain names +// like go1.19.4, comparing the versions. +func toolchainMax(x, y string) string { + if toolchainCmp(x, y) >= 0 { + return x + } + return y +} + +// toolchainCmp returns -1, 0, or +1 depending on whether +// x < y, x == y, or x > y, interpreted as toolchain versions. +func toolchainCmp(x, y string) int { + if x == y { + return 0 + } + if y == "" { + return +1 + } + if x == "" { + return -1 + } + if !strings.HasPrefix(x, "go1") && !strings.HasPrefix(y, "go1") { + return 0 + } + if !strings.HasPrefix(x, "go1") { + return +1 + } + if !strings.HasPrefix(y, "go1") { + return -1 + } + x = strings.TrimPrefix(x, "go") + y = strings.TrimPrefix(y, "go") + for x != "" || y != "" { + if x == y { + return 0 + } + xN, xRest := versionCut(x) + yN, yRest := versionCut(y) + if xN > yN { + return +1 + } + if xN < yN { + return -1 + } + x = xRest + y = yRest + } + return 0 +} + +// versionCut cuts the version x after the next dot or before the next non-digit, +// returning the leading decimal found and the remainder of the string. +func versionCut(x string) (int, string) { + // Treat empty string as infinite source of .0.0.0... + if x == "" { + return 0, "" + } + i := 0 + v := 0 + for i < len(x) && '0' <= x[i] && x[i] <= '9' { + v = v*10 + int(x[i]-'0') + i++ + } + // Treat non-empty non-number as -1 (for release candidates, etc), + // but stop at next number. + if i == 0 { + for i < len(x) && (x[i] < '0' || '9' < x[i]) { + i++ + } + if i < len(x) && x[i] == '.' { + i++ + } + if strings.Contains(x[:i], "alpha") { + return -3, x[i:] + } + if strings.Contains(x[:i], "beta") { + return -2, x[i:] + } + return -1, x[i:] + } + if i < len(x) && x[i] == '.' { + i++ + } + return v, x[i:] +} diff --git a/src/cmd/go/gotoolchain_test.go b/src/cmd/go/gotoolchain_test.go new file mode 100644 index 0000000000..22c3958f28 --- /dev/null +++ b/src/cmd/go/gotoolchain_test.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import "testing" + +var toolchainCmpTests = []struct { + x string + y string + out int +}{ + {"", "", 0}, + {"x", "x", 0}, + {"", "x", -1}, + {"go1.5", "go1.6", -1}, + {"go1.5", "go1.10", -1}, + {"go1.6", "go1.6.1", -1}, + {"go1.999", "devel go1.4", -1}, + {"devel go1.5", "devel go1.6", 0}, // devels are all +infinity + {"go1.19", "go1.19.1", -1}, + {"go1.19rc1", "go1.19", -1}, + {"go1.19rc1", "go1.19.1", -1}, + {"go1.19rc1", "go1.19rc2", -1}, + {"go1.19.0", "go1.19.1", -1}, + {"go1.19rc1", "go1.19.0", -1}, + {"go1.19alpha3", "go1.19beta2", -1}, + {"go1.19beta2", "go1.19rc1", -1}, + + // Syntax we don't ever plan to use, but just in case we do. + {"go1.19.0-rc.1", "go1.19.0-rc.2", -1}, + {"go1.19.0-rc.1", "go1.19.0", -1}, + {"go1.19.0-alpha.3", "go1.19.0-beta.2", -1}, + {"go1.19.0-beta.2", "go1.19.0-rc.1", -1}, +} + +func TestToolchainCmp(t *testing.T) { + for _, tt := range toolchainCmpTests { + out := toolchainCmp(tt.x, tt.y) + if out != tt.out { + t.Errorf("toolchainCmp(%q, %q) = %d, want %d", tt.x, tt.y, out, tt.out) + } + out = toolchainCmp(tt.y, tt.x) + if out != -tt.out { + t.Errorf("toolchainCmp(%q, %q) = %d, want %d", tt.y, tt.x, out, -tt.out) + } + } +} diff --git a/src/cmd/go/internal/base/path.go b/src/cmd/go/internal/base/path.go index ebe4f153ed..64f213b408 100644 --- a/src/cmd/go/internal/base/path.go +++ b/src/cmd/go/internal/base/path.go @@ -15,14 +15,22 @@ import ( var cwd string var cwdOnce sync.Once +// UncachedCwd returns the current working directory. +// Most callers should use Cwd, which caches the result for future use. +// UncachedCwd is appropriate to call early in program startup before flag parsing, +// because the -C flag may change the current directory. +func UncachedCwd() string { + wd, err := os.Getwd() + if err != nil { + Fatalf("cannot determine current directory: %v", err) + } + return wd +} + // Cwd returns the current working directory at the time of the first call. func Cwd() string { cwdOnce.Do(func() { - var err error - cwd, err = os.Getwd() - if err != nil { - Fatalf("cannot determine current directory: %v", err) - } + cwd = UncachedCwd() }) return cwd } diff --git a/src/cmd/go/internal/modload/init.go b/src/cmd/go/internal/modload/init.go index 31c66a6fde..661a379d82 100644 --- a/src/cmd/go/internal/modload/init.go +++ b/src/cmd/go/internal/modload/init.go @@ -292,21 +292,29 @@ func BinDir() string { // operate in workspace mode. It should not be called by other commands, // for example 'go mod tidy', that don't operate in workspace mode. func InitWorkfile() { + workFilePath = FindGoWork(base.Cwd()) +} + +// FindGoWork returns the name of the go.work file for this command, +// or the empty string if there isn't one. +// Most code should use Init and Enabled rather than use this directly. +// It is exported mainly for Go toolchain switching, which must process +// the go.work very early at startup. +func FindGoWork(wd string) string { if RootMode == NoRoot { - workFilePath = "" - return + return "" } switch gowork := cfg.Getenv("GOWORK"); gowork { case "off": - workFilePath = "" + return "" case "", "auto": - workFilePath = findWorkspaceFile(base.Cwd()) + return findWorkspaceFile(wd) default: if !filepath.IsAbs(gowork) { - base.Fatalf("the path provided to GOWORK must be an absolute path") + base.Fatalf("go: invalid GOWORK: not an absolute path") } - workFilePath = gowork + return gowork } } @@ -467,19 +475,30 @@ func WillBeEnabled() bool { return false } - if modRoot := findModuleRoot(base.Cwd()); modRoot == "" { + return FindGoMod(base.Cwd()) != "" +} + +// FindGoMod returns the name of the go.mod file for this command, +// or the empty string if there isn't one. +// Most code should use Init and Enabled rather than use this directly. +// It is exported mainly for Go toolchain switching, which must process +// the go.mod very early at startup. +func FindGoMod(wd string) string { + modRoot := findModuleRoot(wd) + if modRoot == "" { // GO111MODULE is 'auto', and we can't find a module root. // Stay in GOPATH mode. - return false - } else if search.InDir(modRoot, os.TempDir()) == "." { + return "" + } + if search.InDir(modRoot, os.TempDir()) == "." { // If you create /tmp/go.mod for experimenting, // then any tests that create work directories under /tmp // will find it and get modules when they're not expecting them. // It's a bit of a peculiar thing to disallow but quite mysterious // when it happens. See golang.org/issue/26708. - return false + return "" } - return true + return filepath.Join(modRoot, "go.mod") } // Enabled reports whether modules are (or must be) enabled. diff --git a/src/cmd/go/testdata/script/gotoolchain.txt b/src/cmd/go/testdata/script/gotoolchain.txt index 4df56887b6..a202901ef3 100644 --- a/src/cmd/go/testdata/script/gotoolchain.txt +++ b/src/cmd/go/testdata/script/gotoolchain.txt @@ -32,6 +32,120 @@ env GOTOOLCHAIN=go1.999testmod go version stderr 'go: downloading go1.999testmod \(.*/.*\)' +# GOTOOLCHAIN=auto +env GOTOOLCHAIN=auto +env TESTGO_VERSION=go1.100 # set TESTGO_VERSION because devel is newer than everything + +# toolchain line in go.mod +cp go119toolchain1999 go.mod +go version +stdout go1.999 + +# toolchain line in go.work +cp empty go.mod +cp go119toolchain1999 go.work +go version +stdout go1.999 +rm go.work + +# go version in go.mod +cp go1999 go.mod +go version +stdout go1.999 + +# go version in go.work +cp empty go.mod +cp go1999 go.work +go version +stdout go1.999 +rm go.work + +# GOTOOLCHAIN=auto falls back to local toolchain if newer than go line +env TESTGO_VERSION=go1.1000 + +# toolchain line in go.mod +cp go119toolchain1999 go.mod +go version +stdout go1.999 + +# toolchain line in go.work +cp empty go.mod +cp go119toolchain1999 go.work +go version +stdout go1.999 +rm go.work + +# go version in go.mod +cp go1999 go.mod +go version +! stdout go1.999 + +# go version in go.work +cp empty go.mod +cp go1999 go.work +go version +! stdout go1.999 +rm go.work + +# GOTOOLCHAIN=auto+go1.1000 falls back to go1.1000 if newer than go line +env TESTGO_VERSION=go1.1 +env GOTOOLCHAIN=auto+go1.1000 + +# toolchain line in go.mod +cp go119toolchain1999 go.mod +go version +stdout go1.999 + +# toolchain line in go.work +cp empty go.mod +cp go119toolchain1999 go.work +go version +stdout go1.999 +rm go.work + +# go version in go.mod +cp go1999 go.mod +! go version +stderr go1.1000 + +# go version in go.work +cp empty go.mod +cp go1999 go.work +! go version +stderr go1.1000 +rm go.work + +# GOTOOLCHAIN=path refuses to download +env GOTOOLCHAIN=path +env TESTGO_VERSION=go1.19 + +cp go1999 go.mod +go version +stdout go1.999 + +cp go1999mod go.mod +! go version +stderr '^go: cannot find "go1.999mod" in PATH$' + +-- empty -- + +-- go1999 -- +go 1.999testpath + +-- go1999mod -- +go 1.999mod + +-- go119 --- +go 1.19 + +-- go119toolchain1999 -- +go 1.19 +toolchain go1.999testpath + +-- go1999toolchain119 -- +go 1.999testpath +toolchain go1.19 + -- go1.999testpath.go -- package main