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