cuelang.org/go@v0.10.1/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 "fmt" 11 "slices" 12 "sort" 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 i := len(postorder) - 1; i >= 0; i-- { 261 m := postorder[i] 262 if max[reqs.Path(m)] != reqs.Version(m) { 263 // Older version. 264 continue 265 } 266 if !have[m] { 267 min = append(min, m) 268 walk(m) 269 } 270 } 271 sort.Slice(min, func(i, j int) bool { 272 return reqs.Path(min[i]) < reqs.Path(min[j]) 273 }) 274 return min, nil 275 } 276 277 // UpgradeAll returns a build list for the target module 278 // in which every module is upgraded to its latest version. 279 func UpgradeAll[V comparable](target V, reqs UpgradeReqs[V]) ([]V, error) { 280 return buildList([]V{target}, Reqs[V](reqs), func(m V) (V, error) { 281 if reqs.Path(m) == reqs.Path(target) { 282 return target, nil 283 } 284 285 return reqs.Upgrade(m) 286 }) 287 } 288 289 // Upgrade returns a build list for the target module 290 // in which the given additional modules are upgraded. 291 func Upgrade[V comparable](target V, reqs UpgradeReqs[V], upgrade ...V) ([]V, error) { 292 list, err := reqs.Required(target) 293 if err != nil { 294 return nil, err 295 } 296 297 pathInList := make(map[string]bool, len(list)) 298 for _, m := range list { 299 pathInList[reqs.Path(m)] = true 300 } 301 list = append([]V(nil), list...) 302 303 upgradeTo := make(map[string]string, len(upgrade)) 304 for _, u := range upgrade { 305 if !pathInList[reqs.Path(u)] { 306 newv, err := reqs.New(reqs.Path(u), "none") 307 if err != nil { 308 // Can't happen because arguments to New above are known to be OK. 309 panic(err) 310 } 311 list = append(list, newv) 312 } 313 if prev, dup := upgradeTo[reqs.Path(u)]; dup { 314 upgradeTo[reqs.Path(u)] = reqs.Max(prev, reqs.Version(u)) 315 } else { 316 upgradeTo[reqs.Path(u)] = reqs.Version(u) 317 } 318 } 319 320 return buildList([]V{target}, &override[V]{target, list, reqs}, func(m V) (V, error) { 321 if v, ok := upgradeTo[reqs.Path(m)]; ok { 322 return reqs.New(reqs.Path(m), v) 323 } 324 return m, nil 325 }) 326 } 327 328 // Downgrade returns a build list for the target module 329 // in which the given additional modules are downgraded, 330 // potentially overriding the requirements of the target. 331 // 332 // The versions to be downgraded may be unreachable from reqs.Latest and 333 // reqs.Previous, but the methods of reqs must otherwise handle such versions 334 // correctly. 335 func Downgrade[V comparable](target V, reqs DowngradeReqs[V], downgrade ...V) ([]V, error) { 336 // Per https://research.swtch.com/vgo-mvs#algorithm_4: 337 // “To avoid an unnecessary downgrade to E 1.1, we must also add a new 338 // requirement on E 1.2. We can apply Algorithm R to find the minimal set of 339 // new requirements to write to go.mod.” 340 // 341 // In order to generate those new requirements, we need to identify versions 342 // for every module in the build list — not just reqs.Required(target). 343 list, err := BuildList([]V{target}, reqs) 344 if err != nil { 345 return nil, err 346 } 347 list = list[1:] // remove target 348 349 max := make(map[string]string) 350 for _, r := range list { 351 max[reqs.Path(r)] = reqs.Version(r) 352 } 353 for _, d := range downgrade { 354 if v, ok := max[reqs.Path(d)]; !ok || reqs.Max(v, reqs.Version(d)) != reqs.Version(d) { 355 max[reqs.Path(d)] = reqs.Version(d) 356 } 357 } 358 359 var ( 360 added = make(map[V]bool) 361 rdeps = make(map[V][]V) 362 excluded = make(map[V]bool) 363 ) 364 var exclude func(V) 365 exclude = func(m V) { 366 if excluded[m] { 367 return 368 } 369 excluded[m] = true 370 for _, p := range rdeps[m] { 371 exclude(p) 372 } 373 } 374 var add func(V) 375 add = func(m V) { 376 if added[m] { 377 return 378 } 379 added[m] = true 380 if v, ok := max[reqs.Path(m)]; ok && reqs.Max(reqs.Version(m), v) != v { 381 // m would upgrade an existing dependency — it is not a strict downgrade, 382 // and because it was already present as a dependency, it could affect the 383 // behavior of other relevant packages. 384 exclude(m) 385 return 386 } 387 list, err := reqs.Required(m) 388 if err != nil { 389 // If we can't load the requirements, we couldn't load the go.mod file. 390 // There are a number of reasons this can happen, but this usually 391 // means an older version of the module had a missing or invalid 392 // go.mod file. For example, if example.com/mod released v2.0.0 before 393 // migrating to modules (v2.0.0+incompatible), then added a valid go.mod 394 // in v2.0.1, downgrading from v2.0.1 would cause this error. 395 // 396 // TODO(golang.org/issue/31730, golang.org/issue/30134): if the error 397 // is transient (we couldn't download go.mod), return the error from 398 // Downgrade. Currently, we can't tell what kind of error it is. 399 exclude(m) 400 return 401 } 402 for _, r := range list { 403 add(r) 404 if excluded[r] { 405 exclude(m) 406 return 407 } 408 rdeps[r] = append(rdeps[r], m) 409 } 410 } 411 412 downgraded := make([]V, 0, len(list)+1) 413 downgraded = append(downgraded, target) 414 List: 415 for _, r := range list { 416 add(r) 417 for excluded[r] { 418 p, err := reqs.Previous(r) 419 if err != nil { 420 // This is likely a transient error reaching the repository, 421 // rather than a permanent error with the retrieved version. 422 // 423 // TODO(golang.org/issue/31730, golang.org/issue/30134): 424 // decode what to do based on the actual error. 425 return nil, err 426 } 427 // If the target version is a pseudo-version, it may not be 428 // included when iterating over prior versions using reqs.Previous. 429 // Insert it into the right place in the iteration. 430 // If v is excluded, p should be returned again by reqs.Previous on the next iteration. 431 if v := max[reqs.Path(r)]; reqs.Max(v, reqs.Version(r)) != v && reqs.Max(reqs.Version(p), v) != reqs.Version(p) { 432 p0, err := reqs.New(reqs.Path(p), v) 433 if err != nil { 434 // Can't happen because arguments to New above are known to be OK. 435 panic(err) 436 } 437 p = p0 438 } 439 if reqs.Version(p) == "none" { 440 continue List 441 } 442 add(p) 443 r = p 444 } 445 downgraded = append(downgraded, r) 446 } 447 448 // The downgrades we computed above only downgrade to versions enumerated by 449 // reqs.Previous. However, reqs.Previous omits some versions — such as 450 // pseudo-versions and retracted versions — that may be selected as transitive 451 // requirements of other modules. 452 // 453 // If one of those requirements pulls the version back up above the version 454 // identified by reqs.Previous, then the transitive dependencies of that 455 // initially-downgraded version should no longer matter — in particular, we 456 // should not add new dependencies on module paths that nothing else in the 457 // updated module graph even requires. 458 // 459 // In order to eliminate those spurious dependencies, we recompute the build 460 // list with the actual versions of the downgraded modules as selected by MVS, 461 // instead of our initial downgrades. 462 // (See the downhiddenartifact and downhiddencross test cases). 463 actual, err := BuildList([]V{target}, &override[V]{ 464 target: target, 465 list: downgraded, 466 Reqs: reqs, 467 }) 468 if err != nil { 469 return nil, err 470 } 471 actualVersion := make(map[string]string, len(actual)) 472 for _, m := range actual { 473 actualVersion[reqs.Path(m)] = reqs.Version(m) 474 } 475 476 downgraded = downgraded[:0] 477 for _, m := range list { 478 if v, ok := actualVersion[reqs.Path(m)]; ok { 479 m1, err := reqs.New(reqs.Path(m), v) 480 if err != nil { 481 // Can't happen because arguments to New above are known to be OK. 482 panic(err) 483 } 484 downgraded = append(downgraded, m1) 485 } 486 } 487 488 return BuildList([]V{target}, &override[V]{ 489 target: target, 490 list: downgraded, 491 Reqs: reqs, 492 }) 493 } 494 495 type override[V comparable] struct { 496 target V 497 list []V 498 Reqs[V] 499 } 500 501 func (r *override[V]) Required(m V) ([]V, error) { 502 if m == r.target { 503 return r.list, nil 504 } 505 return r.Reqs.Required(m) 506 }