From d59926ec7e5bf709265afc17680ef720e9110696 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Tue, 30 May 2023 13:28:56 -0400 Subject: [PATCH] cmd/go: scan $PATH to find version list in NewerToolchain 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 TryBot-Bypass: Russ Cox Run-TryBot: Russ Cox Auto-Submit: Russ Cox --- src/cmd/go/internal/toolchain/path_none.go | 21 +++++ src/cmd/go/internal/toolchain/path_plan9.go | 29 +++++++ src/cmd/go/internal/toolchain/path_unix.go | 46 +++++++++++ src/cmd/go/internal/toolchain/path_windows.go | 78 +++++++++++++++++++ src/cmd/go/internal/toolchain/toolchain.go | 53 ++++++++++++- .../go/testdata/script/gotoolchain_path.txt | 51 ++++++++---- 6 files changed, 261 insertions(+), 17 deletions(-) create mode 100644 src/cmd/go/internal/toolchain/path_none.go create mode 100644 src/cmd/go/internal/toolchain/path_plan9.go create mode 100644 src/cmd/go/internal/toolchain/path_unix.go create mode 100644 src/cmd/go/internal/toolchain/path_windows.go diff --git a/src/cmd/go/internal/toolchain/path_none.go b/src/cmd/go/internal/toolchain/path_none.go new file mode 100644 index 0000000000..8fdf71a6e6 --- /dev/null +++ b/src/cmd/go/internal/toolchain/path_none.go @@ -0,0 +1,21 @@ +// 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 +} diff --git a/src/cmd/go/internal/toolchain/path_plan9.go b/src/cmd/go/internal/toolchain/path_plan9.go new file mode 100644 index 0000000000..3f836a07b1 --- /dev/null +++ b/src/cmd/go/internal/toolchain/path_plan9.go @@ -0,0 +1,29 @@ +// 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 +} diff --git a/src/cmd/go/internal/toolchain/path_unix.go b/src/cmd/go/internal/toolchain/path_unix.go new file mode 100644 index 0000000000..519c53ec30 --- /dev/null +++ b/src/cmd/go/internal/toolchain/path_unix.go @@ -0,0 +1,46 @@ +// 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 +} diff --git a/src/cmd/go/internal/toolchain/path_windows.go b/src/cmd/go/internal/toolchain/path_windows.go new file mode 100644 index 0000000000..086c591e05 --- /dev/null +++ b/src/cmd/go/internal/toolchain/path_windows.go @@ -0,0 +1,78 @@ +// 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 +} diff --git a/src/cmd/go/internal/toolchain/toolchain.go b/src/cmd/go/internal/toolchain/toolchain.go index 3d565021e7..3a8d348abb 100644 --- a/src/cmd/go/internal/toolchain/toolchain.go +++ b/src/cmd/go/internal/toolchain/toolchain.go @@ -15,6 +15,7 @@ import ( "os/exec" "path/filepath" "runtime" + "sort" "strings" "cmd/go/internal/base" @@ -175,6 +176,19 @@ func Switch() { // 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, "") @@ -185,9 +199,44 @@ func NewerToolchain(ctx context.Context, version string) (string, error) { 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. diff --git a/src/cmd/go/testdata/script/gotoolchain_path.txt b/src/cmd/go/testdata/script/gotoolchain_path.txt index f0e7ab9123..2549fa4753 100644 --- a/src/cmd/go/testdata/script/gotoolchain_path.txt +++ b/src/cmd/go/testdata/script/gotoolchain_path.txt @@ -18,33 +18,53 @@ stdout go1.21pre3 # 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 @@ -60,5 +80,6 @@ func 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") } -- 2.50.0