]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: download newer toolchain if needed during go install m@v
authorRuss Cox <rsc@golang.org>
Mon, 22 May 2023 15:17:31 +0000 (11:17 -0400)
committerGopher Robot <gobot@golang.org>
Tue, 23 May 2023 16:36:17 +0000 (16:36 +0000)
go install m@v and go run m@v are the only commands
that ignore the local go.mod. As such they need to use a
different signal to find the Go version, namely the m@v go.mod.
Because there is no way to predict that Go version (no equivalent
of "go version" for interrogating the local go.mod), if we do switch
toolchains we always print about it.

For #57001.

Change-Id: I981a0b8fa61992b353589355ba72a3b9d55914e2
Reviewed-on: https://go-review.googlesource.com/c/go/+/497079
Auto-Submit: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>

src/cmd/go/gotoolchain.go
src/cmd/go/testdata/mod/rsc.io_fortune_v0.0.1.txt [new file with mode: 0644]
src/cmd/go/testdata/script/gotoolchain.txt
src/cmd/go/testdata/script/install_dep_version.txt

index d43d854e3674a8875b9597524471c08c40d072f0..088f9a8040fd3a79006ba51c39431742dbaf726a 100644 (file)
@@ -9,21 +9,28 @@ package main
 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 (
@@ -81,16 +88,30 @@ func switchGoToolchain() {
        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
+                       }
                }
        }
 
@@ -252,6 +273,114 @@ func modGoToolchain() (goVers, toolchain string) {
        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
+}
diff --git a/src/cmd/go/testdata/mod/rsc.io_fortune_v0.0.1.txt b/src/cmd/go/testdata/mod/rsc.io_fortune_v0.0.1.txt
new file mode 100644 (file)
index 0000000..7aae658
--- /dev/null
@@ -0,0 +1,17 @@
+rsc.io/fortune 0.0.1
+written by hand
+
+-- .mod --
+module rsc.io/fortune
+go 1.21rc999
+
+-- .info --
+{"Version":"v0.0.1"}
+-- fortune.go --
+package main
+
+import "rsc.io/quote"
+
+func main() {
+       println(quote.Hello())
+}
index 505317d2834c91d2995140754ce8ecc6b3bb3a6c..0309db3c513aeb0613840a4fb9e6f98a9003af0c 100644 (file)
@@ -158,6 +158,28 @@ cp go1999mod go.mod
 ! go version
 stderr '^go: cannot find "go1.999mod" in PATH$'
 
+# go install m@v should use go version in m@v's go.mod
+env GOTOOLCHAIN=path
+env TESTGO_VERSION=go1.19
+cp go1999 go.mod
+! go install rsc.io/fortune/nonexist@v0.0.1
+stderr '^go: cannot find "go1.21rc999" in PATH$'
+
+# go run m@v should use go version in m@v's go.mod
+env GOTOOLCHAIN=path
+env TESTGO_VERSION=go1.19
+cp go1999 go.mod
+! go run -unknownflag=here rsc.io/fortune/nonexist@v0.0.1 args here
+stderr '^go: cannot find "go1.21rc999" in PATH$'
+go run -unknownflag here rsc.io/fortune/nonexist@v0.0.1
+stdout 'go1.999testpath here!'
+
+# go run m@v should handle known flags correctly
+! go run -gcflags foo rsc.io/fortune/nonexist@v0.0.1 args here
+stderr '^go: cannot find "go1.21rc999" in PATH$'
+! go run -x rsc.io/fortune/nonexist@v0.0.1 args here
+stderr '^go: cannot find "go1.21rc999" in PATH$'
+
 -- empty --
 
 -- go1999 --
index 22b52e541e8205915987f8ed9eadae6d51783294..58330e6b72a1ab5668acc413215809ccf449c874 100644 (file)
@@ -3,4 +3,6 @@
 # because the checksumOk function was failing because modfetch.GoSumFile
 # was not set when running outside of a module.
 
+env GOTOOLCHAIN=local
+
 go install --mod=readonly example.com/depends/on/generics@v1.0.0
\ No newline at end of file