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"
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
}
// 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)
}
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
+}
--- /dev/null
+// 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:]
+}
--- /dev/null
+// 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)
+ }
+ }
+}
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
}
// 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
}
}
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.
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