When we do 'go get', the Go version can change now.
That means we need to do the pruning conversions that
until now have only been necessary in go mod tidy -go=version.
We may also need to upgrade the toolchain in order to load enough o
the module graph to finish the edit, so we should let a TooNewError
bubble up to the caller instead of trying to downgrade the affected
module to avoid the error.
Revised from CL 498120.
For #57001.
Change-Id: Ic8994737eca4ed61ccc093a69e46f5a6caa8be87
Reviewed-on: https://go-review.googlesource.com/c/go/+/498267
Reviewed-by: Russ Cox <rsc@golang.org>
Auto-Submit: Bryan Mills <bcmills@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
fmt.Fprintf(os.Stderr, "go: removed %s %s\n", c.path, c.old)
} else if gover.ModCompare(c.path, c.new, c.old) > 0 {
fmt.Fprintf(os.Stderr, "go: upgraded %s %s => %s\n", c.path, c.old, c.new)
+ if c.path == "go" && gover.Compare(c.old, gover.ExplicitIndirectVersion) < 0 && gover.Compare(c.new, gover.ExplicitIndirectVersion) >= 0 {
+ fmt.Fprintf(os.Stderr, "\tnote: expanded dependencies to upgrade to go %s or higher; run 'go mod tidy' to clean up\n", gover.ExplicitIndirectVersion)
+ }
+
} else {
fmt.Fprintf(os.Stderr, "go: downgraded %s %s => %s\n", c.path, c.old, c.new)
}
}
}
- 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))
+ if pruning != workspace {
+ if workFilePath != "" {
+ panic("in workspace mode, but pruning is not workspace in newRequirements")
}
- if m.Path == "" || m.Version == "" {
- panic(fmt.Sprintf("bad requirement: rootModules[%v] = %v", i, m))
+ 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))
+ }
+ if m.Path == "" || m.Version == "" {
+ panic(fmt.Sprintf("bad requirement: rootModules[%v] = %v", i, m))
+ }
}
}
- // 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: rootModules,
direct: direct,
}
- for _, m := range rootModules {
+ for i, m := range rootModules {
+ 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))
+ }
+ }
+
if v, ok := rs.maxRootVersion[m.Path]; ok && gover.ModCompare(m.Path, v, m.Version) >= 0 {
continue
}
rs.maxRootVersion[m.Path] = m.Version
}
+
+ if rs.maxRootVersion["go"] == "" {
+ panic(`newRequirements called without a "go" version`)
+ }
return rs
}
})
}
+// GoVersion returns the Go language version for the Requirements.
+func (rs *Requirements) GoVersion() string {
+ v, _ := rs.rootSelected("go")
+ return v
+}
+
// rootSelected returns the version of the root dependency with the given module
// path, or the zero module.Version and ok=false if the module is not a root
// dependency.
"cmd/go/internal/mvs"
"cmd/go/internal/par"
"context"
+ "errors"
"fmt"
"maps"
"os"
panic("editRequirements cannot edit workspace requirements")
}
+ orig := rs
+ // If we already know what go version we will end up on after the edit, and
+ // the pruning for that version is different, go ahead and apply it now.
+ //
+ // If we are changing from pruned to unpruned, then we MUST check the unpruned
+ // graph for conflicts from the start. (Checking only for pruned conflicts
+ // would miss some that would be introduced later.)
+ //
+ // If we are changing from unpruned to pruned, then we would like to avoid
+ // unnecessary downgrades due to conflicts that would be pruned out of the
+ // final graph anyway.
+ //
+ // Note that even if we don't find a go version in mustSelect, it is possible
+ // that we will switch from unpruned to pruned (but not the other way around!)
+ // after applying the edits if we find a dependency that requires a high
+ // enough go version to trigger an upgrade.
+ rootPruning := orig.pruning
+ for _, m := range mustSelect {
+ if m.Path == "go" {
+ rootPruning = pruningForGoVersion(m.Version)
+ break
+ } else if m.Path == "toolchain" && pruningForGoVersion(gover.FromToolchain(m.Version)) == unpruned {
+ // We don't know exactly what go version we will end up at, but we know
+ // that it must be a version supported by the requested toolchain, and
+ // that toolchain does not support pruning.
+ //
+ // TODO(bcmills): 'go get' ought to reject explicit toolchain versions
+ // older than gover.GoStrictVersion. Once that is fixed, is this still
+ // needed?
+ rootPruning = unpruned
+ break
+ }
+ }
+
+ if rootPruning != rs.pruning {
+ rs, err = convertPruning(ctx, rs, rootPruning)
+ if err != nil {
+ return orig, false, err
+ }
+ }
+
// selectedRoot records the edited version (possibly "none") for each module
// path that would be a root in the edited requirements.
var selectedRoot map[string]string // module path → edited version
- if rs.pruning == pruned {
+ if rootPruning == pruned {
selectedRoot = maps.Clone(rs.maxRootVersion)
} else {
// In a module without graph pruning, modules that provide packages imported
if err != nil {
// If we couldn't load the graph, we don't know what its requirements were
// to begin with, so we can't edit those requirements in a coherent way.
- return rs, false, err
+ return orig, false, err
}
bl := mg.BuildList()[MainModules.Len():]
selectedRoot = make(map[string]string, len(bl))
// of every root. The upgraded roots are in addition to the original
// roots, so we will have enough information to trace a path to each
// conflict we discover from one or more of the original roots.
- mg, upgradedRoots, err := extendGraph(ctx, rs, roots, selectedRoot)
+ mg, upgradedRoots, err := extendGraph(ctx, rootPruning, roots, selectedRoot)
if err != nil {
- if mg == nil {
- return rs, false, err
+ var tooNew *gover.TooNewError
+ if mg == nil || errors.As(err, &tooNew) {
+ return orig, false, err
}
// We're about to walk the entire extended module graph, so we will find
// any error then — and we will either try to resolve it by downgrading
// the extended module graph.
extendedRootPruning := make(map[module.Version]modPruning, len(roots)+len(upgradedRoots))
findPruning := func(m module.Version) modPruning {
- if rs.pruning == pruned {
+ if rootPruning == pruned {
summary, _ := mg.loadCache.Get(m)
if summary != nil && summary.pruning == unpruned {
return unpruned
}
}
- return rs.pruning
+ return rootPruning
}
for _, m := range roots {
extendedRootPruning[m] = findPruning(m)
// the edit. We want to make sure we consider keeping it as-is,
// even if it wouldn't normally be included. (For example, it might
// be a pseudo-version or pre-release.)
- origMG, _ := rs.Graph(ctx)
+ origMG, _ := orig.Graph(ctx)
origV := origMG.Selected(m.Path)
if conflict.Err != nil && origV == m.Version {
prev.Version = origV
} else if err != nil {
// We don't know the next downgrade to try. Give up.
- return rs, false, err
+ return orig, false, err
}
if rejectedRoot[prev] {
// We already rejected prev in a previous round.
// To ensure that this algorithm terminates, don't try it again.
continue
}
- pruning := rs.pruning
+ pruning := rootPruning
if pruning == pruned {
if summary, err := mg.loadCache.Get(m); err == nil {
pruning = summary.pruning
break
}
if len(conflicts) > 0 {
- return rs, false, &ConstraintError{Conflicts: conflicts}
+ return orig, false, &ConstraintError{Conflicts: conflicts}
}
- if rs.pruning == unpruned {
+ if rootPruning == unpruned {
// An unpruned go.mod file lists only a subset of the requirements needed
// for building packages. Figure out which requirements need to be explicit.
var rootPaths []string
}
}
- changed = !slices.Equal(roots, rs.rootModules)
+ changed = rootPruning != orig.pruning || !slices.Equal(roots, orig.rootModules)
if !changed {
// Because the roots we just computed are unchanged, the entire graph must
// be the same as it was before. Save the original rs, since we have
// probably already loaded its requirement graph.
- return rs, false, nil
+ return orig, false, nil
}
// A module that is not even in the build list necessarily cannot provide
direct[m.Path] = true
}
}
- return newRequirements(rs.pruning, roots, direct), changed, nil
+ edited = newRequirements(rootPruning, roots, direct)
+
+ // If we ended up adding a dependency that upgrades our go version far enough
+ // to activate pruning, we must convert the edited Requirements in order to
+ // avoid dropping transitive dependencies from the build list the next time
+ // someone uses the updated go.mod file.
+ //
+ // Note that it isn't possible to go in the other direction (from pruned to
+ // unpruned) unless the "go" or "toolchain" module is explicitly listed in
+ // mustSelect, which we already handled at the very beginning of the edit.
+ // That is because the virtual "go" module only requires a "toolchain",
+ // and the "toolchain" module never requires anything else, which means that
+ // those two modules will never be downgraded due to a conflict with any other
+ // constraint.
+ if rootPruning == unpruned {
+ if v, ok := edited.rootSelected("go"); ok && pruningForGoVersion(v) == pruned {
+ // Since we computed the edit with the unpruned graph, and the pruned
+ // graph is a strict subset of the unpruned graph, this conversion
+ // preserves the exact (edited) build list that we already computed.
+ //
+ // However, it does that by shoving the whole build list into the roots of
+ // the graph. 'go get' will check for that sort of transition and log a
+ // message reminding the user how to clean up this mess we're about to
+ // make. 😅
+ edited, err = convertPruning(ctx, edited, pruned)
+ if err != nil {
+ return orig, false, err
+ }
+ }
+ }
+ return edited, true, nil
}
// extendGraph loads the module graph from roots, and iteratively extends it by
// The extended graph is useful for diagnosing version conflicts: for each
// selected module version, it can provide a complete path of requirements from
// some root to that version.
-func extendGraph(ctx context.Context, rs *Requirements, roots []module.Version, selectedRoot map[string]string) (mg *ModuleGraph, upgradedRoot map[module.Version]bool, err error) {
+func extendGraph(ctx context.Context, rootPruning modPruning, roots []module.Version, selectedRoot map[string]string) (mg *ModuleGraph, upgradedRoot map[module.Version]bool, err error) {
for {
- mg, err = readModGraph(ctx, rs.pruning, roots, upgradedRoot)
+ mg, err = readModGraph(ctx, rootPruning, roots, upgradedRoot)
// We keep on going even if err is non-nil until we reach a steady state.
// (Note that readModGraph returns a non-nil *ModuleGraph even in case of
// errors.) The caller may be able to fix the errors by adjusting versions,
// so we really want to return as complete a result as we can.
- if rs.pruning == unpruned {
+ if rootPruning == unpruned {
// Everything is already unpruned, so there isn't anything we can do to
// extend it further.
break
// any module.
mainModule := module.Version{Path: "command-line-arguments"}
MainModules = makeMainModules([]module.Version{mainModule}, []string{""}, []*modfile.File{nil}, []*modFileIndex{nil}, nil)
- goVersion := gover.Local()
- rawGoVersion.Store(mainModule, goVersion)
- pruning := pruningForGoVersion(goVersion)
+ var (
+ goVersion string
+ pruning modPruning
+ roots []module.Version
+ direct = map[string]bool{"go": true}
+ )
if inWorkspaceMode() {
+ // Since we are in a workspace, the Go version for the synthetic
+ // "command-line-arguments" module must not exceed the Go version
+ // for the workspace.
+ goVersion = MainModules.GoVersion()
pruning = workspace
+ roots = []module.Version{
+ mainModule,
+ {Path: "go", Version: goVersion},
+ {Path: "toolchain", Version: gover.LocalToolchain()},
+ }
+ } else {
+ goVersion = gover.Local()
+ pruning = pruningForGoVersion(goVersion)
+ roots = []module.Version{
+ {Path: "go", Version: goVersion},
+ {Path: "toolchain", Version: gover.LocalToolchain()},
+ }
}
- roots := []module.Version{
- {Path: "go", Version: gover.Local()},
- {Path: "toolchain", Version: gover.LocalToolchain()},
- }
- requirements = newRequirements(pruning, roots, nil)
+ rawGoVersion.Store(mainModule, goVersion)
+ requirements = newRequirements(pruning, roots, direct)
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
goVersion = gover.DefaultGoModVersion
}
roots = append(roots, module.Version{Path: "go", Version: goVersion})
- direct["go"] = true
+ direct["go"] = true // Every module directly uses the language and runtime.
- if toolchain == "" {
- toolchain = "go" + goVersion
+ if toolchain != "" {
+ roots = append(roots, module.Version{Path: "toolchain", Version: toolchain})
+ // Leave the toolchain as indirect: nothing in the user's module directly
+ // imports a package from the toolchain, and (like an indirect dependency in
+ // a module without graph pruning) we may remove the toolchain line
+ // automatically if the 'go' version is changed so that it implies the exact
+ // same toolchain.
}
- roots = append(roots, module.Version{Path: "toolchain", Version: toolchain})
- direct["toolchain"] = true
gover.ModSort(roots)
rs := newRequirements(pruning, roots, direct)
ld.errorf("go: go.mod file indicates go %s, but maximum version supported by tidy is %s\n", ld.GoVersion, gover.Local())
base.ExitIfErrors()
}
+ } else {
+ ld.requirements = overrideRoots(ctx, ld.requirements, []module.Version{{Path: "go", Version: ld.GoVersion}})
}
if ld.Tidy {
// rawGoModSummary cannot be used on the main module outside of workspace mode.
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,
+ if m.Path == "go" && gover.Compare(m.Version, gover.GoStrictVersion) >= 0 {
+ // Declare that go 1.21.3 requires toolchain 1.21.3,
// 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
-- 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
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
-- why.want --
go mod tidy -go=''
cmpenv go.mod go.mod.latest
+# Repeat with go get go@ instead of mod tidy.
+
+# Go 1.16 -> 1.17 should be a no-op.
+cp go.mod.116 go.mod
+go get go@1.16
+cmp go.mod go.mod.116
+
+# Go 1.17 -> 1.16 should leave b (go get is not tidy).
+cp go.mod.117 go.mod
+go get go@1.16
+cmp go.mod go.mod.116from117
+
+# Go 1.15 -> 1.16 should leave d (go get is not tidy).
+cp go.mod.115 go.mod
+go get go@1.16
+cmp go.mod go.mod.116from115
+
+# Go 1.16 -> 1.17 should add b.
+cp go.mod.116 go.mod
+go get go@1.17
+stderr '^\tnote: expanded dependencies to upgrade to go 1.17 or higher; run ''go mod tidy'' to clean up'
+cmp go.mod go.mod.117
+
+# Go 1.16 -> 1.15 should add d,
+# but 'go get' doesn't load enough packages to know that.
+# (This leaves the module untidy, but the user can fix it by running 'go mod tidy'.)
+cp go.mod.116 go.mod
+go get go@1.15 toolchain@none
+cmp go.mod go.mod.115from116
+go mod tidy
+cmp go.mod go.mod.115-2
# Updating the go line to 1.21 or higher also updates the toolchain line,
# only if the toolchain is higher than what would be implied by the go line.
example.net/d v0.1.0 // indirect
)
+replace (
+ example.net/a v0.1.0 => ./a
+ example.net/a v0.2.0 => ./a
+ example.net/b v0.1.0 => ./b
+ example.net/b v0.2.0 => ./b
+ example.net/c v0.1.0 => ./c
+ example.net/c v0.2.0 => ./c
+ example.net/d v0.1.0 => ./d
+ example.net/d v0.2.0 => ./d
+)
+-- go.mod.115from116 --
+module example.com/m
+
+go 1.15
+
+require example.net/a v0.1.0
+
+require example.net/c v0.1.0 // indirect
+
+replace (
+ example.net/a v0.1.0 => ./a
+ example.net/a v0.2.0 => ./a
+ example.net/b v0.1.0 => ./b
+ example.net/b v0.2.0 => ./b
+ example.net/c v0.1.0 => ./c
+ example.net/c v0.2.0 => ./c
+ example.net/d v0.1.0 => ./d
+ example.net/d v0.2.0 => ./d
+)
+-- go.mod.116from115 --
+module example.com/m
+
+go 1.16
+
+require example.net/a v0.1.0
+
+require (
+ example.net/c v0.1.0 // indirect
+ example.net/d v0.1.0 // indirect
+)
+
replace (
example.net/a v0.1.0 => ./a
example.net/a v0.2.0 => ./a
example.net/c v0.1.0 // indirect
)
+replace (
+ example.net/a v0.1.0 => ./a
+ example.net/a v0.2.0 => ./a
+ example.net/b v0.1.0 => ./b
+ example.net/b v0.2.0 => ./b
+ example.net/c v0.1.0 => ./c
+ example.net/c v0.2.0 => ./c
+ example.net/d v0.1.0 => ./d
+ example.net/d v0.2.0 => ./d
+)
+-- go.mod.116from117 --
+module example.com/m
+
+go 1.16
+
+require example.net/a v0.1.0
+
+require (
+ example.net/b v0.1.0 // indirect
+ example.net/c v0.1.0 // indirect
+)
+
replace (
example.net/a v0.1.0 => ./a
example.net/a v0.2.0 => ./a
stdout '# rsc.io/quote\nexample.com/a\nrsc.io/quote'
go mod graph
-stdout 'example.com/a rsc.io/quote@v1.5.2\nexample.com/b example.com/c@v1.0.0\ngo@1.18 toolchain@go1.18\nrsc.io/quote@v1.5.2 rsc.io/sampler@v1.3.0\nrsc.io/sampler@v1.3.0 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c'
+stdout 'example.com/a rsc.io/quote@v1.5.2\nexample.com/b example.com/c@v1.0.0\nrsc.io/quote@v1.5.2 rsc.io/sampler@v1.3.0\nrsc.io/sampler@v1.3.0 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c'
-- go.work --
go 1.18