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>
// 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)
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
"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"
)
}
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()
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 {
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)
}
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))
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 {
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 {
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
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)
-}
// 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
// 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
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.
}
return b.String()
}
+
+func (e *BuildListError) Unwrap() error { return e.Err }
}
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
}
// 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)
+}
"cmd/go/internal/gover"
"cmd/go/internal/imports"
"cmd/go/internal/modload"
+ "cmd/go/internal/toolchain"
"context"
+ "errors"
"golang.org/x/mod/module"
)
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{}
--- /dev/null
+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"