]> Cypherpunks.ru repositories - gostls13.git/blobdiff - src/cmd/go/internal/modload/load.go
cmd/go/internal/modload: avoid calling strings.HasPrefix twice in *MainModuleSet...
[gostls13.git] / src / cmd / go / internal / modload / load.go
index 9b6a81dd7c5dbc71a8b0bf5f50df650a92a00ad4..51eb141d4b38b827d1233c33b26619d51d15181f 100644 (file)
@@ -113,6 +113,7 @@ import (
        "cmd/go/internal/base"
        "cmd/go/internal/cfg"
        "cmd/go/internal/fsys"
+       "cmd/go/internal/gover"
        "cmd/go/internal/imports"
        "cmd/go/internal/modfetch"
        "cmd/go/internal/modindex"
@@ -122,7 +123,6 @@ import (
        "cmd/go/internal/str"
 
        "golang.org/x/mod/module"
-       "golang.org/x/mod/semver"
 )
 
 // loaded is the most recently-used package loader.
@@ -135,13 +135,13 @@ var loaded *loader
 
 // PackageOpts control the behavior of the LoadPackages function.
 type PackageOpts struct {
-       // GoVersion is the Go version to which the go.mod file should be updated
+       // TidyGoVersion is the Go version to which the go.mod file should be updated
        // after packages have been loaded.
        //
-       // An empty GoVersion means to use the Go version already specified in the
+       // An empty TidyGoVersion means to use the Go version already specified in the
        // main module's go.mod file, or the latest Go version if there is no main
        // module.
-       GoVersion string
+       TidyGoVersion string
 
        // Tags are the build tags in effect (as interpreted by the
        // cmd/go/internal/imports package).
@@ -149,7 +149,7 @@ type PackageOpts struct {
        Tags map[string]bool
 
        // Tidy, if true, requests that the build list and go.sum file be reduced to
-       // the minimial dependencies needed to reproducibly reload the requested
+       // the minimal dependencies needed to reproducibly reload the requested
        // packages.
        Tidy bool
 
@@ -234,6 +234,10 @@ type PackageOpts struct {
 
        // Resolve the query against this module.
        MainModule module.Version
+
+       // If Switcher is non-nil, then LoadPackages passes all encountered errors
+       // to Switcher.Error and tries Switcher.Switch before base.ExitIfErrors.
+       Switcher gover.Switcher
 }
 
 // LoadPackages identifies the set of packages matching the given patterns and
@@ -338,7 +342,10 @@ func LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string) (ma
                }
        }
 
-       initialRS := LoadModFile(ctx)
+       initialRS, err := loadModFile(ctx, &opts)
+       if err != nil {
+               base.Fatal(err)
+       }
 
        ld := loadFromRoots(ctx, loaderParams{
                PackageOpts:  opts,
@@ -363,11 +370,11 @@ func LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string) (ma
        if !ld.SilencePackageErrors {
                for _, match := range matches {
                        for _, err := range match.Errs {
-                               ld.errorf("%v\n", err)
+                               ld.error(err)
                        }
                }
        }
-       base.ExitIfErrors()
+       ld.exitIfErrors(ctx)
 
        if !opts.SilenceUnmatchedWarnings {
                search.WarnUnmatched(matches)
@@ -376,7 +383,6 @@ func LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string) (ma
        if opts.Tidy {
                if cfg.BuildV {
                        mg, _ := ld.requirements.Graph(ctx)
-
                        for _, m := range initialRS.rootModules {
                                var unused bool
                                if ld.requirements.pruning == unpruned {
@@ -398,9 +404,26 @@ func LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string) (ma
                }
 
                keep := keepSums(ctx, ld, ld.requirements, loadedZipSumsOnly)
-               if compatDepth := pruningForGoVersion(ld.TidyCompatibleVersion); compatDepth != ld.requirements.pruning {
-                       compatRS := newRequirements(compatDepth, ld.requirements.rootModules, ld.requirements.direct)
-                       ld.checkTidyCompatibility(ctx, compatRS)
+               compatVersion := ld.TidyCompatibleVersion
+               goVersion := ld.requirements.GoVersion()
+               if compatVersion == "" {
+                       if gover.Compare(goVersion, gover.GoStrictVersion) < 0 {
+                               compatVersion = gover.Prev(goVersion)
+                       } else {
+                               // Starting at GoStrictVersion, we no longer maintain compatibility with
+                               // versions older than what is listed in the go.mod file.
+                               compatVersion = goVersion
+                       }
+               }
+               if gover.Compare(compatVersion, goVersion) > 0 {
+                       // Each version of the Go toolchain knows how to interpret go.mod and
+                       // go.sum files produced by all previous versions, so a compatibility
+                       // version higher than the go.mod version adds nothing.
+                       compatVersion = goVersion
+               }
+               if compatPruning := pruningForGoVersion(compatVersion); compatPruning != ld.requirements.pruning {
+                       compatRS := newRequirements(compatPruning, ld.requirements.rootModules, ld.requirements.direct)
+                       ld.checkTidyCompatibility(ctx, compatRS, compatVersion)
 
                        for m := range keepSums(ctx, ld, compatRS, loadedZipSumsOnly) {
                                keep[m] = true
@@ -415,15 +438,10 @@ func LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string) (ma
                        // loaded.requirements, but here we may have also loaded (and want to
                        // preserve checksums for) additional entities from compatRS, which are
                        // only needed for compatibility with ld.TidyCompatibleVersion.
-                       if err := modfetch.WriteGoSum(keep, mustHaveCompleteRequirements()); err != nil {
-                               base.Fatalf("go: %v", err)
+                       if err := modfetch.WriteGoSum(ctx, keep, mustHaveCompleteRequirements()); err != nil {
+                               base.Fatal(err)
                        }
                }
-
-               // Update the go.mod file's Go version if necessary.
-               if modFile := ModFile(); modFile != nil && ld.GoVersion != "" {
-                       modFile.AddGoStmt(ld.GoVersion)
-               }
        }
 
        // Success! Update go.mod and go.sum (if needed) and return the results.
@@ -441,8 +459,8 @@ func LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string) (ma
        sort.Strings(loadedPackages)
 
        if !ExplicitWriteGoMod && opts.ResolveMissingImports {
-               if err := commitRequirements(ctx); err != nil {
-                       base.Fatalf("go: %v", err)
+               if err := commitRequirements(ctx, WriteOpts{}); err != nil {
+                       base.Fatal(err)
                }
        }
 
@@ -553,7 +571,7 @@ func resolveLocalPackage(ctx context.Context, dir string, rs *Requirements) (str
                                        return "", fmt.Errorf("without -mod=vendor, directory %s has no package path", absDir)
                                }
 
-                               readVendorList(mainModule)
+                               readVendorList(VendorDir())
                                if _, ok := vendorPkgModule[pkg]; !ok {
                                        return "", fmt.Errorf("directory %s is not a package listed in vendor/modules.txt", absDir)
                                }
@@ -628,6 +646,9 @@ var (
 // if dir is in the module cache copy of a module in our build list.
 func pathInModuleCache(ctx context.Context, dir string, rs *Requirements) string {
        tryMod := func(m module.Version) (string, bool) {
+               if gover.IsToolchain(m.Path) {
+                       return "", false
+               }
                var root string
                var err error
                if repl := Replacement(m); repl.Path != "" && repl.Version == "" {
@@ -636,9 +657,9 @@ func pathInModuleCache(ctx context.Context, dir string, rs *Requirements) string
                                root = filepath.Join(replaceRelativeTo(), root)
                        }
                } else if repl.Path != "" {
-                       root, err = modfetch.DownloadDir(repl)
+                       root, err = modfetch.DownloadDir(ctx, repl)
                } else {
-                       root, err = modfetch.DownloadDir(m)
+                       root, err = modfetch.DownloadDir(ctx, m)
                }
                if err != nil {
                        return "", false
@@ -702,7 +723,7 @@ func ImportFromFiles(ctx context.Context, gofiles []string) {
        tags := imports.Tags()
        imports, testImports, err := imports.ScanFiles(gofiles, tags)
        if err != nil {
-               base.Fatalf("go: %v", err)
+               base.Fatal(err)
        }
 
        loaded = loadFromRoots(ctx, loaderParams{
@@ -721,8 +742,8 @@ func ImportFromFiles(ctx context.Context, gofiles []string) {
        requirements = loaded.requirements
 
        if !ExplicitWriteGoMod {
-               if err := commitRequirements(ctx); err != nil {
-                       base.Fatalf("go: %v", err)
+               if err := commitRequirements(ctx, WriteOpts{}); err != nil {
+                       base.Fatal(err)
                }
        }
 }
@@ -756,7 +777,7 @@ func (mms *MainModuleSet) DirImportPath(ctx context.Context, dir string) (path s
                                longestPrefixVersion = v
                                suffix := filepath.ToSlash(str.TrimFilePathPrefix(dir, modRoot))
                                if strings.HasPrefix(suffix, "vendor/") {
-                                       longestPrefixPath = strings.TrimPrefix(suffix, "vendor/")
+                                       longestPrefixPath = suffix[len("vendor/"):]
                                        continue
                                }
                                longestPrefixPath = pathpkg.Join(mms.PathPrefix(v), suffix)
@@ -858,16 +879,42 @@ func (ld *loader) reset() {
        ld.pkgs = nil
 }
 
-// errorf reports an error via either os.Stderr or base.Errorf,
+// error reports an error via either os.Stderr or base.Error,
 // according to whether ld.AllowErrors is set.
-func (ld *loader) errorf(format string, args ...any) {
+func (ld *loader) error(err error) {
        if ld.AllowErrors {
-               fmt.Fprintf(os.Stderr, format, args...)
+               fmt.Fprintf(os.Stderr, "go: %v\n", err)
+       } else if ld.Switcher != nil {
+               ld.Switcher.Error(err)
        } else {
-               base.Errorf(format, args...)
+               base.Error(err)
        }
 }
 
+// switchIfErrors switches toolchains if a switch is needed.
+func (ld *loader) switchIfErrors(ctx context.Context) {
+       if ld.Switcher != nil {
+               ld.Switcher.Switch(ctx)
+       }
+}
+
+// exitIfErrors switches toolchains if a switch is needed
+// or else exits if any errors have been reported.
+func (ld *loader) exitIfErrors(ctx context.Context) {
+       ld.switchIfErrors(ctx)
+       base.ExitIfErrors()
+}
+
+// goVersion reports the Go version that should be used for the loader's
+// requirements: ld.TidyGoVersion if set, or ld.requirements.GoVersion()
+// otherwise.
+func (ld *loader) goVersion() string {
+       if ld.TidyGoVersion != "" {
+               return ld.TidyGoVersion
+       }
+       return ld.requirements.GoVersion()
+}
+
 // A loadPkg records information about a single loaded package.
 type loadPkg struct {
        // Populated at construction time:
@@ -989,46 +1036,6 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
                work:         par.NewQueue(runtime.GOMAXPROCS(0)),
        }
 
-       if ld.GoVersion == "" {
-               ld.GoVersion = MainModules.GoVersion()
-
-               if ld.Tidy && versionLess(LatestGoVersion(), ld.GoVersion) {
-                       ld.errorf("go: go.mod file indicates go %s, but maximum version supported by tidy is %s\n", ld.GoVersion, LatestGoVersion())
-                       base.ExitIfErrors()
-               }
-       }
-
-       if ld.Tidy {
-               if ld.TidyCompatibleVersion == "" {
-                       ld.TidyCompatibleVersion = priorGoVersion(ld.GoVersion)
-               } else if versionLess(ld.GoVersion, ld.TidyCompatibleVersion) {
-                       // Each version of the Go toolchain knows how to interpret go.mod and
-                       // go.sum files produced by all previous versions, so a compatibility
-                       // version higher than the go.mod version adds nothing.
-                       ld.TidyCompatibleVersion = ld.GoVersion
-               }
-
-               if semver.Compare("v"+ld.GoVersion, tidyGoModSumVersionV) < 0 {
-                       ld.skipImportModFiles = true
-               }
-       }
-
-       if semver.Compare("v"+ld.GoVersion, narrowAllVersionV) < 0 && !ld.UseVendorAll {
-               // The module's go version explicitly predates the change in "all" for graph
-               // pruning, so continue to use the older interpretation.
-               ld.allClosesOverTests = true
-       }
-
-       var err error
-       desiredPruning := pruningForGoVersion(ld.GoVersion)
-       if ld.requirements.pruning == workspace {
-               desiredPruning = workspace
-       }
-       ld.requirements, err = convertPruning(ctx, ld.requirements, desiredPruning)
-       if err != nil {
-               ld.errorf("go: %v\n", err)
-       }
-
        if ld.requirements.pruning == unpruned {
                // If the module graph does not support pruning, we assume that we will need
                // the full module graph in order to load package dependencies.
@@ -1041,12 +1048,36 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
                var err error
                ld.requirements, _, err = expandGraph(ctx, ld.requirements)
                if err != nil {
-                       ld.errorf("go: %v\n", err)
+                       ld.error(err)
                }
        }
+       ld.exitIfErrors(ctx)
+
+       updateGoVersion := func() {
+               goVersion := ld.goVersion()
+
+               if ld.requirements.pruning != workspace {
+                       var err error
+                       ld.requirements, err = convertPruning(ctx, ld.requirements, pruningForGoVersion(goVersion))
+                       if err != nil {
+                               ld.error(err)
+                               ld.exitIfErrors(ctx)
+                       }
+               }
+
+               // If the module's Go version omits go.sum entries for go.mod files for test
+               // dependencies of external packages, avoid loading those files in the first
+               // place.
+               ld.skipImportModFiles = ld.Tidy && gover.Compare(goVersion, gover.TidyGoModSumVersion) < 0
+
+               // If the module's go version explicitly predates the change in "all" for
+               // graph pruning, continue to use the older interpretation.
+               ld.allClosesOverTests = gover.Compare(goVersion, gover.NarrowAllVersion) < 0 && !ld.UseVendorAll
+       }
 
        for {
                ld.reset()
+               updateGoVersion()
 
                // Load the root packages and their imports.
                // Note: the returned roots can change on each iteration,
@@ -1092,7 +1123,7 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
 
                changed, err := ld.updateRequirements(ctx)
                if err != nil {
-                       ld.errorf("go: %v\n", err)
+                       ld.error(err)
                        break
                }
                if changed {
@@ -1109,7 +1140,11 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
                        break
                }
 
-               modAddedBy := ld.resolveMissingImports(ctx)
+               modAddedBy, err := ld.resolveMissingImports(ctx)
+               if err != nil {
+                       ld.error(err)
+                       break
+               }
                if len(modAddedBy) == 0 {
                        // The roots are stable, and we've resolved all of the missing packages
                        // that we can.
@@ -1120,7 +1155,7 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
                for m := range modAddedBy {
                        toAdd = append(toAdd, m)
                }
-               module.Sort(toAdd) // to make errors deterministic
+               gover.ModSort(toAdd) // to make errors deterministic
 
                // We ran updateRequirements before resolving missing imports and it didn't
                // make any changes, so we know that the requirement graph is already
@@ -1139,11 +1174,11 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
                        // are more descriptive.
                        if err, ok := err.(*mvs.BuildListError); ok {
                                if pkg := modAddedBy[err.Module()]; pkg != nil {
-                                       ld.errorf("go: %s: %v\n", pkg.stackText(), err.Err)
+                                       ld.error(fmt.Errorf("%s: %w", pkg.stackText(), err.Err))
                                        break
                                }
                        }
-                       ld.errorf("go: %v\n", err)
+                       ld.error(err)
                        break
                }
                if reflect.DeepEqual(rs.rootModules, ld.requirements.rootModules) {
@@ -1155,31 +1190,62 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
                }
                ld.requirements = rs
        }
-       base.ExitIfErrors() // TODO(bcmills): Is this actually needed?
+       ld.exitIfErrors(ctx)
 
        // Tidy the build list, if applicable, before we report errors.
        // (The process of tidying may remove errors from irrelevant dependencies.)
        if ld.Tidy {
                rs, err := tidyRoots(ctx, ld.requirements, ld.pkgs)
                if err != nil {
-                       ld.errorf("go: %v\n", err)
-                       base.ExitIfErrors()
+                       ld.error(err)
                } else {
+                       if ld.TidyGoVersion != "" {
+                               // Attempt to switch to the requested Go version. We have been using its
+                               // pruning and semantics all along, but there may have been — and may
+                               // still be — requirements on higher versions in the graph.
+                               tidy := overrideRoots(ctx, rs, []module.Version{{Path: "go", Version: ld.TidyGoVersion}})
+                               mg, err := tidy.Graph(ctx)
+                               if err != nil {
+                                       ld.error(err)
+                               }
+                               if v := mg.Selected("go"); v == ld.TidyGoVersion {
+                                       rs = tidy
+                               } else {
+                                       conflict := Conflict{
+                                               Path: mg.g.FindPath(func(m module.Version) bool {
+                                                       return m.Path == "go" && m.Version == v
+                                               })[1:],
+                                               Constraint: module.Version{Path: "go", Version: ld.TidyGoVersion},
+                                       }
+                                       msg := conflict.Summary()
+                                       if cfg.BuildV {
+                                               msg = conflict.String()
+                                       }
+                                       ld.error(errors.New(msg))
+                               }
+                       }
+
                        if ld.requirements.pruning == pruned {
-                               // We continuously add tidy roots to ld.requirements during loading, so at
-                               // this point the tidy roots should be a subset of the roots of
-                               // ld.requirements, ensuring that no new dependencies are brought inside
-                               // the graph-pruning horizon.
+                               // We continuously add tidy roots to ld.requirements during loading, so
+                               // at this point the tidy roots (other than possibly the "go" version
+                               // edited above) should be a subset of the roots of ld.requirements,
+                               // ensuring that no new dependencies are brought inside the
+                               // graph-pruning horizon.
                                // If that is not the case, there is a bug in the loading loop above.
                                for _, m := range rs.rootModules {
+                                       if m.Path == "go" && ld.TidyGoVersion != "" {
+                                               continue
+                                       }
                                        if v, ok := ld.requirements.rootSelected(m.Path); !ok || v != m.Version {
-                                               ld.errorf("go: internal error: a requirement on %v is needed but was not added during package loading\n", m)
-                                               base.ExitIfErrors()
+                                               ld.error(fmt.Errorf("internal error: a requirement on %v is needed but was not added during package loading (selected %s)", m, v))
                                        }
                                }
                        }
+
                        ld.requirements = rs
                }
+
+               ld.exitIfErrors(ctx)
        }
 
        // Report errors, if any.
@@ -1201,7 +1267,7 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
                        // Add importer go version information to import errors of standard
                        // library packages arising from newer releases.
                        if importer := pkg.stack; importer != nil {
-                               if v, ok := rawGoVersion.Load(importer.mod); ok && versionLess(LatestGoVersion(), v.(string)) {
+                               if v, ok := rawGoVersion.Load(importer.mod); ok && gover.Compare(gover.Local(), v.(string)) < 0 {
                                        stdErr.importerGoVersion = v.(string)
                                }
                        }
@@ -1216,19 +1282,13 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
                        continue
                }
 
-               ld.errorf("%s: %v\n", pkg.stackText(), pkg.err)
+               ld.error(fmt.Errorf("%s: %w", pkg.stackText(), pkg.err))
        }
 
        ld.checkMultiplePaths()
        return ld
 }
 
-// versionLess returns whether a < b according to semantic version precedence.
-// Both strings are interpreted as go version strings, e.g. "1.19".
-func versionLess(a, b string) bool {
-       return semver.Compare("v"+a, "v"+b) < 0
-}
-
 // updateRequirements ensures that ld.requirements is consistent with the
 // information gained from ld.pkgs.
 //
@@ -1272,10 +1332,19 @@ func (ld *loader) updateRequirements(ctx context.Context) (changed bool, err err
                }
        }
 
+       var maxTooNew *gover.TooNewError
        for _, pkg := range ld.pkgs {
+               if pkg.err != nil {
+                       if tooNew := (*gover.TooNewError)(nil); errors.As(pkg.err, &tooNew) {
+                               if maxTooNew == nil || gover.Compare(tooNew.GoVersion, maxTooNew.GoVersion) > 0 {
+                                       maxTooNew = tooNew
+                               }
+                       }
+               }
                if pkg.mod.Version != "" || !MainModules.Contains(pkg.mod.Path) {
                        continue
                }
+
                for _, dep := range pkg.imports {
                        if !dep.fromExternalModule() {
                                continue
@@ -1285,6 +1354,15 @@ func (ld *loader) updateRequirements(ctx context.Context) (changed bool, err err
                                // In workspace mode / workspace pruning mode, the roots are the main modules
                                // rather than the main module's direct dependencies. The check below on the selected
                                // roots does not apply.
+                               if cfg.BuildMod == "vendor" {
+                                       // In workspace vendor mode, we don't need to load the requirements of the workspace
+                                       // modules' dependencies so the check below doesn't work. But that's okay, because
+                                       // checking whether modules are required directly for the purposes of pruning is
+                                       // less important in vendor mode: if we were able to load the package, we have
+                                       // everything we need  to build the package, and dependencies' tests are pruned out
+                                       // of the vendor directory anyway.
+                                       continue
+                               }
                                if mg, err := rs.Graph(ctx); err != nil {
                                        return false, err
                                } else if _, ok := mg.RequiredBy(dep.mod); !ok {
@@ -1326,6 +1404,9 @@ func (ld *loader) updateRequirements(ctx context.Context) (changed bool, err err
                        direct[dep.mod.Path] = true
                }
        }
+       if maxTooNew != nil {
+               return false, maxTooNew
+       }
 
        var addRoots []module.Version
        if ld.Tidy {
@@ -1377,7 +1458,14 @@ func (ld *loader) updateRequirements(ctx context.Context) (changed bool, err err
                return false, err
        }
 
-       if rs != ld.requirements && !reflect.DeepEqual(rs.rootModules, ld.requirements.rootModules) {
+       if rs.GoVersion() != ld.requirements.GoVersion() {
+               // A change in the selected Go version may or may not affect the set of
+               // loaded packages, but in some cases it can change the meaning of the "all"
+               // pattern, the level of pruning in the module graph, and even the set of
+               // packages present in the standard library. If it has changed, it's best to
+               // reload packages once more to be sure everything is stable.
+               changed = true
+       } else if rs != ld.requirements && !reflect.DeepEqual(rs.rootModules, ld.requirements.rootModules) {
                // The roots of the module graph have changed in some way (not just the
                // "direct" markings). Check whether the changes affected any of the loaded
                // packages.
@@ -1424,7 +1512,7 @@ func (ld *loader) updateRequirements(ctx context.Context) (changed bool, err err
 // The newly-resolved packages are added to the addedModuleFor map, and
 // resolveMissingImports returns a map from each new module version to
 // the first missing package that module would resolve.
-func (ld *loader) resolveMissingImports(ctx context.Context) (modAddedBy map[module.Version]*loadPkg) {
+func (ld *loader) resolveMissingImports(ctx context.Context) (modAddedBy map[module.Version]*loadPkg, err error) {
        type pkgMod struct {
                pkg *loadPkg
                mod *module.Version
@@ -1485,6 +1573,24 @@ func (ld *loader) resolveMissingImports(ctx context.Context) (modAddedBy map[mod
        <-ld.work.Idle()
 
        modAddedBy = map[module.Version]*loadPkg{}
+
+       var (
+               maxTooNew    *gover.TooNewError
+               maxTooNewPkg *loadPkg
+       )
+       for _, pm := range pkgMods {
+               if tooNew := (*gover.TooNewError)(nil); errors.As(pm.pkg.err, &tooNew) {
+                       if maxTooNew == nil || gover.Compare(tooNew.GoVersion, maxTooNew.GoVersion) > 0 {
+                               maxTooNew = tooNew
+                               maxTooNewPkg = pm.pkg
+                       }
+               }
+       }
+       if maxTooNew != nil {
+               fmt.Fprintf(os.Stderr, "go: toolchain upgrade needed to resolve %s\n", maxTooNewPkg.path)
+               return nil, maxTooNew
+       }
+
        for _, pm := range pkgMods {
                pkg, mod := pm.pkg, *pm.mod
                if mod.Path == "" {
@@ -1497,7 +1603,7 @@ func (ld *loader) resolveMissingImports(ctx context.Context) (modAddedBy map[mod
                }
        }
 
-       return modAddedBy
+       return modAddedBy, nil
 }
 
 // pkg locates the *loadPkg for path, creating and queuing it for loading if
@@ -1662,15 +1768,15 @@ func (ld *loader) preloadRootModules(ctx context.Context, rootPkgs []string) (ch
        for m := range need {
                toAdd = append(toAdd, m)
        }
-       module.Sort(toAdd)
+       gover.ModSort(toAdd)
 
        rs, err := updateRoots(ctx, ld.requirements.direct, ld.requirements, nil, toAdd, ld.AssumeRootsImported)
        if err != nil {
                // We are missing some root dependency, and for some reason we can't load
                // enough of the module dependency graph to add the missing root. Package
                // loading is doomed to fail, so fail quickly.
-               ld.errorf("go: %v\n", err)
-               base.ExitIfErrors()
+               ld.error(err)
+               ld.exitIfErrors(ctx)
                return false
        }
        if reflect.DeepEqual(rs.rootModules, ld.requirements.rootModules) {
@@ -1874,14 +1980,15 @@ func (ld *loader) checkMultiplePaths() {
                if prev, ok := firstPath[src]; !ok {
                        firstPath[src] = mod.Path
                } else if prev != mod.Path {
-                       ld.errorf("go: %s@%s used for two different module paths (%s and %s)\n", src.Path, src.Version, prev, mod.Path)
+                       ld.error(fmt.Errorf("%s@%s used for two different module paths (%s and %s)", src.Path, src.Version, prev, mod.Path))
                }
        }
 }
 
 // checkTidyCompatibility emits an error if any package would be loaded from a
 // different module under rs than under ld.requirements.
-func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements) {
+func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements, compatVersion string) {
+       goVersion := rs.GoVersion()
        suggestUpgrade := false
        suggestEFlag := false
        suggestFixes := func() {
@@ -1898,13 +2005,13 @@ func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements)
                fmt.Fprintln(os.Stderr)
 
                goFlag := ""
-               if ld.GoVersion != MainModules.GoVersion() {
-                       goFlag = " -go=" + ld.GoVersion
+               if goVersion != MainModules.GoVersion() {
+                       goFlag = " -go=" + goVersion
                }
 
                compatFlag := ""
-               if ld.TidyCompatibleVersion != priorGoVersion(ld.GoVersion) {
-                       compatFlag = " -compat=" + ld.TidyCompatibleVersion
+               if compatVersion != gover.Prev(goVersion) {
+                       compatFlag = " -compat=" + compatVersion
                }
                if suggestUpgrade {
                        eDesc := ""
@@ -1913,16 +2020,16 @@ func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements)
                                eDesc = ", leaving some packages unresolved"
                                eFlag = " -e"
                        }
-                       fmt.Fprintf(os.Stderr, "To upgrade to the versions selected by go %s%s:\n\tgo mod tidy%s -go=%s && go mod tidy%s -go=%s%s\n", ld.TidyCompatibleVersion, eDesc, eFlag, ld.TidyCompatibleVersion, eFlag, ld.GoVersion, compatFlag)
+                       fmt.Fprintf(os.Stderr, "To upgrade to the versions selected by go %s%s:\n\tgo mod tidy%s -go=%s && go mod tidy%s -go=%s%s\n", compatVersion, eDesc, eFlag, compatVersion, eFlag, goVersion, compatFlag)
                } else if suggestEFlag {
                        // If some packages are missing but no package is upgraded, then we
                        // shouldn't suggest upgrading to the Go 1.16 versions explicitly — that
                        // wouldn't actually fix anything for Go 1.16 users, and *would* break
                        // something for Go 1.17 users.
-                       fmt.Fprintf(os.Stderr, "To proceed despite packages unresolved in go %s:\n\tgo mod tidy -e%s%s\n", ld.TidyCompatibleVersion, goFlag, compatFlag)
+                       fmt.Fprintf(os.Stderr, "To proceed despite packages unresolved in go %s:\n\tgo mod tidy -e%s%s\n", compatVersion, goFlag, compatFlag)
                }
 
-               fmt.Fprintf(os.Stderr, "If reproducibility with go %s is not needed:\n\tgo mod tidy%s -compat=%s\n", ld.TidyCompatibleVersion, goFlag, ld.GoVersion)
+               fmt.Fprintf(os.Stderr, "If reproducibility with go %s is not needed:\n\tgo mod tidy%s -compat=%s\n", compatVersion, goFlag, goVersion)
 
                // TODO(#46141): Populate the linked wiki page.
                fmt.Fprintf(os.Stderr, "For other options, see:\n\thttps://golang.org/doc/modules/pruning\n")
@@ -1930,8 +2037,10 @@ func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements)
 
        mg, err := rs.Graph(ctx)
        if err != nil {
-               ld.errorf("go: error loading go %s module graph: %v\n", ld.TidyCompatibleVersion, err)
+               ld.error(fmt.Errorf("error loading go %s module graph: %w", compatVersion, err))
+               ld.switchIfErrors(ctx)
                suggestFixes()
+               ld.exitIfErrors(ctx)
                return
        }
 
@@ -1990,7 +2099,7 @@ func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements)
                for _, m := range ld.requirements.rootModules {
                        if v := mg.Selected(m.Path); v != m.Version {
                                fmt.Fprintln(os.Stderr)
-                               base.Fatalf("go: internal error: failed to diagnose selected-version mismatch for module %s: go %s selects %s, but go %s selects %s\n\tPlease report this at https://golang.org/issue.", m.Path, ld.GoVersion, m.Version, ld.TidyCompatibleVersion, v)
+                               base.Fatalf("go: internal error: failed to diagnose selected-version mismatch for module %s: go %s selects %s, but go %s selects %s\n\tPlease report this at https://golang.org/issue.", m.Path, goVersion, m.Version, compatVersion, v)
                        }
                }
                return
@@ -2006,7 +2115,7 @@ func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements)
 
                if pkg.isTest() {
                        // We already did (or will) report an error for the package itself,
-                       // so don't report a duplicate (and more vebose) error for its test.
+                       // so don't report a duplicate (and more verbose) error for its test.
                        if _, ok := mismatches[pkg.testOf]; !ok {
                                base.Fatalf("go: internal error: mismatch recorded for test %s, but not its non-test package", pkg.path)
                        }
@@ -2031,12 +2140,12 @@ func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements)
                                        Path:    pkg.mod.Path,
                                        Version: mg.Selected(pkg.mod.Path),
                                }
-                               ld.errorf("%s loaded from %v,\n\tbut go %s would fail to locate it in %s\n", pkg.stackText(), pkg.mod, ld.TidyCompatibleVersion, selected)
+                               ld.error(fmt.Errorf("%s loaded from %v,\n\tbut go %s would fail to locate it in %s", pkg.stackText(), pkg.mod, compatVersion, selected))
                        } else {
                                if ambiguous := (*AmbiguousImportError)(nil); errors.As(mismatch.err, &ambiguous) {
                                        // TODO: Is this check needed?
                                }
-                               ld.errorf("%s loaded from %v,\n\tbut go %s would fail to locate it:\n\t%v\n", pkg.stackText(), pkg.mod, ld.TidyCompatibleVersion, mismatch.err)
+                               ld.error(fmt.Errorf("%s loaded from %v,\n\tbut go %s would fail to locate it:\n\t%v", pkg.stackText(), pkg.mod, compatVersion, mismatch.err))
                        }
 
                        suggestEFlag = true
@@ -2074,7 +2183,7 @@ func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements)
                        // pkg.err should have already been logged elsewhere — along with a
                        // stack trace — so log only the import path and non-error info here.
                        suggestUpgrade = true
-                       ld.errorf("%s failed to load from any module,\n\tbut go %s would load it from %v\n", pkg.path, ld.TidyCompatibleVersion, mismatch.mod)
+                       ld.error(fmt.Errorf("%s failed to load from any module,\n\tbut go %s would load it from %v", pkg.path, compatVersion, mismatch.mod))
 
                case pkg.mod != mismatch.mod:
                        // The package is loaded successfully by both Go versions, but from a
@@ -2082,15 +2191,16 @@ func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements)
                        // unnoticed!) variations in behavior between builds with different
                        // toolchains.
                        suggestUpgrade = true
-                       ld.errorf("%s loaded from %v,\n\tbut go %s would select %v\n", pkg.stackText(), pkg.mod, ld.TidyCompatibleVersion, mismatch.mod.Version)
+                       ld.error(fmt.Errorf("%s loaded from %v,\n\tbut go %s would select %v\n", pkg.stackText(), pkg.mod, compatVersion, mismatch.mod.Version))
 
                default:
                        base.Fatalf("go: internal error: mismatch recorded for package %s, but no differences found", pkg.path)
                }
        }
 
+       ld.switchIfErrors(ctx)
        suggestFixes()
-       base.ExitIfErrors()
+       ld.exitIfErrors(ctx)
 }
 
 // scanDir is like imports.ScanDir but elides known magic imports from the list,