cuelang.org/go@v0.13.0/internal/mod/mvs/mvs.go (about) 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. 7 package mvs 8 9 import ( 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. 28 type 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. 47 type 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. 65 type 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. 89 func BuildList[V comparable](targets []V, reqs Reqs[V]) ([]V, error) { 90 return buildList(targets, reqs, nil) 91 } 92 93 func 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. 186 func 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. 278 func 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. 290 func 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. 334 func 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) 413 List: 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 494 type override[V comparable] struct { 495 target V 496 list []V 497 Reqs[V] 498 } 499 500 func (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 }