| // 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) |
| } |