From: Russ Cox Date: Thu, 11 May 2023 18:32:56 +0000 (-0400) Subject: cmd/go: implement GOTOOLCHAIN=auto X-Git-Tag: go1.21rc1~399 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=9c447b7cf630cb158fe5059bee3cf03d5ce56f97;p=gostls13.git cmd/go: implement GOTOOLCHAIN=auto The documentation is yet to be written (more work in the go command remains first). This CL implements the toolchain selection described in https://go.dev/design/57001-gotoolchain#the-and-lines-in-in-the-work-module with these changes based on the issue discussion: 1. GOTOOLCHAIN=auto looks for a go1.19.1 binary in $PATH and if found uses it instead of downloading Go 1.19.1 as a module. 2. GOTOOLCHAIN=path is like GOTOOLCHAIN=auto, with downloading disabled. 3. GOTOOLCHAIN=auto+version and GOTOOLCHAIN=path+version set a different minimum version of Go to use during the version selection. The default is to use the newer of what's on the go line or the current toolchain. If you are have Go 1.22 installed locally and want to switch to a minimum of Go 1.25 with go.mod files allowed to bump even further, you would set GOTOOLCHAIN=auto+go1.25. The minimum is also important when there is no go.mod involved, such as when you write a tiny x.go program and run "go run x.go". That would get Go 1.25 in this example, instead of falling back to the local Go 1.22. Change-Id: I286625a24420424c313d1082b9949a463b2fe14a Reviewed-on: https://go-review.googlesource.com/c/go/+/494436 Run-TryBot: Russ Cox TryBot-Result: Gopher Robot Reviewed-by: Bryan Mills Auto-Submit: Russ Cox --- 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" + gotoolchain = "auto" + } + + 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 } - env := gotoolchain + + pathOnly := gotoolchain == "path" if gotoolchain == "auto" || gotoolchain == "path" { - // TODO: Locate and read go.mod or go.work. - base.Fatalf("GOTOOLCHAIN=auto not yet implemented") + // 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 == runtime.Version() { + 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