NewerToolchain needs a list of candidate toolchains.
Currently it always consults the module version list, using the network.
When GOTOOLCHAIN=path, it should probably not do this,
both because =path implies we don't want to use the network
and because not every released version will be in $PATH.
Instead, scan $PATH to find the available versions.
For #57001.
Change-Id: I478612c88d1504704a3f53fcfc73d8d4eedae493
Reviewed-on: https://go-review.googlesource.com/c/go/+/499296
Reviewed-by: Bryan Mills <bcmills@google.com>
TryBot-Bypass: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
Auto-Submit: Russ Cox <rsc@golang.org>
--- /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.
+
+//go:build !unix && !plan9 && !windows
+
+package toolchain
+
+import "io/fs"
+
+// pathDirs returns the directories in the system search path.
+func pathDirs() []string {
+ return nil
+}
+
+// pathVersion returns the Go version implemented by the file
+// described by de and info in directory dir.
+// The analysis only uses the name itself; it does not run the program.
+func pathVersion(dir string, de fs.DirEntry, info fs.FileInfo) (string, bool) {
+ return "", false
+}
--- /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 toolchain
+
+import (
+ "io/fs"
+ "os"
+ "path/filepath"
+
+ "cmd/go/internal/gover"
+)
+
+// pathDirs returns the directories in the system search path.
+func pathDirs() []string {
+ return filepath.SplitList(os.Getenv("path"))
+}
+
+// pathVersion returns the Go version implemented by the file
+// described by de and info in directory dir.
+// The analysis only uses the name itself; it does not run the program.
+func pathVersion(dir string, de fs.DirEntry, info fs.FileInfo) (string, bool) {
+ v := gover.FromToolchain(de.Name())
+ if v == "" || info.Mode()&0111 == 0 {
+ return "", false
+ }
+ return v, true
+}
--- /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.
+
+//go:build unix
+
+package toolchain
+
+import (
+ "internal/syscall/unix"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "syscall"
+
+ "cmd/go/internal/gover"
+)
+
+// pathDirs returns the directories in the system search path.
+func pathDirs() []string {
+ return filepath.SplitList(os.Getenv("PATH"))
+}
+
+// pathVersion returns the Go version implemented by the file
+// described by de and info in directory dir.
+// The analysis only uses the name itself; it does not run the program.
+func pathVersion(dir string, de fs.DirEntry, info fs.FileInfo) (string, bool) {
+ v := gover.FromToolchain(de.Name())
+ if v == "" {
+ return "", false
+ }
+
+ // Mimicking exec.findExecutable here.
+ // ENOSYS means Eaccess is not available or not implemented.
+ // EPERM can be returned by Linux containers employing seccomp.
+ // In both cases, fall back to checking the permission bits.
+ err := unix.Eaccess(filepath.Join(dir, de.Name()), unix.X_OK)
+ if (err == syscall.ENOSYS || err == syscall.EPERM) && info.Mode()&0111 != 0 {
+ err = nil
+ }
+ if err != nil {
+ return "", false
+ }
+
+ return v, true
+}
--- /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 toolchain
+
+import (
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "cmd/go/internal/gover"
+)
+
+// pathExts is a cached PATHEXT list.
+var pathExts struct {
+ once sync.Once
+ list []string
+}
+
+func initPathExts() {
+ var exts []string
+ x := os.Getenv(`PATHEXT`)
+ if x != "" {
+ for _, e := range strings.Split(strings.ToLower(x), `;`) {
+ if e == "" {
+ continue
+ }
+ if e[0] != '.' {
+ e = "." + e
+ }
+ exts = append(exts, e)
+ }
+ } else {
+ exts = []string{".com", ".exe", ".bat", ".cmd"}
+ }
+ pathExts.list = exts
+}
+
+// pathDirs returns the directories in the system search path.
+func pathDirs() []string {
+ return filepath.SplitList(os.Getenv("PATH"))
+}
+
+// pathVersion returns the Go version implemented by the file
+// described by de and info in directory dir.
+// The analysis only uses the name itself; it does not run the program.
+func pathVersion(dir string, de fs.DirEntry, info fs.FileInfo) (string, bool) {
+ pathExts.once.Do(initPathExts)
+ name, _, ok := cutExt(de.Name(), pathExts.list)
+ if !ok {
+ return "", false
+ }
+ v := gover.FromToolchain(name)
+ if v == "" {
+ return "", false
+ }
+ return v, true
+}
+
+// cutExt looks for any of the known extensions at the end of file.
+// If one is found, cutExt returns the file name with the extension trimmed,
+// the extension itself, and true to signal that an extension was found.
+// Otherwise cutExt returns file, "", false.
+func cutExt(file string, exts []string) (name, ext string, found bool) {
+ i := strings.LastIndex(file, ".")
+ if i < 0 {
+ return file, "", false
+ }
+ for _, x := range exts {
+ if strings.EqualFold(file[i:], x) {
+ return file[:i], file[i:], true
+ }
+ }
+ return file, "", false
+}
"os/exec"
"path/filepath"
"runtime"
+ "sort"
"strings"
"cmd/go/internal/base"
// Otherwise we use the latest 1.N if that's allowed.
// Otherwise we use the latest release.
func NewerToolchain(ctx context.Context, version string) (string, error) {
+ fetch := autoToolchains
+ if !HasAuto() {
+ fetch = pathToolchains
+ }
+ list, err := fetch(ctx)
+ if err != nil {
+ return "", err
+ }
+ return newerToolchain(version, list)
+}
+
+// autoToolchains returns the list of toolchain versions available to GOTOOLCHAIN=auto or =min+auto mode.
+func autoToolchains(ctx context.Context) ([]string, error) {
var versions *modfetch.Versions
err := modfetch.TryProxies(func(proxy string) error {
v, err := modfetch.Lookup(ctx, proxy, "go").Versions(ctx, "")
return nil
})
if err != nil {
- return "", err
+ return nil, err
}
- return newerToolchain(version, versions.List)
+ return versions.List, nil
+}
+
+// pathToolchains returns the list of toolchain versions available to GOTOOLCHAIN=path or =min+path mode.
+func pathToolchains(ctx context.Context) ([]string, error) {
+ have := make(map[string]bool)
+ var list []string
+ for _, dir := range pathDirs() {
+ if dir == "" || !filepath.IsAbs(dir) {
+ // Refuse to use local directories in $PATH (hard-coding exec.ErrDot).
+ continue
+ }
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ continue
+ }
+ for _, de := range entries {
+ if de.IsDir() || !strings.HasPrefix(de.Name(), "go1.") {
+ continue
+ }
+ info, err := de.Info()
+ if err != nil {
+ continue
+ }
+ v, ok := pathVersion(dir, de, info)
+ if !ok || !strings.HasPrefix(v, "1.") || have[v] {
+ continue
+ }
+ have[v] = true
+ list = append(list, v)
+ }
+ }
+ sort.Slice(list, func(i, j int) bool {
+ return gover.Compare(list[i], list[j]) < 0
+ })
+ return list, nil
}
// newerToolchain implements NewerToolchain where the list of choices is known.
# GOTOOLCHAIN=go1.50.0
env GOTOOLCHAIN=go1.50.0
-go version
-stdout 'running go1.50.0 from PATH'
+! go version
+stderr 'running go1.50.0 from PATH'
# GOTOOLCHAIN=path with toolchain line
-env GOTOOLCHAIN=path
+env GOTOOLCHAIN=local
go mod init m
go mod edit -toolchain=go1.50.0
-go version
-stdout 'running go1.50.0 from PATH'
+grep go1.50.0 go.mod
+env GOTOOLCHAIN=path
+! go version
+stderr 'running go1.50.0 from PATH'
# GOTOOLCHAIN=path with go line
+env GOTOOLCHAIN=local
+go mod edit -toolchain=none -go=1.50.0
+grep 'go 1.50.0' go.mod
+! grep toolchain go.mod
env GOTOOLCHAIN=path
-go mod edit -toolchain=none -go=go1.50.0
-go version
-stdout 'running go1.50.0 from PATH'
+! go version
+stderr 'running go1.50.0 from PATH'
# GOTOOLCHAIN=auto with toolchain line
-env GOTOOLCHAIN=auto
+env GOTOOLCHAIN=local
go mod edit -toolchain=go1.50.0 -go=1.21
-go version
-stdout 'running go1.50.0 from PATH'
+grep 'go 1.21$' go.mod
+grep 'toolchain go1.50.0' go.mod
+env GOTOOLCHAIN=auto
+! go version
+stderr 'running go1.50.0 from PATH'
# GOTOOLCHAIN=auto with go line
+env GOTOOLCHAIN=local
+go mod edit -toolchain=none -go=1.50.0
+grep 'go 1.50.0$' go.mod
+! grep toolchain go.mod
env GOTOOLCHAIN=auto
-go mod edit -toolchain=none -go=go1.50.0
-go version
-stdout 'running go1.50.0 from PATH'
+! go version
+stderr 'running go1.50.0 from PATH'
+
+# NewerToolchain should find Go 1.50.0.
+env GOTOOLCHAIN=local
+go mod edit -toolchain=none -go=1.22
+grep 'go 1.22$' go.mod
+! grep toolchain go.mod
+env GOTOOLCHAIN=path
+! go run rsc.io/fortune@v0.0.1
+stderr 'running go1.50.0 from PATH'
-- fakego.go --
package main
exe, _ := os.Executable()
name := filepath.Base(exe)
name = strings.TrimSuffix(name, ".exe")
- fmt.Printf("running %s from PATH\n", name)
+ fmt.Fprintf(os.Stderr, "running %s from PATH\n", name)
+ os.Exit(1) // fail in case we are running this accidentally (like in "go mod edit")
}