blob: 4d1f3c78262f0d51bfb2d34a99451f78cb1a9165 [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modload
import (
"context"
"sort"
"cmd/go/internal/mvs"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
)
// editBuildList returns an edited version of initial such that:
//
// 1. Each module version in mustSelect is selected, unless it is upgraded
// by the transitive requirements of another version in mustSelect.
//
// 2. Each module version in tryUpgrade is upgraded toward the indicated
// version as far as can be done without violating (1).
//
// 3. Each module version in initial is downgraded from its original version
// only to the extent needed to satisfy (1), or upgraded only to the extent
// needed to satisfy (1) and (2).
//
// 4. No module is upgraded above the maximum version of its path found in the
// combined dependency graph of list, tryUpgrade, and mustSelect.
func editBuildList(ctx context.Context, initial, tryUpgrade, mustSelect []module.Version) ([]module.Version, error) {
// Per https://research.swtch.com/vgo-mvs#algorithm_4:
// “To avoid an unnecessary downgrade to E 1.1, we must also add a new
// requirement on E 1.2. We can apply Algorithm R to find the minimal set of
// new requirements to write to go.mod.”
//
// In order to generate those new requirements, we need consider versions for
// every module in the existing build list, plus every module being directly
// added by the edit. However, modules added only as dependencies of tentative
// versions should not be retained if they end up being upgraded or downgraded
// away due to versions in mustSelect.
// When we downgrade modules in order to reach mustSelect, we don't want to
// upgrade any existing module above the version that would be selected if we
// just added all of the new requirements and *didn't* downgrade.
//
// So we'll do exactly that: just add all of the new requirements and not
// downgrade, and return the resulting versions as an upper bound. This
// intentionally limits our solution space so that edits that the user
// percieves as “downgrades” will not also result in upgrades.
max := make(map[string]string)
maxes, err := mvs.Upgrade(Target, &mvsReqs{
buildList: append(capVersionSlice(initial), mustSelect...),
}, tryUpgrade...)
if err != nil {
return nil, err
}
for _, m := range maxes {
max[m.Path] = m.Version
}
// The versions in mustSelect override whatever we would naively select —
// we will downgrade other modules as needed in order to meet them.
for _, m := range mustSelect {
max[m.Path] = m.Version
}
limiter := newVersionLimiter(max)
// Force the selected versions in mustSelect, even if they conflict.
//
// TODO(bcmills): Instead of forcing these versions, record conflicts
// so that the caller doesn't have to recompute them.
for _, m := range mustSelect {
limiter.selected[m.Path] = m.Version
}
// For each module, we want to get as close as we can to either the upgrade
// version or the previously-selected version in the build list, whichever is
// higher. We can compute those in either order, but the upgrades will tend to
// be higher than the build list, so we arbitrarily start with those.
for _, m := range tryUpgrade {
if err := limiter.upgradeToward(ctx, m); err != nil {
return nil, err
}
}
for _, m := range initial {
if err := limiter.upgradeToward(ctx, m); err != nil {
return nil, err
}
}
// We've identified acceptable versions for each of the modules, but those
// versions are not necessarily consistent with each other: one upgraded or
// downgraded module may require a higher (but still allowed) version of
// another. The lower version may require extraneous dependencies that aren't
// actually relevant, so we need to compute the actual selected versions.
adjusted := make([]module.Version, 0, len(maxes))
for _, m := range maxes {
if v, ok := limiter.selected[m.Path]; ok {
adjusted = append(adjusted, module.Version{Path: m.Path, Version: v})
}
}
consistent, err := mvs.BuildList(Target, &mvsReqs{buildList: adjusted})
if err != nil {
return nil, err
}
// We have the correct selected versions. Now we need to re-run MVS with only
// the actually-selected versions in order to eliminate extraneous
// dependencies from lower-than-selected ones.
compacted := consistent[:0]
for _, m := range consistent {
if _, ok := limiter.selected[m.Path]; ok {
// The fact that the limiter has a version for m.Path indicates that we
// care about retaining that path, even if the version was upgraded for
// consistency.
compacted = append(compacted, m)
}
}
return mvs.BuildList(Target, &mvsReqs{buildList: compacted})
}
// A versionLimiter tracks the versions that may be selected for each module
// subject to constraints on the maximum versions of transitive dependencies.
type versionLimiter struct {
// max maps each module path to the maximum version that may be selected for
// that path. Paths with no entry are unrestricted.
max map[string]string
// selected maps each module path to a version of that path (if known) whose
// transitive dependencies do not violate any max version. The version kept
// is the highest one found during any call to upgradeToward for the given
// module path.
//
// If a higher acceptable version is found during a call to upgradeToward for
// some *other* module path, that does not update the selected version.
// Ignoring those versions keeps the downgrades computed for two modules
// together close to the individual downgrades that would be computed for each
// module in isolation. (The only way one module can affect another is if the
// final downgraded version of the one module explicitly requires a higher
// version of the other.)
//
// Version "none" of every module is always known not to violate any max
// version, so paths at version "none" are omitted.
selected map[string]string
// disqualified maps each encountered version to either true (if that version
// is known to be disqualified due to a conflict with a max version) or false
// (if that version is not known to be disqualified, either because it is ok
// or because we are currently traversing a cycle that includes it).
disqualified map[module.Version]bool
// requiredBy maps each not-yet-disqualified module version to the versions
// that directly require it. If that version becomes disqualified, the
// disqualification will be propagated to all of the versions in the list.
requiredBy map[module.Version][]module.Version
}
func newVersionLimiter(max map[string]string) *versionLimiter {
return &versionLimiter{
selected: map[string]string{Target.Path: Target.Version},
max: max,
disqualified: map[module.Version]bool{Target: false},
requiredBy: map[module.Version][]module.Version{},
}
}
// upgradeToward attempts to upgrade the selected version of m.Path as close as
// possible to m.Version without violating l's maximum version limits.
func (l *versionLimiter) upgradeToward(ctx context.Context, m module.Version) error {
selected, ok := l.selected[m.Path]
if ok {
if cmpVersion(selected, m.Version) >= 0 {
// The selected version is already at least m, so no upgrade is needed.
return nil
}
} else {
selected = "none"
}
if l.isDisqualified(m) {
candidates, err := versions(ctx, m.Path, CheckAllowed)
if err != nil {
// This is likely a transient error reaching the repository,
// rather than a permanent error with the retrieved version.
//
// TODO(golang.org/issue/31730, golang.org/issue/30134):
// decode what to do based on the actual error.
return err
}
// Skip to candidates < m.Version.
i := sort.Search(len(candidates), func(i int) bool {
return semver.Compare(candidates[i], m.Version) >= 0
})
candidates = candidates[:i]
for l.isDisqualified(m) {
n := len(candidates)
if n == 0 || cmpVersion(selected, candidates[n-1]) >= 0 {
// We couldn't find a suitable candidate above the already-selected version.
// Retain that version unmodified.
return nil
}
m.Version, candidates = candidates[n-1], candidates[:n-1]
}
}
l.selected[m.Path] = m.Version
return nil
}
// isDisqualified reports whether m (or its transitive dependencies) would
// violate l's maximum version limits if added to the module requirement graph.
func (l *versionLimiter) isDisqualified(m module.Version) bool {
if m.Version == "none" || m == Target {
// version "none" has no requirements, and the dependencies of Target are
// tautological.
return false
}
if dq, seen := l.disqualified[m]; seen {
return dq
}
l.disqualified[m] = false
if max, ok := l.max[m.Path]; ok && cmpVersion(m.Version, max) > 0 {
l.disqualify(m)
return true
}
summary, err := goModSummary(m)
if err != nil {
// If we can't load the requirements, we couldn't load the go.mod file.
// There are a number of reasons this can happen, but this usually
// means an older version of the module had a missing or invalid
// go.mod file. For example, if example.com/mod released v2.0.0 before
// migrating to modules (v2.0.0+incompatible), then added a valid go.mod
// in v2.0.1, downgrading from v2.0.1 would cause this error.
//
// TODO(golang.org/issue/31730, golang.org/issue/30134): if the error
// is transient (we couldn't download go.mod), return the error from
// Downgrade. Currently, we can't tell what kind of error it is.
l.disqualify(m)
return true
}
for _, r := range summary.require {
if l.isDisqualified(r) {
l.disqualify(m)
return true
}
// r and its dependencies are (perhaps provisionally) ok.
//
// However, if there are cycles in the requirement graph, we may have only
// checked a portion of the requirement graph so far, and r (and thus m) may
// yet be disqualified by some path we have not yet visited. Remember this edge
// so that we can disqualify m and its dependents if that occurs.
l.requiredBy[r] = append(l.requiredBy[r], m)
}
return false
}
// disqualify records that m (or one of its transitive dependencies)
// violates l's maximum version limits.
func (l *versionLimiter) disqualify(m module.Version) {
if l.disqualified[m] {
return
}
l.disqualified[m] = true
for _, p := range l.requiredBy[m] {
l.disqualify(p)
}
// Now that we have disqualified the modules that depend on m, we can forget
// about them — we won't need to disqualify them again.
delete(l.requiredBy, m)
}