this repo has no description
1// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package mvs implements Minimal Version Selection.
6// See https://research.swtch.com/vgo-mvs.
7package mvs
8
9import (
10 "cmp"
11 "fmt"
12 "slices"
13 "sync"
14
15 "cuelang.org/go/internal/par"
16)
17
18// A Reqs is the requirement graph on which Minimal Version Selection (MVS) operates.
19//
20// The version strings are opaque except for the special version "none"
21// (see the documentation for V). In particular, MVS does not
22// assume that the version strings are semantic versions; instead, the Max method
23// gives access to the comparison operation.
24//
25// It must be safe to call methods on a Reqs from multiple goroutines simultaneously.
26// Because a Reqs may read the underlying graph from the network on demand,
27// the MVS algorithms parallelize the traversal to overlap network delays.
28type Reqs[V comparable] interface {
29 Versions[V]
30
31 // Required returns the module versions explicitly required by m itself.
32 // The caller must not modify the returned list.
33 Required(m V) ([]V, error)
34
35 // Max returns the maximum of v1 and v2 (it returns either v1 or v2).
36 //
37 // For all versions v, Max(v, "none") must be v,
38 // and for the target passed as the first argument to MVS functions,
39 // Max(target, v) must be target.
40 //
41 // Note that v1 < v2 can be written Max(v1, v2) != v1
42 // and similarly v1 <= v2 can be written Max(v1, v2) == v2.
43 Max(v1, v2 string) string
44}
45
46// An UpgradeReqs is a Reqs that can also identify available upgrades.
47type UpgradeReqs[V comparable] interface {
48 Reqs[V]
49
50 // Upgrade returns the upgraded version of m,
51 // for use during an UpgradeAll operation.
52 // If m should be kept as is, Upgrade returns m.
53 // If m is not yet used in the build, then m.Version will be "none".
54 // More typically, m.Version will be the version required
55 // by some other module in the build.
56 //
57 // If no module version is available for the given path,
58 // Upgrade returns a non-nil error.
59 // TODO(rsc): Upgrade must be able to return errors,
60 // but should "no latest version" just return m instead?
61 Upgrade(m V) (V, error)
62}
63
64// A DowngradeReqs is a Reqs that can also identify available downgrades.
65type DowngradeReqs[V comparable] interface {
66 Reqs[V]
67
68 // Previous returns the version of m.Path immediately prior to m.Version,
69 // or "none" if no such version is known.
70 Previous(m V) (V, error)
71}
72
73// BuildList returns the build list for the target module.
74//
75// target is the root vertex of a module requirement graph. For cmd/cue, this is
76// typically the main module, but note that this algorithm is not intended to
77// be Go-specific: module paths and versions are treated as opaque values.
78//
79// reqs describes the module requirement graph and provides an opaque method
80// for comparing versions.
81//
82// BuildList traverses the graph and returns a list containing the highest
83// version for each visited module. The first element of the returned list is
84// target itself; reqs.Max requires target.Version to compare higher than all
85// other versions, so no other version can be selected. The remaining elements
86// of the list are sorted by path.
87//
88// See https://research.swtch.com/vgo-mvs for details.
89func BuildList[V comparable](targets []V, reqs Reqs[V]) ([]V, error) {
90 return buildList(targets, reqs, nil)
91}
92
93func buildList[V comparable](targets []V, reqs Reqs[V], upgrade func(V) (V, error)) ([]V, error) {
94 cmp := func(v1, v2 string) int {
95 if reqs.Max(v1, v2) != v1 {
96 return -1
97 }
98 if reqs.Max(v2, v1) != v2 {
99 return 1
100 }
101 return 0
102 }
103
104 var (
105 mu sync.Mutex
106 g = NewGraph(Versions[V](reqs), cmp, targets)
107 upgrades = map[V]V{}
108 errs = map[V]error{} // (non-nil errors only)
109 )
110
111 // Explore work graph in parallel in case reqs.Required
112 // does high-latency network operations.
113 var work par.Work[V]
114 for _, target := range targets {
115 work.Add(target)
116 }
117 work.Do(10, func(m V) {
118
119 var required []V
120 var err error
121 if reqs.Version(m) != "none" {
122 required, err = reqs.Required(m)
123 }
124
125 u := m
126 if upgrade != nil {
127 upgradeTo, upErr := upgrade(m)
128 if upErr == nil {
129 u = upgradeTo
130 } else if err == nil {
131 err = upErr
132 }
133 }
134
135 mu.Lock()
136 if err != nil {
137 errs[m] = err
138 }
139 if u != m {
140 upgrades[m] = u
141 required = append([]V{u}, required...)
142 }
143 g.Require(m, required)
144 mu.Unlock()
145
146 for _, r := range required {
147 work.Add(r)
148 }
149 })
150
151 // If there was an error, find the shortest path from the target to the
152 // node where the error occurred so we can report a useful error message.
153 if len(errs) > 0 {
154 errPath := g.FindPath(func(m V) bool {
155 return errs[m] != nil
156 })
157 if len(errPath) == 0 {
158 panic("internal error: could not reconstruct path to module with error")
159 }
160
161 err := errs[errPath[len(errPath)-1]]
162 isUpgrade := func(from, to V) bool {
163 if u, ok := upgrades[from]; ok {
164 return u == to
165 }
166 return false
167 }
168 return nil, NewBuildListError(err, errPath, g.v, isUpgrade)
169 }
170
171 // The final list is the minimum version of each module found in the graph.
172 list := g.BuildList()
173 if vs := list[:len(targets)]; !slices.Equal(vs, targets) {
174 // target.Version will be "" for modload, the main client of MVS.
175 // "" denotes the main module, which has no version. However, MVS treats
176 // version strings as opaque, so "" is not a special value here.
177 // See golang.org/issue/31491, golang.org/issue/29773.
178 panic(fmt.Sprintf("mistake: chose versions %+v instead of targets %+v", vs, targets))
179 }
180 return list, nil
181}
182
183// Req returns the minimal requirement list for the target module,
184// with the constraint that all module paths listed in base must
185// appear in the returned list.
186func Req[V comparable](mainModule V, base []string, reqs Reqs[V]) ([]V, error) {
187 list, err := BuildList([]V{mainModule}, reqs)
188 if err != nil {
189 return nil, err
190 }
191
192 // Note: Not running in parallel because we assume
193 // that list came from a previous operation that paged
194 // in all the requirements, so there's no I/O to overlap now.
195
196 max := map[string]string{}
197 for _, m := range list {
198 max[reqs.Path(m)] = reqs.Version(m)
199 }
200
201 // Compute postorder, cache requirements.
202 var postorder []V
203 reqCache := map[V][]V{}
204 reqCache[mainModule] = nil
205
206 var walk func(V) error
207 walk = func(m V) error {
208 _, ok := reqCache[m]
209 if ok {
210 return nil
211 }
212 required, err := reqs.Required(m)
213 if err != nil {
214 return err
215 }
216 reqCache[m] = required
217 for _, m1 := range required {
218 if err := walk(m1); err != nil {
219 return err
220 }
221 }
222 postorder = append(postorder, m)
223 return nil
224 }
225 for _, m := range list {
226 if err := walk(m); err != nil {
227 return nil, err
228 }
229 }
230
231 // Walk modules in reverse post-order, only adding those not implied already.
232 have := map[V]bool{}
233 walk = func(m V) error {
234 if have[m] {
235 return nil
236 }
237 have[m] = true
238 for _, m1 := range reqCache[m] {
239 walk(m1)
240 }
241 return nil
242 }
243 // First walk the base modules that must be listed.
244 var min []V
245 haveBase := map[string]bool{}
246 for _, path := range base {
247 if haveBase[path] {
248 continue
249 }
250 m, err := reqs.New(path, max[path])
251 if err != nil {
252 // Can't happen because arguments to New above are known to be OK.
253 panic(err)
254 }
255 min = append(min, m)
256 walk(m)
257 haveBase[path] = true
258 }
259 // Now the reverse postorder to bring in anything else.
260 for _, m := range slices.Backward(postorder) {
261 if max[reqs.Path(m)] != reqs.Version(m) {
262 // Older version.
263 continue
264 }
265 if !have[m] {
266 min = append(min, m)
267 walk(m)
268 }
269 }
270 slices.SortFunc(min, func(a, b V) int {
271 return cmp.Compare(reqs.Path(a), reqs.Path(b))
272 })
273 return min, nil
274}
275
276// UpgradeAll returns a build list for the target module
277// in which every module is upgraded to its latest version.
278func UpgradeAll[V comparable](target V, reqs UpgradeReqs[V]) ([]V, error) {
279 return buildList([]V{target}, Reqs[V](reqs), func(m V) (V, error) {
280 if reqs.Path(m) == reqs.Path(target) {
281 return target, nil
282 }
283
284 return reqs.Upgrade(m)
285 })
286}
287
288// Upgrade returns a build list for the target module
289// in which the given additional modules are upgraded.
290func Upgrade[V comparable](target V, reqs UpgradeReqs[V], upgrade ...V) ([]V, error) {
291 list, err := reqs.Required(target)
292 if err != nil {
293 return nil, err
294 }
295
296 pathInList := make(map[string]bool, len(list))
297 for _, m := range list {
298 pathInList[reqs.Path(m)] = true
299 }
300 list = slices.Clone(list)
301
302 upgradeTo := make(map[string]string, len(upgrade))
303 for _, u := range upgrade {
304 if !pathInList[reqs.Path(u)] {
305 newv, err := reqs.New(reqs.Path(u), "none")
306 if err != nil {
307 // Can't happen because arguments to New above are known to be OK.
308 panic(err)
309 }
310 list = append(list, newv)
311 }
312 if prev, dup := upgradeTo[reqs.Path(u)]; dup {
313 upgradeTo[reqs.Path(u)] = reqs.Max(prev, reqs.Version(u))
314 } else {
315 upgradeTo[reqs.Path(u)] = reqs.Version(u)
316 }
317 }
318
319 return buildList([]V{target}, &override[V]{target, list, reqs}, func(m V) (V, error) {
320 if v, ok := upgradeTo[reqs.Path(m)]; ok {
321 return reqs.New(reqs.Path(m), v)
322 }
323 return m, nil
324 })
325}
326
327// Downgrade returns a build list for the target module
328// in which the given additional modules are downgraded,
329// potentially overriding the requirements of the target.
330//
331// The versions to be downgraded may be unreachable from reqs.Latest and
332// reqs.Previous, but the methods of reqs must otherwise handle such versions
333// correctly.
334func Downgrade[V comparable](target V, reqs DowngradeReqs[V], downgrade ...V) ([]V, error) {
335 // Per https://research.swtch.com/vgo-mvs#algorithm_4:
336 // “To avoid an unnecessary downgrade to E 1.1, we must also add a new
337 // requirement on E 1.2. We can apply Algorithm R to find the minimal set of
338 // new requirements to write to go.mod.”
339 //
340 // In order to generate those new requirements, we need to identify versions
341 // for every module in the build list — not just reqs.Required(target).
342 list, err := BuildList([]V{target}, reqs)
343 if err != nil {
344 return nil, err
345 }
346 list = list[1:] // remove target
347
348 max := make(map[string]string)
349 for _, r := range list {
350 max[reqs.Path(r)] = reqs.Version(r)
351 }
352 for _, d := range downgrade {
353 if v, ok := max[reqs.Path(d)]; !ok || reqs.Max(v, reqs.Version(d)) != reqs.Version(d) {
354 max[reqs.Path(d)] = reqs.Version(d)
355 }
356 }
357
358 var (
359 added = make(map[V]bool)
360 rdeps = make(map[V][]V)
361 excluded = make(map[V]bool)
362 )
363 var exclude func(V)
364 exclude = func(m V) {
365 if excluded[m] {
366 return
367 }
368 excluded[m] = true
369 for _, p := range rdeps[m] {
370 exclude(p)
371 }
372 }
373 var add func(V)
374 add = func(m V) {
375 if added[m] {
376 return
377 }
378 added[m] = true
379 if v, ok := max[reqs.Path(m)]; ok && reqs.Max(reqs.Version(m), v) != v {
380 // m would upgrade an existing dependency — it is not a strict downgrade,
381 // and because it was already present as a dependency, it could affect the
382 // behavior of other relevant packages.
383 exclude(m)
384 return
385 }
386 list, err := reqs.Required(m)
387 if err != nil {
388 // If we can't load the requirements, we couldn't load the go.mod file.
389 // There are a number of reasons this can happen, but this usually
390 // means an older version of the module had a missing or invalid
391 // go.mod file. For example, if example.com/mod released v2.0.0 before
392 // migrating to modules (v2.0.0+incompatible), then added a valid go.mod
393 // in v2.0.1, downgrading from v2.0.1 would cause this error.
394 //
395 // TODO(golang.org/issue/31730, golang.org/issue/30134): if the error
396 // is transient (we couldn't download go.mod), return the error from
397 // Downgrade. Currently, we can't tell what kind of error it is.
398 exclude(m)
399 return
400 }
401 for _, r := range list {
402 add(r)
403 if excluded[r] {
404 exclude(m)
405 return
406 }
407 rdeps[r] = append(rdeps[r], m)
408 }
409 }
410
411 downgraded := make([]V, 0, len(list)+1)
412 downgraded = append(downgraded, target)
413List:
414 for _, r := range list {
415 add(r)
416 for excluded[r] {
417 p, err := reqs.Previous(r)
418 if err != nil {
419 // This is likely a transient error reaching the repository,
420 // rather than a permanent error with the retrieved version.
421 //
422 // TODO(golang.org/issue/31730, golang.org/issue/30134):
423 // decode what to do based on the actual error.
424 return nil, err
425 }
426 // If the target version is a pseudo-version, it may not be
427 // included when iterating over prior versions using reqs.Previous.
428 // Insert it into the right place in the iteration.
429 // If v is excluded, p should be returned again by reqs.Previous on the next iteration.
430 if v := max[reqs.Path(r)]; reqs.Max(v, reqs.Version(r)) != v && reqs.Max(reqs.Version(p), v) != reqs.Version(p) {
431 p0, err := reqs.New(reqs.Path(p), v)
432 if err != nil {
433 // Can't happen because arguments to New above are known to be OK.
434 panic(err)
435 }
436 p = p0
437 }
438 if reqs.Version(p) == "none" {
439 continue List
440 }
441 add(p)
442 r = p
443 }
444 downgraded = append(downgraded, r)
445 }
446
447 // The downgrades we computed above only downgrade to versions enumerated by
448 // reqs.Previous. However, reqs.Previous omits some versions — such as
449 // pseudo-versions and retracted versions — that may be selected as transitive
450 // requirements of other modules.
451 //
452 // If one of those requirements pulls the version back up above the version
453 // identified by reqs.Previous, then the transitive dependencies of that
454 // initially-downgraded version should no longer matter — in particular, we
455 // should not add new dependencies on module paths that nothing else in the
456 // updated module graph even requires.
457 //
458 // In order to eliminate those spurious dependencies, we recompute the build
459 // list with the actual versions of the downgraded modules as selected by MVS,
460 // instead of our initial downgrades.
461 // (See the downhiddenartifact and downhiddencross test cases).
462 actual, err := BuildList([]V{target}, &override[V]{
463 target: target,
464 list: downgraded,
465 Reqs: reqs,
466 })
467 if err != nil {
468 return nil, err
469 }
470 actualVersion := make(map[string]string, len(actual))
471 for _, m := range actual {
472 actualVersion[reqs.Path(m)] = reqs.Version(m)
473 }
474
475 downgraded = downgraded[:0]
476 for _, m := range list {
477 if v, ok := actualVersion[reqs.Path(m)]; ok {
478 m1, err := reqs.New(reqs.Path(m), v)
479 if err != nil {
480 // Can't happen because arguments to New above are known to be OK.
481 panic(err)
482 }
483 downgraded = append(downgraded, m1)
484 }
485 }
486
487 return BuildList([]V{target}, &override[V]{
488 target: target,
489 list: downgraded,
490 Reqs: reqs,
491 })
492}
493
494type override[V comparable] struct {
495 target V
496 list []V
497 Reqs[V]
498}
499
500func (r *override[V]) Required(m V) ([]V, error) {
501 if m == r.target {
502 return r.list, nil
503 }
504 return r.Reqs.Required(m)
505}