]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: implement GOTOOLCHAIN=auto
authorRuss Cox <rsc@golang.org>
Thu, 11 May 2023 18:32:56 +0000 (14:32 -0400)
committerGopher Robot <gobot@golang.org>
Sat, 20 May 2023 19:17:27 +0000 (19:17 +0000)
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 <rsc@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
Auto-Submit: Russ Cox <rsc@golang.org>

src/cmd/go/gotoolchain.go
src/cmd/go/gotoolchain_port.go [new file with mode: 0644]
src/cmd/go/gotoolchain_test.go [new file with mode: 0644]
src/cmd/go/internal/base/path.go
src/cmd/go/internal/modload/init.go
src/cmd/go/testdata/script/gotoolchain.txt

index ef1b5313131d911942f7239dc9baabfe4e70ac0d..b66561cadc8ac5d12bc5f34eab92f1fbe87fad0d 100644 (file)
@@ -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 (file)
index 0000000..a530059
--- /dev/null
@@ -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 (file)
index 0000000..22c3958
--- /dev/null
@@ -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)
+               }
+       }
+}
index ebe4f153ed2a20d9c2b13107687108351082ad54..64f213b408628eb155a4650484957db693d74696 100644 (file)
@@ -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
 }
index 31c66a6fde139a40acc7b42c7456325a462bae0d..661a379d82ead7c45b9e8b556f993731dfdca337 100644 (file)
@@ -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.
index 4df56887b6cf1ffb7f591c84f9c1f63273770244..a202901ef30fd6ebe5f1cf774f97c0bf54b6bed2 100644 (file)
@@ -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