]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: propagate gover.TooNewError from modload.LoadModGraph
authorBryan C. Mills <bcmills@google.com>
Wed, 31 May 2023 19:17:14 +0000 (15:17 -0400)
committerGopher Robot <gobot@golang.org>
Thu, 1 Jun 2023 17:22:28 +0000 (17:22 +0000)
For #57001.

Change-Id: I639190b5f035139ba42a93ca03dd8a4c747556ea
Reviewed-on: https://go-review.googlesource.com/c/go/+/499678
Auto-Submit: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Bryan Mills <bcmills@google.com>
Reviewed-by: Russ Cox <rsc@golang.org>
src/cmd/go/internal/modcmd/download.go
src/cmd/go/internal/modcmd/graph.go
src/cmd/go/internal/modcmd/verify.go
src/cmd/go/internal/modget/get.go
src/cmd/go/internal/modload/buildlist.go
src/cmd/go/internal/mvs/errors.go
src/cmd/go/internal/toolchain/toolchain.go
src/cmd/go/internal/workcmd/sync.go
src/cmd/go/testdata/script/gotoolchain_modcmds.txt [new file with mode: 0644]

index 26ef1998de9e0d959e3ebd2392032d79604bee75..955f33650a0a8ac5d3d2caeb085222b57a9b6673 100644 (file)
@@ -150,7 +150,10 @@ func runDownload(ctx context.Context, cmd *base.Command, args []string) {
                                // However, we also need to load the full module graph, to ensure that
                                // we have downloaded enough of the module graph to run 'go list all',
                                // 'go mod graph', and similar commands.
-                               _ = modload.LoadModGraph(ctx, "")
+                               _, err := modload.LoadModGraph(ctx, "")
+                               if err != nil {
+                                       base.Fatalf("go: %v", err)
+                               }
 
                                for _, m := range modFile.Require {
                                        args = append(args, m.Mod.Path)
@@ -176,6 +179,23 @@ func runDownload(ctx context.Context, cmd *base.Command, args []string) {
        type token struct{}
        sem := make(chan token, runtime.GOMAXPROCS(0))
        infos, infosErr := modload.ListModules(ctx, args, 0, *downloadReuse)
+
+       // There is a bit of a chicken-and-egg problem here: ideally we need to know
+       // which Go version to switch to to download the requested modules, but if we
+       // haven't downloaded the module's go.mod file yet the GoVersion field of its
+       // info struct is not yet populated.
+       //
+       // We also need to be careful to only print the info for each module once
+       // if the -json flag is set.
+       //
+       // In theory we could go through each module in the list, attempt to download
+       // its go.mod file, and record the maximum version (either from the file or
+       // from the resulting TooNewError), all before we try the actual full download
+       // of each module.
+       //
+       // For now, we just let it fail: the user can explicitly set GOTOOLCHAIN
+       // and retry if they want to.
+
        if !haveExplicitArgs && modload.WorkFilePath() == "" {
                // 'go mod download' is sometimes run without arguments to pre-populate the
                // module cache. In modules that aren't at go 1.17 or higher, it may fetch
index 555604dc84a3817d7085a580909e50f6775ff2e3..0265a0074c405a61d332ac208633f59cd23215b1 100644 (file)
@@ -13,7 +13,9 @@ import (
 
        "cmd/go/internal/base"
        "cmd/go/internal/cfg"
+       "cmd/go/internal/gover"
        "cmd/go/internal/modload"
+       "cmd/go/internal/toolchain"
 
        "golang.org/x/mod/module"
 )
@@ -57,7 +59,20 @@ func runGraph(ctx context.Context, cmd *base.Command, args []string) {
        }
        modload.ForceUseModules = true
        modload.RootMode = modload.NeedRoot
-       mg := modload.LoadModGraph(ctx, graphGo.String())
+
+       goVersion := graphGo.String()
+       if goVersion != "" && gover.Compare(gover.Local(), goVersion) < 0 {
+               toolchain.TryVersion(ctx, goVersion)
+               base.Fatalf("go: %v", &gover.TooNewError{
+                       What:      "-go flag",
+                       GoVersion: goVersion,
+               })
+       }
+
+       mg, err := modload.LoadModGraph(ctx, goVersion)
+       if err != nil {
+               base.Fatalf("go: %v", err)
+       }
 
        w := bufio.NewWriter(os.Stdout)
        defer w.Flush()
index 0828c4718d4a34b2fed31d039cd39328f07902e8..3bc6c5a1409225169df77148e028e0af8bff2d08 100644 (file)
@@ -57,9 +57,12 @@ func runVerify(ctx context.Context, cmd *base.Command, args []string) {
        type token struct{}
        sem := make(chan token, runtime.GOMAXPROCS(0))
 
+       mg, err := modload.LoadModGraph(ctx, "")
+       if err != nil {
+               base.Fatalf("go: %v", err)
+       }
+       mods := mg.BuildList()[modload.MainModules.Len():]
        // Use a slice of result channels, so that the output is deterministic.
-       const defaultGoVersion = ""
-       mods := modload.LoadModGraph(ctx, defaultGoVersion).BuildList()[modload.MainModules.Len():]
        errsChans := make([]<-chan []error, len(mods))
 
        for i, mod := range mods {
index e1c0e5b4f6295ac639a7d5b650bd796c9a86319d..33b9d6b14f99dc343969c09d98cdbb28787c66ee 100644 (file)
@@ -384,13 +384,12 @@ func runGet(ctx context.Context, cmd *base.Command, args []string) {
        oldReqs := reqsFromGoMod(modload.ModFile())
 
        if err := modload.WriteGoMod(ctx, opts); err != nil {
-               if tooNew, ok := err.(*gover.TooNewError); ok {
+               if tooNew := (*gover.TooNewError)(nil); errors.As(err, &tooNew) {
                        // This can happen for 'go get go@newversion'
                        // when all the required modules are old enough
                        // but the command line is not.
-                       // TODO(bcmills): Perhaps LoadModGraph should catch this,
-                       // in which case the tryVersion here should be removed.
-                       tryVersion(ctx, tooNew.GoVersion)
+                       // TODO(bcmills): modload.EditBuildList should catch this instead.
+                       toolchain.TryVersion(ctx, tooNew.GoVersion)
                }
                base.Fatalf("go: %v", err)
        }
@@ -491,8 +490,13 @@ type matchInModuleKey struct {
 func newResolver(ctx context.Context, queries []*query) *resolver {
        // LoadModGraph also sets modload.Target, which is needed by various resolver
        // methods.
-       const defaultGoVersion = ""
-       mg := modload.LoadModGraph(ctx, defaultGoVersion)
+       mg, err := modload.LoadModGraph(ctx, "")
+       if err != nil {
+               if tooNew := (*gover.TooNewError)(nil); errors.As(err, &tooNew) {
+                       toolchain.TryVersion(ctx, tooNew.GoVersion)
+               }
+               base.Fatalf("go: %v", err)
+       }
 
        buildList := mg.BuildList()
        initialVersion := make(map[string]string, len(buildList))
@@ -1229,13 +1233,13 @@ func (r *resolver) resolveQueries(ctx context.Context, queries []*query) (change
                goVers := ""
                for _, q := range queries {
                        for _, cs := range q.candidates {
-                               if e, ok := cs.err.(*gover.TooNewError); ok && gover.Compare(goVers, e.GoVersion) < 0 {
+                               if e := (*gover.TooNewError)(nil); errors.As(cs.err, &e) && gover.Compare(goVers, e.GoVersion) < 0 {
                                        goVers = e.GoVersion
                                }
                        }
                }
                if goVers != "" {
-                       tryVersion(ctx, goVers)
+                       toolchain.TryVersion(ctx, goVers)
                }
 
                for _, q := range queries {
@@ -1831,10 +1835,14 @@ func (r *resolver) updateBuildList(ctx context.Context, additions []module.Versi
 
        changed, err := modload.EditBuildList(ctx, additions, resolved)
        if err != nil {
+               if tooNew := (*gover.TooNewError)(nil); errors.As(err, &tooNew) {
+                       toolchain.TryVersion(ctx, tooNew.GoVersion)
+                       base.Fatalf("go: %v", err)
+               }
+
                var constraint *modload.ConstraintError
                if !errors.As(err, &constraint) {
-                       base.Errorf("go: %v", err)
-                       return false
+                       base.Fatalf("go: %v", err)
                }
 
                if cfg.BuildV {
@@ -1873,8 +1881,15 @@ func (r *resolver) updateBuildList(ctx context.Context, additions []module.Versi
                return false
        }
 
-       const defaultGoVersion = ""
-       r.buildList = modload.LoadModGraph(ctx, defaultGoVersion).BuildList()
+       mg, err := modload.LoadModGraph(ctx, "")
+       if err != nil {
+               if tooNew := (*gover.TooNewError)(nil); errors.As(err, &tooNew) {
+                       toolchain.TryVersion(ctx, tooNew.GoVersion)
+               }
+               base.Fatalf("go: %v", err)
+       }
+
+       r.buildList = mg.BuildList()
        r.buildListVersion = make(map[string]string, len(r.buildList))
        for _, m := range r.buildList {
                r.buildListVersion[m.Path] = m.Version
@@ -1912,22 +1927,3 @@ func isNoSuchPackageVersion(err error) bool {
        var noPackage *modload.PackageNotInModuleError
        return isNoSuchModuleVersion(err) || errors.As(err, &noPackage)
 }
-
-// tryVersion tries to switch to a Go toolchain appropriate for version,
-// which was either found in a go.mod file of a dependency or resolved
-// on the command line from go@v.
-func tryVersion(ctx context.Context, version string) {
-       if !gover.IsValid(version) {
-               fmt.Fprintf(os.Stderr, "go: misuse of tryVersion: invalid version %q\n", version)
-               return
-       }
-       if (!toolchain.HasAuto() && !toolchain.HasPath()) || gover.Compare(version, gover.Local()) <= 0 {
-               return
-       }
-       tv, err := toolchain.NewerToolchain(ctx, version)
-       if err != nil {
-               base.Errorf("go: %v\n", err)
-       }
-       fmt.Fprintf(os.Stderr, "go: switching to %v\n", tv)
-       toolchain.SwitchTo(tv)
-}
index 70092da92f2e7583846957f0bd5048e7a4bde0ce..0e4c7afb2346b60fec8d7d8c9b004c3d644dafdb 100644 (file)
@@ -47,6 +47,9 @@ type Requirements struct {
        // given module path. The root modules of the graph are the set of main
        // modules in workspace mode, and the main module's direct requirements
        // outside workspace mode.
+       //
+       // The roots are always expected to contain an entry for the "go" module,
+       // indicating the Go language version in use.
        rootModules    []module.Version
        maxRootVersion map[string]string
 
@@ -537,10 +540,15 @@ func (mg *ModuleGraph) allRootsSelected() bool {
 // Modules are loaded automatically (and lazily) in LoadPackages:
 // LoadModGraph need only be called if LoadPackages is not,
 // typically in commands that care about modules but no particular package.
-func LoadModGraph(ctx context.Context, goVersion string) *ModuleGraph {
+func LoadModGraph(ctx context.Context, goVersion string) (*ModuleGraph, error) {
        rs := LoadModFile(ctx)
 
        if goVersion != "" {
+               v, _ := rs.rootSelected("go")
+               if gover.Compare(v, GoStrictVersion) >= 0 && gover.Compare(goVersion, v) < 0 {
+                       return nil, fmt.Errorf("requested Go version %s cannot load module graph (requires Go >= %s)", goVersion, v)
+               }
+
                pruning := pruningForGoVersion(goVersion)
                if pruning == unpruned && rs.pruning != unpruned {
                        // Use newRequirements instead of convertDepth because convertDepth
@@ -549,21 +557,15 @@ func LoadModGraph(ctx context.Context, goVersion string) *ModuleGraph {
                        rs = newRequirements(unpruned, rs.rootModules, rs.direct)
                }
 
-               mg, err := rs.Graph(ctx)
-               if err != nil {
-                       base.Fatalf("go: %v", err)
-               }
-               return mg
+               return rs.Graph(ctx)
        }
 
        rs, mg, err := expandGraph(ctx, rs)
        if err != nil {
-               base.Fatalf("go: %v", err)
+               return nil, err
        }
-
        requirements = rs
-
-       return mg
+       return mg, err
 }
 
 // expandGraph loads the complete module graph from rs.
index bf183cea9e88af62b0b8d773297f432bae740b84..8db65d656f027691220dcc2842e115e794f508ca 100644 (file)
@@ -101,3 +101,5 @@ func (e *BuildListError) Error() string {
        }
        return b.String()
 }
+
+func (e *BuildListError) Unwrap() error { return e.Err }
index 757ab6977dac0b72a109a49e824d74ec764177ac..84907c1419ff3b4d6a176b95ad1265ba8cebd491 100644 (file)
@@ -579,7 +579,7 @@ func goInstallVersion() (m module.Version, goVers string, found bool) {
        }
        noneSelected := func(path string) (version string) { return "none" }
        _, err := modload.QueryPackages(ctx, m.Path, m.Version, noneSelected, allowed)
-       if tooNew, ok := err.(*gover.TooNewError); ok {
+       if tooNew := (*gover.TooNewError)(nil); errors.As(err, &tooNew) {
                m.Path, m.Version, _ = strings.Cut(tooNew.What, "@")
                return m, tooNew.GoVersion, true
        }
@@ -591,3 +591,22 @@ func goInstallVersion() (m module.Version, goVers string, found bool) {
        // consulting go.mod.
        return m, "", true
 }
+
+// TryVersion tries to switch to a Go toolchain appropriate for version,
+// which was either found in a go.mod file of a dependency or resolved
+// on the command line from go@v.
+func TryVersion(ctx context.Context, version string) {
+       if !gover.IsValid(version) {
+               fmt.Fprintf(os.Stderr, "go: misuse of tryVersion: invalid version %q\n", version)
+               return
+       }
+       if (!HasAuto() && !HasPath()) || gover.Compare(version, gover.Local()) <= 0 {
+               return
+       }
+       tv, err := NewerToolchain(ctx, version)
+       if err != nil {
+               base.Errorf("go: %v\n", err)
+       }
+       fmt.Fprintf(os.Stderr, "go: switching to %v\n", tv)
+       SwitchTo(tv)
+}
index 1ecc3a8339573253bd7d064f17623d7899ade374..1d57a36dbc03b74a4f8eed879aff56ccc4e4bd43 100644 (file)
@@ -11,7 +11,9 @@ import (
        "cmd/go/internal/gover"
        "cmd/go/internal/imports"
        "cmd/go/internal/modload"
+       "cmd/go/internal/toolchain"
        "context"
+       "errors"
 
        "golang.org/x/mod/module"
 )
@@ -53,7 +55,11 @@ func runSync(ctx context.Context, cmd *base.Command, args []string) {
                base.Fatalf("go: no go.work file found\n\t(run 'go work init' first or specify path using GOWORK environment variable)")
        }
 
-       workGraph := modload.LoadModGraph(ctx, "")
+       workGraph, err := modload.LoadModGraph(ctx, "")
+       if tooNew := (*gover.TooNewError)(nil); errors.As(err, &tooNew) {
+               toolchain.TryVersion(ctx, tooNew.GoVersion)
+               base.Fatalf("go: %v", err)
+       }
        _ = workGraph
        mustSelectFor := map[module.Version][]module.Version{}
 
diff --git a/src/cmd/go/testdata/script/gotoolchain_modcmds.txt b/src/cmd/go/testdata/script/gotoolchain_modcmds.txt
new file mode 100644 (file)
index 0000000..67917da
--- /dev/null
@@ -0,0 +1,54 @@
+env TESTGO_VERSION=go1.21.0
+env TESTGO_VERSION_SWITCH=switch
+
+# If the main module's go.mod file lists a version lower than the version
+# required by its dependencies, the commands that fetch and diagnose the module
+# graph (such as 'go mod download' and 'go mod graph') should fail explicitly:
+# they can't interpret the graph themselves, and they aren't allowed to update
+# the go.mod file to record a specific, stable toolchain version that can.
+
+! go mod download rsc.io/future@v1.0.0
+stderr '^go: rsc.io/future@v1.0.0 requires go >= 1.999 \(running go 1.21.0\)'
+
+! go mod download rsc.io/future
+stderr '^go: rsc.io/future@v1.0.0 requires go >= 1.999 \(running go 1.21.0\)'
+
+! go mod download
+stderr '^go: rsc.io/future@v1.0.0: rsc.io/future requires go >= 1.999 \(running go 1.21.0\)'
+
+! go mod verify
+stderr '^go: rsc.io/future@v1.0.0: rsc.io/future requires go >= 1.999 \(running go 1.21.0\)'
+
+! go mod graph
+stderr '^go: rsc.io/future@v1.0.0: rsc.io/future requires go >= 1.999 \(running go 1.21.0\)'
+
+
+# 'go get' should update the main module's go.mod file to a version compatible with the
+# go version required for rsc.io/future, not fail.
+go get .
+stderr '^go: switching to go1.999testmod$'
+stderr '^go: upgraded go 1.21 => 1.999$'
+stderr '^go: added toolchain go1.999testmod$'
+
+
+# Now, the various 'go mod' subcommands should succeed.
+
+go mod download rsc.io/future@v1.0.0
+go mod download rsc.io/future
+go mod download
+
+go mod verify
+
+go mod graph
+
+
+-- go.mod --
+module example
+
+go 1.21
+
+require rsc.io/future v1.0.0
+-- example.go --
+package example
+
+import _ "rsc.io/future"