import (
"context"
"fmt"
+ "go/build"
"internal/godebug"
"io/fs"
"log"
"os"
"os/exec"
+ "path"
"path/filepath"
"runtime"
"strings"
+ "sync"
"syscall"
"cmd/go/internal/base"
"cmd/go/internal/cfg"
"cmd/go/internal/gover"
"cmd/go/internal/modcmd"
+ "cmd/go/internal/modfetch"
"cmd/go/internal/modload"
+ "cmd/go/internal/run"
+
+ "golang.org/x/mod/module"
)
const (
pathOnly := gotoolchain == "path"
if gotoolchain == "auto" || gotoolchain == "path" {
// Locate and read go.mod or go.work.
- goVers, toolchain := modGoToolchain()
- if toolchain != "" {
- // toolchain line wins by itself
- gotoolchain = toolchain
- } else {
+ // For go install m@v, it's the installed module's go.mod.
+ if m, goVers, ok := goInstallVersion(); ok {
v := strings.TrimPrefix(min, "go")
if gover.Compare(v, goVers) < 0 {
+ // Always print, because otherwise there's no way for the user to know
+ // that a non-default toolchain version is being used here.
+ // (Normally you can run "go version", but go install m@v ignores the
+ // context that "go version" works in.)
+ fmt.Fprintf(os.Stderr, "go: using go%s for %v\n", goVers, m)
v = goVers
}
gotoolchain = "go" + v
+ } else {
+ goVers, toolchain := modGoToolchain()
+ if toolchain != "" {
+ // toolchain line wins by itself
+ gotoolchain = toolchain
+ } else {
+ v := strings.TrimPrefix(min, "go")
+ if gover.Compare(v, goVers) < 0 {
+ v = goVers
+ }
+ gotoolchain = "go" + v
+ }
}
}
if err != nil {
base.Fatalf("%v", err)
}
-
return gover.GoModLookup(data, "go"), gover.GoModLookup(data, "toolchain")
}
+
+// goInstallVersion looks at the command line to see if it is go install m@v or go run m@v.
+// If so, it returns the m@v and the go version from that module's go.mod.
+func goInstallVersion() (m module.Version, goVers string, ok bool) {
+ // Note: We assume there are no flags between 'go' and 'install' or 'run'.
+ // During testing there are some debugging flags that are accepted
+ // in that position, but in production go binaries there are not.
+ if len(os.Args) < 3 || (os.Args[1] != "install" && os.Args[1] != "run") {
+ return module.Version{}, "", false
+ }
+
+ var arg string
+ switch os.Args[1] {
+ case "install":
+ // Cannot parse 'go install' command line precisely, because there
+ // may be new flags we don't know about. Instead, assume the final
+ // argument is a pkg@version we can use.
+ arg = os.Args[len(os.Args)-1]
+ case "run":
+ // For run, the pkg@version can be anywhere on the command line.
+ // We don't know the flags, so we can't strictly speaking do this correctly.
+ // We do the best we can by interrogating the CmdRun flags and assume
+ // that any unknown flag does not take an argument.
+ args := os.Args[2:]
+ for i := 0; i < len(args); i++ {
+ a := args[i]
+ if !strings.HasPrefix(a, "-") {
+ arg = a
+ break
+ }
+ if a == "-" {
+ if i+1 < len(args) {
+ arg = args[i+1]
+ }
+ break
+ }
+ a = strings.TrimPrefix(a, "-")
+ a = strings.TrimPrefix(a, "-")
+ if strings.HasPrefix(a, "-") {
+ // non-flag but also non-m@v
+ break
+ }
+ if strings.Contains(a, "=") {
+ // already has value
+ continue
+ }
+ f := run.CmdRun.Flag.Lookup(a)
+ if f == nil {
+ // Unknown flag. Assume it doesn't take a value: best we can do.
+ continue
+ }
+ if bf, ok := f.Value.(interface{ IsBoolFlag() bool }); ok && bf.IsBoolFlag() {
+ // Does not take value.
+ continue
+ }
+ i++ // Does take a value; skip it.
+ }
+ }
+ if !strings.Contains(arg, "@") || build.IsLocalImport(arg) || filepath.IsAbs(arg) {
+ return module.Version{}, "", false
+ }
+ m.Path, m.Version, _ = strings.Cut(arg, "@")
+ if m.Path == "" || m.Version == "" || gover.IsToolchain(m.Path) {
+ return module.Version{}, "", false
+ }
+
+ // We need to resolve the pkg to a module, to find its go.mod.
+ // Normally we use the module loading code to grab the full
+ // module file tree for pkg and all its path prefixes, checking each
+ // for a file tree that contains source code for pkg.
+ // We can't do that here, because the modules may use newer versions
+ // of Go that affect which files are contained in the modules and therefore
+ // affect their checksums: there is no guarantee an older version of Go
+ // can extract a newer Go module from a VCS repo and choose the right files
+ // (this allows evolution such as https://go.dev/issue/42965).
+ // Instead, we check for a module at all path prefixes (including path itself)
+ // and take the max of the Go versions along the path.
+ var paths []string
+ for len(m.Path) > 1 {
+ paths = append(paths, m.Path)
+ m.Path = path.Dir(m.Path)
+ }
+ goVersions := make([]string, len(paths))
+ var wg sync.WaitGroup
+ for i, path := range paths {
+ i := i
+ path := path
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ // TODO(rsc): m.Version could in general be something like latest or patch or upgrade.
+ // Use modload.Query. See review comment on https://go.dev/cl/497079.
+ data, err := modfetch.GoMod(context.Background(), path, m.Version)
+ if err != nil {
+ return
+ }
+ goVersions[i] = gover.GoModLookup(data, "go")
+ }()
+ }
+ wg.Wait()
+ goVers = ""
+ for i, v := range goVersions {
+ if gover.Compare(goVers, v) < 0 {
+ m.Path = paths[i]
+ goVers = v
+ }
+ }
+ return m, goVers, true
+}