]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: make toolchain less special in MVS
authorRuss Cox <rsc@golang.org>
Wed, 31 May 2023 14:24:23 +0000 (10:24 -0400)
committerGopher Robot <gobot@golang.org>
Thu, 1 Jun 2023 02:51:07 +0000 (02:51 +0000)
We were using the omission of toolchain from the MVS graph
as a signal that toolchain was not mentioned on the go get line,
but not including it in the graph causes various problems,
and it may be reintroduced to the graph during operations like
pruning conversion, after which its presence is not a good signal
about whether it was mentioned on the go get command line.
Fix all this irregularity by explicitly telling WriteGoMod whether
the command line mentioned toolchain instead.

For #57001.

Change-Id: I74084637c177c30918fdb114a0d9030cdee7324e
Reviewed-on: https://go-review.googlesource.com/c/go/+/499575
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
src/cmd/go/internal/modget/get.go
src/cmd/go/internal/modload/buildlist.go
src/cmd/go/internal/modload/init.go
src/cmd/go/internal/modload/modfile.go
src/cmd/go/testdata/script/mod_indirect_main.txt
src/cmd/go/testdata/script/mod_skip_write.txt

index ca5f0dc7630b5f4154547fde97a3359d1f9bf7d5..e1c0e5b4f6295ac639a7d5b650bd796c9a86319d 100644 (file)
@@ -304,6 +304,14 @@ func runGet(ctx context.Context, cmd *base.Command, args []string) {
        }
 
        dropToolchain, queries := parseArgs(ctx, args)
+       opts := modload.WriteOpts{
+               DropToolchain: dropToolchain,
+       }
+       for _, q := range queries {
+               if q.pattern == "toolchain" {
+                       opts.ExplicitToolchain = true
+               }
+       }
 
        r := newResolver(ctx, queries)
        r.performLocalQueries(ctx)
@@ -372,14 +380,10 @@ func runGet(ctx context.Context, cmd *base.Command, args []string) {
        }
        r.checkPackageProblems(ctx, pkgPatterns)
 
-       if dropToolchain {
-               modload.OverrideRoots(ctx, []module.Version{{Path: "toolchain", Version: "none"}})
-       }
-
        // Everything succeeded. Update go.mod.
        oldReqs := reqsFromGoMod(modload.ModFile())
 
-       if err := modload.WriteGoMod(ctx, modload.WriteOpts{}); err != nil {
+       if err := modload.WriteGoMod(ctx, opts); err != nil {
                if tooNew, ok := err.(*gover.TooNewError); ok {
                        // This can happen for 'go get go@newversion'
                        // when all the required modules are old enough
index 517ecfcf6650d8b508cb97e6940d457fd44357a0..70092da92f2e7583846957f0bd5048e7a4bde0ce 100644 (file)
@@ -5,12 +5,6 @@
 package modload
 
 import (
-       "cmd/go/internal/base"
-       "cmd/go/internal/cfg"
-       "cmd/go/internal/gover"
-       "cmd/go/internal/mvs"
-       "cmd/go/internal/par"
-       "cmd/go/internal/slices"
        "context"
        "errors"
        "fmt"
@@ -18,10 +12,17 @@ import (
        "reflect"
        "runtime"
        "runtime/debug"
+       "slices"
        "strings"
        "sync"
        "sync/atomic"
 
+       "cmd/go/internal/base"
+       "cmd/go/internal/cfg"
+       "cmd/go/internal/gover"
+       "cmd/go/internal/mvs"
+       "cmd/go/internal/par"
+
        "golang.org/x/mod/module"
 )
 
@@ -91,6 +92,15 @@ type cachedGraph struct {
 // accept and/or return an explicit parameter.
 var requirements *Requirements
 
+func mustHaveGoRoot(roots []module.Version) {
+       for _, m := range roots {
+               if m.Path == "go" {
+                       return
+               }
+       }
+       panic("go: internal error: missing go root module")
+}
+
 // newRequirements returns a new requirement set with the given root modules.
 // The dependencies of the roots will be loaded lazily at the first call to the
 // Graph method.
@@ -102,6 +112,8 @@ var requirements *Requirements
 // If vendoring is in effect, the caller must invoke initVendor on the returned
 // *Requirements before any other method.
 func newRequirements(pruning modPruning, rootModules []module.Version, direct map[string]bool) *Requirements {
+       mustHaveGoRoot(rootModules)
+
        if pruning == workspace {
                return &Requirements{
                        pruning:        pruning,
@@ -114,7 +126,6 @@ func newRequirements(pruning modPruning, rootModules []module.Version, direct ma
        if workFilePath != "" && pruning != workspace {
                panic("in workspace mode, but pruning is not workspace in newRequirements")
        }
-
        for i, m := range rootModules {
                if m.Version == "" && MainModules.Contains(m.Path) {
                        panic(fmt.Sprintf("newRequirements called with untrimmed build list: rootModules[%v] is a main module", i))
@@ -122,17 +133,21 @@ func newRequirements(pruning modPruning, rootModules []module.Version, direct ma
                if m.Path == "" || m.Version == "" {
                        panic(fmt.Sprintf("bad requirement: rootModules[%v] = %v", i, m))
                }
-               if i > 0 {
-                       prev := rootModules[i-1]
-                       if prev.Path > m.Path || (prev.Path == m.Path && gover.ModCompare(m.Path, prev.Version, m.Version) > 0) {
-                               panic(fmt.Sprintf("newRequirements called with unsorted roots: %v", rootModules))
-                       }
-               }
+       }
+
+       // Allow unsorted root modules, because go and toolchain
+       // are treated as the final graph roots but not trimmed from the build list,
+       // so they always appear at the beginning of the list.
+       r := slices.Clip(slices.Clone(rootModules))
+       gover.ModSort(r)
+       if !reflect.DeepEqual(r, rootModules) {
+               fmt.Fprintln(os.Stderr, "RM", rootModules)
+               panic("unsorted")
        }
 
        rs := &Requirements{
                pruning:        pruning,
-               rootModules:    slices.Clip(rootModules),
+               rootModules:    rootModules,
                maxRootVersion: make(map[string]string, len(rootModules)),
                direct:         direct,
        }
@@ -157,7 +172,7 @@ func (rs *Requirements) String() string {
 func (rs *Requirements) initVendor(vendorList []module.Version) {
        rs.graphOnce.Do(func() {
                mg := &ModuleGraph{
-                       g: mvs.NewGraph(cmpVersion, MainModules.GraphRoots()),
+                       g: mvs.NewGraph(cmpVersion, MainModules.Versions()),
                }
 
                if MainModules.Len() != 1 {
@@ -278,6 +293,7 @@ var readModGraphDebugOnce sync.Once
 // Unlike LoadModGraph, readModGraph does not attempt to diagnose or update
 // inconsistent roots.
 func readModGraph(ctx context.Context, pruning modPruning, roots []module.Version, unprune map[module.Version]bool) (*ModuleGraph, error) {
+       mustHaveGoRoot(roots)
        if pruning == pruned {
                // Enable diagnostics for lazy module loading
                // (https://golang.org/ref/mod#lazy-loading) only if the module graph is
@@ -301,13 +317,20 @@ func readModGraph(ctx context.Context, pruning modPruning, roots []module.Versio
                })
        }
 
+       var graphRoots []module.Version
+       if inWorkspaceMode() {
+               graphRoots = roots
+       } else {
+               graphRoots = MainModules.Versions()
+       }
        var (
                mu       sync.Mutex // guards mg.g and hasError during loading
                hasError bool
                mg       = &ModuleGraph{
-                       g: mvs.NewGraph(cmpVersion, MainModules.GraphRoots()),
+                       g: mvs.NewGraph(cmpVersion, graphRoots),
                }
        )
+
        if pruning != workspace {
                if inWorkspaceMode() {
                        panic("pruning is not workspace in workspace mode")
@@ -380,6 +403,7 @@ func readModGraph(ctx context.Context, pruning modPruning, roots []module.Versio
                })
        }
 
+       mustHaveGoRoot(roots)
        for _, m := range roots {
                enqueue(m, pruning)
        }
@@ -789,6 +813,10 @@ func tidyPrunedRoots(ctx context.Context, mainModule module.Version, old *Requir
                roots = append(roots, module.Version{Path: "go", Version: v})
                pathIsRoot["go"] = true
        }
+       if v, ok := old.rootSelected("toolchain"); ok {
+               roots = append(roots, module.Version{Path: "toolchain", Version: v})
+               pathIsRoot["toolchain"] = true
+       }
        // We start by adding roots for every package in "all".
        //
        // Once that is done, we may still need to add more roots to cover upgraded or
@@ -1254,6 +1282,11 @@ func tidyUnprunedRoots(ctx context.Context, mainModule module.Version, old *Requ
        )
        if v, ok := old.rootSelected("go"); ok {
                keep = append(keep, module.Version{Path: "go", Version: v})
+               keptPath["go"] = true
+       }
+       if v, ok := old.rootSelected("toolchain"); ok {
+               keep = append(keep, module.Version{Path: "toolchain", Version: v})
+               keptPath["toolchain"] = true
        }
        for _, pkg := range pkgs {
                if !pkg.fromExternalModule() {
index efdd339998d3dcbcd1177776a59f21c87161b1fc..6377e19856316c69973758952380c5af8090c199 100644 (file)
@@ -90,6 +90,7 @@ type MainModuleSet struct {
        // versions are the module.Version values of each of the main modules.
        // For each of them, the Path fields are ordinary module paths and the Version
        // fields are empty strings.
+       // versions is clipped (len=cap).
        versions []module.Version
 
        // modRoot maps each module in versions to its absolute filesystem path.
@@ -108,7 +109,7 @@ type MainModuleSet struct {
 
        modContainingCWD module.Version
 
-       workFileGoVersion string
+       workFile *modfile.WorkFile
 
        workFileReplaceMap map[module.Version]module.Version
        // highest replaced version of each module path; empty string for wildcard-only replacements
@@ -133,18 +134,6 @@ func (mms *MainModuleSet) Versions() []module.Version {
        return mms.versions
 }
 
-// GraphRoots returns the graph roots for the main module set.
-// Callers should not modify the returned slice.
-// This function is the same as Versions except that in workspace
-// mode it adds a "go" version from the go.work file.
-func (mms *MainModuleSet) GraphRoots() []module.Version {
-       versions := mms.Versions()
-       if inWorkspaceMode() {
-               versions = append(slices.Clip(versions), module.Version{Path: "go", Version: mms.GoVersion()})
-       }
-       return versions
-}
-
 func (mms *MainModuleSet) Contains(path string) bool {
        if mms == nil {
                return false
@@ -232,19 +221,49 @@ func (mms *MainModuleSet) HighestReplaced() map[string]string {
 // GoVersion returns the go version set on the single module, in module mode,
 // or the go.work file in workspace mode.
 func (mms *MainModuleSet) GoVersion() string {
-       switch {
-       case inWorkspaceMode():
-               v := mms.workFileGoVersion
-               if v == "" {
-                       // Fall back to 1.18 for go.work files.
-                       v = "1.18"
-               }
-               return v
-       case mms == nil || len(mms.versions) == 0:
-               return "1.18"
-       default:
-               return modFileGoVersion(mms.ModFile(mms.mustGetSingleMainModule()))
+       if inWorkspaceMode() {
+               if mms.workFile != nil && mms.workFile.Go != nil {
+                       return mms.workFile.Go.Version
+               }
+               return defaultGoWorkVersion
+       }
+       if mms != nil && len(mms.versions) == 1 {
+               f := mms.ModFile(mms.mustGetSingleMainModule())
+               if f == nil {
+                       // Special case: we are outside a module, like 'go run x.go'.
+                       // Assume the local Go version.
+                       // TODO(#49228): Clean this up; see loadModFile.
+                       return gover.Local()
+               }
+               if f.Go != nil {
+                       return f.Go.Version
+               }
        }
+       return defaultGoModVersion
+}
+
+// Toolchain returns the toolchain set on the single module, in module mode,
+// or the go.work file in workspace mode.
+func (mms *MainModuleSet) Toolchain() string {
+       if inWorkspaceMode() {
+               if mms.workFile != nil && mms.workFile.Toolchain != nil {
+                       return mms.workFile.Toolchain.Name
+               }
+               return "go" + mms.GoVersion()
+       }
+       if mms != nil && len(mms.versions) == 1 {
+               f := mms.ModFile(mms.mustGetSingleMainModule())
+               if f == nil {
+                       // Special case: we are outside a module, like 'go run x.go'.
+                       // Assume the local Go version.
+                       // TODO(#49228): Clean this up; see loadModFile.
+                       return gover.LocalToolchain()
+               }
+               if f.Toolchain != nil {
+                       return f.Toolchain.Name
+               }
+       }
+       return "go" + mms.GoVersion()
 }
 
 func (mms *MainModuleSet) WorkFileReplaceMap() map[module.Version]module.Version {
@@ -759,14 +778,18 @@ func loadModFile(ctx context.Context, opts *PackageOpts) *Requirements {
                // make MainModules.Len() == 0 mean that we're in module mode but not inside
                // any module.
                mainModule := module.Version{Path: "command-line-arguments"}
-               MainModules = makeMainModules([]module.Version{mainModule}, []string{""}, []*modfile.File{nil}, []*modFileIndex{nil}, "", nil)
+               MainModules = makeMainModules([]module.Version{mainModule}, []string{""}, []*modfile.File{nil}, []*modFileIndex{nil}, nil)
                goVersion := gover.Local()
                rawGoVersion.Store(mainModule, goVersion)
                pruning := pruningForGoVersion(goVersion)
                if inWorkspaceMode() {
                        pruning = workspace
                }
-               requirements = newRequirements(pruning, nil, nil)
+               roots := []module.Version{
+                       {Path: "go", Version: gover.Local()},
+                       {Path: "toolchain", Version: gover.LocalToolchain()},
+               }
+               requirements = newRequirements(pruning, roots, nil)
                if cfg.BuildMod == "vendor" {
                        // For issue 56536: Some users may have GOFLAGS=-mod=vendor set.
                        // Make sure it behaves as though the fake module is vendored
@@ -804,15 +827,7 @@ func loadModFile(ctx context.Context, opts *PackageOpts) *Requirements {
                }
        }
 
-       var wfGoVersion string
-       var wfReplace []*modfile.Replace
-       if workFile != nil && workFile.Go != nil {
-               wfGoVersion = workFile.Go.Version
-       }
-       if workFile != nil {
-               wfReplace = workFile.Replace
-       }
-       MainModules = makeMainModules(mainModules, modRoots, modFiles, indices, wfGoVersion, wfReplace)
+       MainModules = makeMainModules(mainModules, modRoots, modFiles, indices, workFile)
        setDefaultBuildMod() // possibly enable automatic vendoring
        rs := requirementsFromModFiles(ctx, workFile, modFiles, opts)
 
@@ -869,7 +884,7 @@ func loadModFile(ctx context.Context, opts *PackageOpts) *Requirements {
                                }
                        }
                } else {
-                       rawGoVersion.Store(mainModule, modFileGoVersion(MainModules.ModFile(mainModule)))
+                       rawGoVersion.Store(mainModule, defaultGoModVersion)
                }
        }
 
@@ -923,7 +938,7 @@ func CreateModFile(ctx context.Context, modPath string) {
        fmt.Fprintf(os.Stderr, "go: creating new go.mod: module %s\n", modPath)
        modFile := new(modfile.File)
        modFile.AddModuleStmt(modPath)
-       MainModules = makeMainModules([]module.Version{modFile.Module.Mod}, []string{modRoot}, []*modfile.File{modFile}, []*modFileIndex{nil}, "", nil)
+       MainModules = makeMainModules([]module.Version{modFile.Module.Mod}, []string{modRoot}, []*modfile.File{modFile}, []*modFileIndex{nil}, nil)
        addGoStmt(modFile, modFile.Module.Mod, gover.Local()) // Add the go directive before converted module requirements.
 
        convertedFrom, err := convertLegacyConfig(modFile, modRoot)
@@ -1058,7 +1073,7 @@ func AllowMissingModuleImports() {
 
 // makeMainModules creates a MainModuleSet and associated variables according to
 // the given main modules.
-func makeMainModules(ms []module.Version, rootDirs []string, modFiles []*modfile.File, indices []*modFileIndex, workFileGoVersion string, workFileReplaces []*modfile.Replace) *MainModuleSet {
+func makeMainModules(ms []module.Version, rootDirs []string, modFiles []*modfile.File, indices []*modFileIndex, workFile *modfile.WorkFile) *MainModuleSet {
        for _, m := range ms {
                if m.Version != "" {
                        panic("mainModulesCalled with module.Version with non empty Version field: " + fmt.Sprintf("%#v", m))
@@ -1066,15 +1081,19 @@ func makeMainModules(ms []module.Version, rootDirs []string, modFiles []*modfile
        }
        modRootContainingCWD := findModuleRoot(base.Cwd())
        mainModules := &MainModuleSet{
-               versions:           slices.Clip(ms),
-               inGorootSrc:        map[module.Version]bool{},
-               pathPrefix:         map[module.Version]string{},
-               modRoot:            map[module.Version]string{},
-               modFiles:           map[module.Version]*modfile.File{},
-               indices:            map[module.Version]*modFileIndex{},
-               workFileGoVersion:  workFileGoVersion,
-               workFileReplaceMap: toReplaceMap(workFileReplaces),
-               highestReplaced:    map[string]string{},
+               versions:        slices.Clip(ms),
+               inGorootSrc:     map[module.Version]bool{},
+               pathPrefix:      map[module.Version]string{},
+               modRoot:         map[module.Version]string{},
+               modFiles:        map[module.Version]*modfile.File{},
+               indices:         map[module.Version]*modFileIndex{},
+               highestReplaced: map[string]string{},
+               workFile:        workFile,
+       }
+       var workFileReplaces []*modfile.Replace
+       if workFile != nil {
+               workFileReplaces = workFile.Replace
+               mainModules.workFileReplaceMap = toReplaceMap(workFile.Replace)
        }
        mainModulePaths := make(map[string]bool)
        for _, m := range ms {
@@ -1162,17 +1181,17 @@ func requirementsFromModFiles(ctx context.Context, workFile *modfile.WorkFile, m
        var roots []module.Version
        direct := map[string]bool{}
        var pruning modPruning
+       var goVersion, toolchain string
        if inWorkspaceMode() {
                pruning = workspace
                roots = make([]module.Version, len(MainModules.Versions()), 2+len(MainModules.Versions()))
                copy(roots, MainModules.Versions())
-               // Note: Ignoring the 'go' line in the main modules during mod tidy. See note below.
-               if workFile.Go != nil && (opts == nil || !opts.TidyGo) {
-                       roots = append(roots, module.Version{Path: "go", Version: workFile.Go.Version})
-                       direct["go"] = true
+               if workFile.Go != nil {
+                       goVersion = workFile.Go.Version
+               }
+               if workFile.Toolchain != nil {
+                       toolchain = workFile.Toolchain.Name
                }
-               // Do not add toolchain to roots.
-               // We only want to see it in roots if it is on the command line.
        } else {
                pruning = pruningForGoVersion(MainModules.GoVersion())
                if len(modFiles) != 1 {
@@ -1196,16 +1215,30 @@ func requirementsFromModFiles(ctx context.Context, workFile *modfile.WorkFile, m
                                direct[r.Mod.Path] = true
                        }
                }
-               // Note: Ignoring the 'go' line in the main modules during mod tidy -go=
-               // so that we can find out the implied minimum go line from the
-               // dependencies instead. If it is higher than the -go= flag, we report an error in LoadPackages.
-               if modFile.Go != nil && (opts == nil || !opts.TidyGo) {
-                       roots = append(roots, module.Version{Path: "go", Version: modFile.Go.Version})
-                       direct["go"] = true
+               if modFile.Go != nil {
+                       goVersion = modFile.Go.Version
+               }
+               if modFile.Toolchain != nil {
+                       toolchain = modFile.Toolchain.Name
                }
-               // Do not add "toolchain" to roots.
-               // We only want to see it in roots if it is on the command line.
        }
+
+       // Add explicit go and toolchain versions, inferring as needed.
+       if opts != nil && opts.TidyGo {
+               goVersion = opts.GoVersion
+       }
+       if goVersion == "" {
+               goVersion = defaultGoModVersion
+       }
+       roots = append(roots, module.Version{Path: "go", Version: goVersion})
+       direct["go"] = true
+
+       if toolchain == "" {
+               toolchain = "go" + goVersion
+       }
+       roots = append(roots, module.Version{Path: "toolchain", Version: toolchain})
+       direct["toolchain"] = true
+
        gover.ModSort(roots)
        rs := newRequirements(pruning, roots, direct)
        return rs
@@ -1517,6 +1550,8 @@ func findImportComment(file string) string {
 
 // WriteOpts control the behavior of WriteGoMod.
 type WriteOpts struct {
+       DropToolchain     bool // go get toolchain@none
+       ExplicitToolchain bool // go get has set explicit toolchain version
 }
 
 // WriteGoMod writes the current build list back to go.mod.
@@ -1554,11 +1589,10 @@ func commitRequirements(ctx context.Context, opts WriteOpts) (err error) {
 
        var list []*modfile.Require
        toolchain := ""
-       wroteGo := false
+       goVersion := ""
        for _, m := range requirements.rootModules {
                if m.Path == "go" {
-                       wroteGo = true
-                       forceGoStmt(modFile, mainModule, m.Version)
+                       goVersion = m.Version
                        continue
                }
                if m.Path == "toolchain" {
@@ -1571,56 +1605,50 @@ func commitRequirements(ctx context.Context, opts WriteOpts) (err error) {
                })
        }
 
-       var oldToolchain string
-       if modFile.Toolchain != nil {
-               oldToolchain = modFile.Toolchain.Name
-       }
-       oldToolVers := gover.FromToolchain(oldToolchain)
-
-       // Update go and toolchain lines.
-       toolVers := gover.FromToolchain(toolchain)
-
-       // Set go version if missing.
-       if modFile.Go == nil || modFile.Go.Version == "" {
-               wroteGo = true
-               v := modFileGoVersion(modFile)
-               if toolVers != "" && gover.Compare(v, toolVers) > 0 {
-                       v = toolVers
-               }
-               modFile.AddGoStmt(v)
+       // Update go line.
+       // Every MVS graph we consider should have go as a root,
+       // and toolchain is either implied by the go line or explicitly a root.
+       if goVersion == "" {
+               base.Fatalf("go: internal error: missing go root module in WriteGoMod")
        }
-       if gover.Compare(modFile.Go.Version, gover.Local()) > 0 {
+       if gover.Compare(goVersion, gover.Local()) > 0 {
                // We cannot assume that we know how to update a go.mod to a newer version.
-               return &gover.TooNewError{What: "updating go.mod", GoVersion: modFile.Go.Version}
+               return &gover.TooNewError{What: "updating go.mod", GoVersion: goVersion}
        }
-
-       // If we update the go line and don't have an explicit instruction
-       // for what to write in toolchain, make sure toolchain is at least our local version,
-       // for reproducibility.
-       if wroteGo && toolchain == "" && gover.Compare(oldToolVers, gover.Local()) < 0 && gover.Compare(modFile.Go.Version, GoStrictVersion) >= 0 {
-               toolVers = gover.Local()
-               toolchain = "go" + toolVers
+       wroteGo := false
+       if modFile.Go == nil || modFile.Go.Version != goVersion {
+               alwaysUpdate := cfg.BuildMod == "mod" || cfg.CmdName == "mod tidy" || cfg.CmdName == "get"
+               if modFile.Go == nil && goVersion == defaultGoModVersion && !alwaysUpdate {
+                       // The go.mod has no go line, the implied default Go version matches
+                       // what we've computed for the graph, and we're not in one of the
+                       // traditional go.mod-updating programs, so leave it alone.
+               } else {
+                       wroteGo = true
+                       forceGoStmt(modFile, mainModule, goVersion)
+               }
        }
-
-       // Default to old toolchain.
        if toolchain == "" {
-               toolchain = oldToolchain
-               toolVers = oldToolVers
+               toolchain = "go" + goVersion
        }
-       if toolchain == "none" {
-               toolchain = ""
+
+       // For reproducibility, if we are writing a new go line,
+       // and we're not explicitly modifying the toolchain line with 'go get toolchain@something',
+       // and the toolchain running right now is newer than the current toolchain line,
+       // then update the toolchain line to record the newer toolchain.
+       toolVers := gover.FromToolchain(toolchain)
+       if wroteGo && !opts.DropToolchain && !opts.ExplicitToolchain && gover.Compare(gover.Local(), toolVers) > 0 {
+               toolchain = "go" + gover.Local()
        }
 
-       // Remove or add toolchain as needed.
-       // If toolchain is older than go version, drop it.
-       if toolchain == "" || gover.Compare(modFile.Go.Version, toolVers) >= 0 {
+       if opts.DropToolchain || toolchain == "go"+goVersion {
+               // go get toolchain@none or toolchain matches go line; drop it.
                modFile.DropToolchainStmt()
        } else {
                modFile.AddToolchainStmt(toolchain)
        }
 
        // Update require blocks.
-       if gover.Compare(modFileGoVersion(modFile), separateIndirectVersion) < 0 {
+       if gover.Compare(goVersion, separateIndirectVersion) < 0 {
                modFile.SetRequire(list)
        } else {
                modFile.SetRequireSeparateIndirect(list)
index e0261d2c1f518b0ef5f186dfb02b95f1bd9f4c9e..b2bae6255bc96c5f4be2ea6ef51544fbb811ae03 100644 (file)
@@ -33,6 +33,22 @@ const (
        // tests outside of the main module.
        narrowAllVersion = "1.16"
 
+       // defaultGoModVersion is the Go version to assume for go.mod files
+       // that do not declare a Go version. The go command has been
+       // writing go versions to modules since Go 1.12, so a go.mod
+       // without a version is either very old or recently hand-written.
+       // Since we can't tell which, we have to assume it's very old.
+       // The semantics of the go.mod changed at Go 1.17 to support
+       // graph pruning. If see a go.mod without a go line, we have to
+       // assume Go 1.16 so that we interpret the requirements correctly.
+       // Note that this default must stay at Go 1.16; it cannot be moved forward.
+       defaultGoModVersion = "1.16"
+
+       // defaultGoWorkVersion is the Go version to assume for go.work files
+       // that do not declare a Go version. Workspaces were added in Go 1.18,
+       // so use that.
+       defaultGoWorkVersion = "1.18"
+
        // ExplicitIndirectVersion is the Go version at which a
        // module's go.mod file is expected to list explicit requirements on every
        // module that provides any package transitively imported by that module.
@@ -91,27 +107,6 @@ func ReadModFile(gomod string, fix modfile.VersionFixer) (data []byte, f *modfil
        return data, f, err
 }
 
-// modFileGoVersion returns the (non-empty) Go version at which the requirements
-// in modFile are interpreted, or the latest Go version if modFile is nil.
-func modFileGoVersion(modFile *modfile.File) string {
-       if modFile == nil {
-               return gover.Local()
-       }
-       if modFile.Go == nil || modFile.Go.Version == "" {
-               // The main module necessarily has a go.mod file, and that file lacks a
-               // 'go' directive. The 'go' command has been adding that directive
-               // automatically since Go 1.12, so this module either dates to Go 1.11 or
-               // has been erroneously hand-edited.
-               //
-               // The semantics of the go.mod file are more-or-less the same from Go 1.11
-               // through Go 1.16, changing at 1.17 to support module graph pruning.
-               // So even though a go.mod file without a 'go' directive is theoretically a
-               // Go 1.11 file, scripts may assume that it ends up as a Go 1.16 module.
-               return "1.16"
-       }
-       return modFile.Go.Version
-}
-
 // A modFileIndex is an index of data corresponding to a modFile
 // at a specific point in time.
 type modFileIndex struct {
@@ -510,19 +505,6 @@ func (i *modFileIndex) modFileIsDirty(modFile *modfile.File) bool {
                toolchain = modFile.Toolchain.Name
        }
 
-       // go.mod files did not always require a 'go' version, so do not error out
-       // if one is missing — we may be inside an older module
-       // and want to bias toward providing useful behavior.
-       // go lines are required if we need to declare version 1.17 or later.
-       // Note that as of CL 303229, a missing go directive implies 1.16,
-       // not “the latest Go version”.
-       if goV != i.goVersion && i.goVersion == "" && cfg.BuildMod != "mod" && gover.Compare(goV, "1.17") < 0 {
-               goV = ""
-               if toolchain != i.toolchain && i.toolchain == "" {
-                       toolchain = ""
-               }
-       }
-
        if goV != i.goVersion ||
                toolchain != i.toolchain ||
                len(modFile.Require) != len(i.require) ||
@@ -703,7 +685,8 @@ func rawGoModSummary(m module.Version) (*modFileSummary, error) {
        if gover.IsToolchain(m.Path) {
                if m.Path == "go" {
                        // Declare that go 1.2.3 requires toolchain 1.2.3,
-                       // so that go get knows that downgrading toolchain implies downgrading go.
+                       // so that go get knows that downgrading toolchain implies downgrading go
+                       // and similarly upgrading go requires upgrading the toolchain.
                        return &modFileSummary{module: m, require: []module.Version{{Path: "toolchain", Version: "go" + m.Version}}}, nil
                }
                return &modFileSummary{module: m}, nil
index 43aaa390645ac0af1620b78088fd9c20f1e46678..22b344f8668dfe2514cb0bff692a614b623d0fd4 100644 (file)
@@ -62,6 +62,7 @@ golang.org/issue/pkg v0.1.0 => ./pkg-v0.1.0
 -- graph.txt --
 golang.org/issue/root go@1.12
 golang.org/issue/root golang.org/issue/mirror@v0.1.0
+golang.org/issue/root toolchain@go1.12
 go@1.12 toolchain@go1.12
 golang.org/issue/mirror@v0.1.0 golang.org/issue/root@v0.1.0
 golang.org/issue/root@v0.1.0 golang.org/issue/pkg@v0.1.0
index 14b1c3728e3682775c7fa7401aee58950d0bc8ae..1850cdf5fdbb0056f81bc46084de891809c57fa3 100644 (file)
@@ -84,6 +84,7 @@ m golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c
 m rsc.io/quote@v1.5.2
 m rsc.io/sampler@v1.3.0
 m rsc.io/testonly@v1.0.0
+m toolchain@go1.18
 go@1.18 toolchain@go1.18
 rsc.io/quote@v1.5.2 rsc.io/sampler@v1.3.0
 rsc.io/sampler@v1.3.0 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c