]> 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 0a06b1b1253dcb0b80573ec4c92ca8783a735809..51eb141d4b38b827d1233c33b26619d51d15181f 100644 (file)
@@ -135,16 +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
-
-       // TidyGo, if true, indicates that GoVersion is from the tidy -go= flag.
-       TidyGo bool
+       TidyGoVersion string
 
        // Tags are the build tags in effect (as interpreted by the
        // cmd/go/internal/imports package).
@@ -237,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
@@ -341,7 +342,10 @@ func LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string) (ma
                }
        }
 
-       initialRS := loadModFile(ctx, &opts)
+       initialRS, err := loadModFile(ctx, &opts)
+       if err != nil {
+               base.Fatal(err)
+       }
 
        ld := loadFromRoots(ctx, loaderParams{
                PackageOpts:  opts,
@@ -366,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)
@@ -379,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 {
@@ -401,24 +404,30 @@ 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)
-
-                       for m := range keepSums(ctx, ld, compatRS, loadedZipSumsOnly) {
-                               keep[m] = true
+               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)
 
-               // Update the go.mod file's Go version if necessary.
-               if modFile := ModFile(); modFile != nil && ld.GoVersion != "" {
-                       mg, _ := ld.requirements.Graph(ctx)
-                       if ld.TidyGo {
-                               if v := mg.Selected("go"); gover.Compare(ld.GoVersion, v) < 0 {
-                                       base.Fatalf("go: cannot tidy -go=%v: dependencies require %v", ld.GoVersion, v)
-                               }
+                       for m := range keepSums(ctx, ld, compatRS, loadedZipSumsOnly) {
+                               keep[m] = true
                        }
-                       modFile.AddGoStmt(ld.GoVersion)
                }
 
                if !ExplicitWriteGoMod {
@@ -562,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)
                                }
@@ -768,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)
@@ -870,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:
@@ -1001,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(gover.Local(), ld.GoVersion) {
-                       ld.errorf("go: go.mod file indicates go %s, but maximum version supported by tidy is %s\n", ld.GoVersion, gover.Local())
-                       base.ExitIfErrors()
-               }
-       }
-
-       if ld.Tidy {
-               if ld.TidyCompatibleVersion == "" {
-                       ld.TidyCompatibleVersion = gover.Prev(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 gover.Compare(ld.GoVersion, gover.TidyGoModSumVersion) < 0 {
-                       ld.skipImportModFiles = true
-               }
-       }
-
-       if gover.Compare(ld.GoVersion, gover.NarrowAllVersion) < 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.
@@ -1053,13 +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)
                }
        }
-       base.ExitIfErrors() // or we will report them again
+       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,
@@ -1105,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 {
@@ -1122,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.
@@ -1152,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) {
@@ -1168,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.
@@ -1214,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(gover.Local(), v.(string)) {
+                               if v, ok := rawGoVersion.Load(importer.mod); ok && gover.Compare(gover.Local(), v.(string)) < 0 {
                                        stdErr.importerGoVersion = v.(string)
                                }
                        }
@@ -1229,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 gover.Compare(a, b) < 0
-}
-
 // updateRequirements ensures that ld.requirements is consistent with the
 // information gained from ld.pkgs.
 //
@@ -1285,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
@@ -1298,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 {
@@ -1339,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 {
@@ -1390,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.
@@ -1437,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
@@ -1498,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 == "" {
@@ -1510,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
@@ -1682,8 +1775,8 @@ func (ld *loader) preloadRootModules(ctx context.Context, rootPkgs []string) (ch
                // 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) {
@@ -1887,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() {
@@ -1911,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 != gover.Prev(ld.GoVersion) {
-                       compatFlag = " -compat=" + ld.TidyCompatibleVersion
+               if compatVersion != gover.Prev(goVersion) {
+                       compatFlag = " -compat=" + compatVersion
                }
                if suggestUpgrade {
                        eDesc := ""
@@ -1926,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")
@@ -1943,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
        }
 
@@ -2003,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
@@ -2044,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
@@ -2087,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
@@ -2095,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,