github.com/sdboyer/gps@v0.16.3/solver.go (about) 1 package gps 2 3 import ( 4 "container/heap" 5 "fmt" 6 "log" 7 "sort" 8 "strings" 9 10 "github.com/armon/go-radix" 11 "github.com/sdboyer/gps/internal" 12 "github.com/sdboyer/gps/pkgtree" 13 ) 14 15 var ( 16 osList []string 17 archList []string 18 ignoreTags = []string{} //[]string{"appengine", "ignore"} //TODO: appengine is a special case for now: https://github.com/tools/godep/issues/353 19 ) 20 21 func init() { 22 // The supported systems are listed in 23 // https://github.com/golang/go/blob/master/src/go/build/syslist.go 24 // The lists are not exported, so we need to duplicate them here. 25 osListString := "android darwin dragonfly freebsd linux nacl netbsd openbsd plan9 solaris windows" 26 osList = strings.Split(osListString, " ") 27 28 archListString := "386 amd64 amd64p32 arm armbe arm64 arm64be ppc64 ppc64le mips mipsle mips64 mips64le mips64p32 mips64p32le ppc s390 s390x sparc sparc64" 29 archList = strings.Split(archListString, " ") 30 } 31 32 var rootRev = Revision("") 33 34 // SolveParameters hold all arguments to a solver run. 35 // 36 // Only RootDir and RootPackageTree are absolutely required. A nil Manifest is 37 // allowed, though it usually makes little sense. 38 // 39 // Of these properties, only the Manifest and RootPackageTree are (directly) 40 // incorporated in memoization hashing. 41 type SolveParameters struct { 42 // The path to the root of the project on which the solver should operate. 43 // This should point to the directory that should contain the vendor/ 44 // directory. 45 // 46 // In general, it is wise for this to be under an active GOPATH, though it 47 // is not (currently) required. 48 // 49 // A real path to a readable directory is required. 50 RootDir string 51 52 // The ProjectAnalyzer is responsible for extracting Manifest and 53 // (optionally) Lock information from dependencies. The solver passes it 54 // along to its SourceManager's GetManifestAndLock() method as needed. 55 // 56 // An analyzer is required. 57 ProjectAnalyzer ProjectAnalyzer 58 59 // The tree of packages that comprise the root project, as well as the 60 // import path that should identify the root of that tree. 61 // 62 // In most situations, tools should simply pass the result of ListPackages() 63 // directly through here. 64 // 65 // The ImportRoot property must be a non-empty string, and at least one 66 // element must be present in the Packages map. 67 RootPackageTree pkgtree.PackageTree 68 69 // The root manifest. This contains all the dependency constraints 70 // associated with normal Manifests, as well as the particular controls 71 // afforded only to the root project. 72 // 73 // May be nil, but for most cases, that would be unwise. 74 Manifest RootManifest 75 76 // The root lock. Optional. Generally, this lock is the output of a previous 77 // solve run. 78 // 79 // If provided, the solver will attempt to preserve the versions specified 80 // in the lock, unless ToChange or ChangeAll settings indicate otherwise. 81 Lock Lock 82 83 // ToChange is a list of project names that should be changed - that is, any 84 // versions specified for those projects in the root lock file should be 85 // ignored. 86 // 87 // Passing ChangeAll has subtly different behavior from enumerating all 88 // projects into ToChange. In general, ToChange should *only* be used if the 89 // user expressly requested an upgrade for a specific project. 90 ToChange []ProjectRoot 91 92 // ChangeAll indicates that all projects should be changed - that is, any 93 // versions specified in the root lock file should be ignored. 94 ChangeAll bool 95 96 // Downgrade indicates whether the solver will attempt to upgrade (false) or 97 // downgrade (true) projects that are not locked, or are marked for change. 98 // 99 // Upgrading is, by far, the most typical case. The field is named 100 // 'Downgrade' so that the bool's zero value corresponds to that most 101 // typical case. 102 Downgrade bool 103 104 // Trace controls whether the solver will generate informative trace output 105 // as it moves through the solving process. 106 Trace bool 107 108 // TraceLogger is the logger to use for generating trace output. If Trace is 109 // true but no logger is provided, solving will result in an error. 110 TraceLogger *log.Logger 111 } 112 113 // solver is a CDCL-style constraint solver with satisfiability conditions 114 // hardcoded to the needs of the Go package management problem space. 115 type solver struct { 116 // The current number of attempts made over the course of this solve. This 117 // number increments each time the algorithm completes a backtrack and 118 // starts moving forward again. 119 attempts int 120 121 // Logger used exclusively for trace output, if the trace option is set. 122 tl *log.Logger 123 124 // A bridge to the standard SourceManager. The adapter does some local 125 // caching of pre-sorted version lists, as well as translation between the 126 // full-on ProjectIdentifiers that the solver deals with and the simplified 127 // names a SourceManager operates on. 128 b sourceBridge 129 130 // A versionUnifier, to facilitate cross-type version comparison and set 131 // operations. 132 vUnify versionUnifier 133 134 // A stack containing projects and packages that are currently "selected" - 135 // that is, they have passed all satisfiability checks, and are part of the 136 // current solution. 137 // 138 // The *selection type is mostly just a dumb data container; the solver 139 // itself is responsible for maintaining that invariant. 140 sel *selection 141 142 // The current list of projects that we need to incorporate into the solution in 143 // order for the solution to be complete. This list is implemented as a 144 // priority queue that places projects least likely to induce errors at the 145 // front, in order to minimize the amount of backtracking required to find a 146 // solution. 147 // 148 // Entries are added to and removed from this list by the solver at the same 149 // time that the selected queue is updated, either with an addition or 150 // removal. 151 unsel *unselected 152 153 // A stack of all the currently active versionQueues in the solver. The set 154 // of projects represented here corresponds closely to what's in s.sel, 155 // although s.sel will always contain the root project, and s.vqs never 156 // will. Also, s.vqs is only added to (or popped from during backtracking) 157 // when a new project is selected; it is untouched when new packages are 158 // added to an existing project. 159 vqs []*versionQueue 160 161 // Contains data and constraining information from the root project 162 rd rootdata 163 164 // metrics for the current solve run. 165 mtr *metrics 166 } 167 168 func (params SolveParameters) toRootdata() (rootdata, error) { 169 if params.ProjectAnalyzer == nil { 170 return rootdata{}, badOptsFailure("must provide a ProjectAnalyzer") 171 } 172 if params.RootDir == "" { 173 return rootdata{}, badOptsFailure("params must specify a non-empty root directory") 174 } 175 if params.RootPackageTree.ImportRoot == "" { 176 return rootdata{}, badOptsFailure("params must include a non-empty import root") 177 } 178 if len(params.RootPackageTree.Packages) == 0 { 179 return rootdata{}, badOptsFailure("at least one package must be present in the PackageTree") 180 } 181 if params.Lock == nil && len(params.ToChange) != 0 { 182 return rootdata{}, badOptsFailure(fmt.Sprintf("update specifically requested for %s, but no lock was provided to upgrade from", params.ToChange)) 183 } 184 185 if params.Manifest == nil { 186 params.Manifest = simpleRootManifest{} 187 } 188 189 rd := rootdata{ 190 ig: params.Manifest.IgnoredPackages(), 191 req: params.Manifest.RequiredPackages(), 192 ovr: params.Manifest.Overrides(), 193 rpt: params.RootPackageTree.Copy(), 194 chng: make(map[ProjectRoot]struct{}), 195 rlm: make(map[ProjectRoot]LockedProject), 196 chngall: params.ChangeAll, 197 dir: params.RootDir, 198 an: params.ProjectAnalyzer, 199 } 200 201 // Ensure the required, ignore and overrides maps are at least initialized 202 if rd.ig == nil { 203 rd.ig = make(map[string]bool) 204 } 205 if rd.req == nil { 206 rd.req = make(map[string]bool) 207 } 208 if rd.ovr == nil { 209 rd.ovr = make(ProjectConstraints) 210 } 211 212 if len(rd.ig) != 0 { 213 var both []string 214 for pkg := range params.Manifest.RequiredPackages() { 215 if rd.ig[pkg] { 216 both = append(both, pkg) 217 } 218 } 219 switch len(both) { 220 case 0: 221 break 222 case 1: 223 return rootdata{}, badOptsFailure(fmt.Sprintf("%q was given as both a required and ignored package", both[0])) 224 default: 225 return rootdata{}, badOptsFailure(fmt.Sprintf("multiple packages given as both required and ignored: %s", strings.Join(both, ", "))) 226 } 227 } 228 229 // Validate no empties in the overrides map 230 var eovr []string 231 for pr, pp := range rd.ovr { 232 if pp.Constraint == nil && pp.Source == "" { 233 eovr = append(eovr, string(pr)) 234 } 235 } 236 237 if eovr != nil { 238 // Maybe it's a little nitpicky to do this (we COULD proceed; empty 239 // overrides have no effect), but this errs on the side of letting the 240 // tool/user know there's bad input. Purely as a principle, that seems 241 // preferable to silently allowing progress with icky input. 242 if len(eovr) > 1 { 243 return rootdata{}, badOptsFailure(fmt.Sprintf("Overrides lacked any non-zero properties for multiple project roots: %s", strings.Join(eovr, " "))) 244 } 245 return rootdata{}, badOptsFailure(fmt.Sprintf("An override was declared for %s, but without any non-zero properties", eovr[0])) 246 } 247 248 // Prep safe, normalized versions of root manifest and lock data 249 rd.rm = prepManifest(params.Manifest) 250 251 if params.Lock != nil { 252 for _, lp := range params.Lock.Projects() { 253 rd.rlm[lp.Ident().ProjectRoot] = lp 254 } 255 256 // Also keep a prepped one, mostly for the bridge. This is probably 257 // wasteful, but only minimally so, and yay symmetry 258 rd.rl = prepLock(params.Lock) 259 } 260 261 for _, p := range params.ToChange { 262 if _, exists := rd.rlm[p]; !exists { 263 return rootdata{}, badOptsFailure(fmt.Sprintf("cannot update %s as it is not in the lock", p)) 264 } 265 rd.chng[p] = struct{}{} 266 } 267 268 return rd, nil 269 } 270 271 // Prepare readies a Solver for use. 272 // 273 // This function reads and validates the provided SolveParameters. If a problem 274 // with the inputs is detected, an error is returned. Otherwise, a Solver is 275 // returned, ready to hash and check inputs or perform a solving run. 276 func Prepare(params SolveParameters, sm SourceManager) (Solver, error) { 277 if sm == nil { 278 return nil, badOptsFailure("must provide non-nil SourceManager") 279 } 280 if params.Trace && params.TraceLogger == nil { 281 return nil, badOptsFailure("trace requested, but no logger provided") 282 } 283 284 rd, err := params.toRootdata() 285 if err != nil { 286 return nil, err 287 } 288 289 s := &solver{ 290 tl: params.TraceLogger, 291 rd: rd, 292 } 293 294 // Set up the bridge and ensure the root dir is in good, working order 295 // before doing anything else. (This call is stubbed out in tests, via 296 // overriding mkBridge(), so we can run with virtual RootDir.) 297 s.b = mkBridge(s, sm, params.Downgrade) 298 err = s.b.verifyRootDir(params.RootDir) 299 if err != nil { 300 return nil, err 301 } 302 s.vUnify = versionUnifier{ 303 b: s.b, 304 } 305 306 // Initialize stacks and queues 307 s.sel = &selection{ 308 deps: make(map[ProjectRoot][]dependency), 309 vu: s.vUnify, 310 } 311 s.unsel = &unselected{ 312 sl: make([]bimodalIdentifier, 0), 313 cmp: s.unselectedComparator, 314 } 315 316 return s, nil 317 } 318 319 // A Solver is the main workhorse of gps: given a set of project inputs, it 320 // performs a constraint solving analysis to develop a complete Solution, or 321 // else fail with an informative error. 322 // 323 // If a Solution is found, an implementing tool may persist it - typically into 324 // a "lock file" - and/or use it to write out a directory tree of dependencies, 325 // suitable to be a vendor directory, via CreateVendorTree. 326 type Solver interface { 327 // HashInputs hashes the unique inputs to this solver, returning the hash 328 // digest. It is guaranteed that, if the resulting digest is equal to the 329 // digest returned from a previous Solution.InputHash(), that that Solution 330 // is valid for this Solver's inputs. 331 // 332 // In such a case, it may not be necessary to run Solve() at all. 333 HashInputs() []byte 334 335 // Solve initiates a solving run. It will either complete successfully with 336 // a Solution, or fail with an informative error. 337 Solve() (Solution, error) 338 } 339 340 // Solve attempts to find a dependency solution for the given project, as 341 // represented by the SolveParameters with which this Solver was created. 342 // 343 // This is the entry point to the main gps workhorse. 344 func (s *solver) Solve() (Solution, error) { 345 // Set up a metrics object 346 s.mtr = newMetrics() 347 s.vUnify.mtr = s.mtr 348 349 // Prime the queues with the root project 350 err := s.selectRoot() 351 if err != nil { 352 return nil, err 353 } 354 355 all, err := s.solve() 356 357 s.mtr.pop() 358 var soln solution 359 if err == nil { 360 soln = solution{ 361 att: s.attempts, 362 } 363 364 soln.hd = s.HashInputs() 365 366 // Convert ProjectAtoms into LockedProjects 367 soln.p = make([]LockedProject, len(all)) 368 k := 0 369 for pa, pl := range all { 370 soln.p[k] = pa2lp(pa, pl) 371 k++ 372 } 373 } 374 375 s.traceFinish(soln, err) 376 if s.tl != nil { 377 s.mtr.dump(s.tl) 378 } 379 return soln, err 380 } 381 382 // solve is the top-level loop for the solving process. 383 func (s *solver) solve() (map[atom]map[string]struct{}, error) { 384 // Main solving loop 385 for { 386 bmi, has := s.nextUnselected() 387 388 if !has { 389 // no more packages to select - we're done. 390 break 391 } 392 393 // This split is the heart of "bimodal solving": we follow different 394 // satisfiability and selection paths depending on whether we've already 395 // selected the base project/repo that came off the unselected queue. 396 // 397 // (If we've already selected the project, other parts of the algorithm 398 // guarantee the bmi will contain at least one package from this project 399 // that has yet to be selected.) 400 if awp, is := s.sel.selected(bmi.id); !is { 401 s.mtr.push("new-atom") 402 // Analysis path for when we haven't selected the project yet - need 403 // to create a version queue. 404 queue, err := s.createVersionQueue(bmi) 405 if err != nil { 406 // Err means a failure somewhere down the line; try backtracking. 407 s.traceStartBacktrack(bmi, err, false) 408 s.mtr.pop() 409 if s.backtrack() { 410 // backtracking succeeded, move to the next unselected id 411 continue 412 } 413 return nil, err 414 } 415 416 if queue.current() == nil { 417 panic("canary - queue is empty, but flow indicates success") 418 } 419 420 awp := atomWithPackages{ 421 a: atom{ 422 id: queue.id, 423 v: queue.current(), 424 }, 425 pl: bmi.pl, 426 } 427 s.selectAtom(awp, false) 428 s.vqs = append(s.vqs, queue) 429 s.mtr.pop() 430 } else { 431 s.mtr.push("add-atom") 432 // We're just trying to add packages to an already-selected project. 433 // That means it's not OK to burn through the version queue for that 434 // project as we do when first selecting a project, as doing so 435 // would upend the guarantees on which all previous selections of 436 // the project are based (both the initial one, and any package-only 437 // ones). 438 439 // Because we can only safely operate within the scope of the 440 // single, currently selected version, we can skip looking for the 441 // queue and just use the version given in what came back from 442 // s.sel.selected(). 443 nawp := atomWithPackages{ 444 a: atom{ 445 id: bmi.id, 446 v: awp.a.v, 447 }, 448 pl: bmi.pl, 449 } 450 451 s.traceCheckPkgs(bmi) 452 err := s.check(nawp, true) 453 if err != nil { 454 // Err means a failure somewhere down the line; try backtracking. 455 s.traceStartBacktrack(bmi, err, true) 456 if s.backtrack() { 457 // backtracking succeeded, move to the next unselected id 458 continue 459 } 460 s.mtr.pop() 461 return nil, err 462 } 463 s.selectAtom(nawp, true) 464 // We don't add anything to the stack of version queues because the 465 // backtracker knows not to pop the vqstack if it backtracks 466 // across a pure-package addition. 467 s.mtr.pop() 468 } 469 } 470 471 // Getting this far means we successfully found a solution. Combine the 472 // selected projects and packages. 473 projs := make(map[atom]map[string]struct{}) 474 475 // Skip the first project. It's always the root, and that shouldn't be 476 // included in results. 477 for _, sel := range s.sel.projects[1:] { 478 pm, exists := projs[sel.a.a] 479 if !exists { 480 pm = make(map[string]struct{}) 481 projs[sel.a.a] = pm 482 } 483 484 for _, path := range sel.a.pl { 485 pm[path] = struct{}{} 486 } 487 } 488 return projs, nil 489 } 490 491 // selectRoot is a specialized selectAtom, used solely to initially 492 // populate the queues at the beginning of a solve run. 493 func (s *solver) selectRoot() error { 494 s.mtr.push("select-root") 495 // Push the root project onto the queue. 496 awp := s.rd.rootAtom() 497 s.sel.pushSelection(awp, true) 498 499 // If we're looking for root's deps, get it from opts and local root 500 // analysis, rather than having the sm do it 501 deps, err := s.intersectConstraintsWithImports(s.rd.combineConstraints(), s.rd.externalImportList()) 502 if err != nil { 503 // TODO(sdboyer) this could well happen; handle it with a more graceful error 504 panic(fmt.Sprintf("shouldn't be possible %s", err)) 505 } 506 507 for _, dep := range deps { 508 // If we have no lock, or if this dep isn't in the lock, then prefetch 509 // it. See longer explanation in selectAtom() for how we benefit from 510 // parallelism here. 511 if s.rd.needVersionsFor(dep.Ident.ProjectRoot) { 512 go s.b.SyncSourceFor(dep.Ident) 513 } 514 515 s.sel.pushDep(dependency{depender: awp.a, dep: dep}) 516 // Add all to unselected queue 517 heap.Push(s.unsel, bimodalIdentifier{id: dep.Ident, pl: dep.pl, fromRoot: true}) 518 } 519 520 s.traceSelectRoot(s.rd.rpt, deps) 521 s.mtr.pop() 522 return nil 523 } 524 525 func (s *solver) getImportsAndConstraintsOf(a atomWithPackages) ([]string, []completeDep, error) { 526 var err error 527 528 if s.rd.isRoot(a.a.id.ProjectRoot) { 529 panic("Should never need to recheck imports/constraints from root during solve") 530 } 531 532 // Work through the source manager to get project info and static analysis 533 // information. 534 m, _, err := s.b.GetManifestAndLock(a.a.id, a.a.v, s.rd.an) 535 if err != nil { 536 return nil, nil, err 537 } 538 539 ptree, err := s.b.ListPackages(a.a.id, a.a.v) 540 if err != nil { 541 return nil, nil, err 542 } 543 544 rm, em := ptree.ToReachMap(true, false, true, s.rd.ig) 545 // Use maps to dedupe the unique internal and external packages. 546 exmap, inmap := make(map[string]struct{}), make(map[string]struct{}) 547 548 for _, pkg := range a.pl { 549 inmap[pkg] = struct{}{} 550 for _, ipkg := range rm[pkg].Internal { 551 inmap[ipkg] = struct{}{} 552 } 553 } 554 555 var pl []string 556 // If lens are the same, then the map must have the same contents as the 557 // slice; no need to build a new one. 558 if len(inmap) == len(a.pl) { 559 pl = a.pl 560 } else { 561 pl = make([]string, 0, len(inmap)) 562 for pkg := range inmap { 563 pl = append(pl, pkg) 564 } 565 sort.Strings(pl) 566 } 567 568 // Add to the list those packages that are reached by the packages 569 // explicitly listed in the atom 570 for _, pkg := range a.pl { 571 // Skip ignored packages 572 if s.rd.ig[pkg] { 573 continue 574 } 575 576 ie, exists := rm[pkg] 577 if !exists { 578 // Missing package here *should* only happen if the target pkg was 579 // poisoned. Check the errors map 580 if importErr, eexists := em[pkg]; eexists { 581 return nil, nil, importErr 582 } 583 584 // Nope, it's actually full-on not there. 585 return nil, nil, fmt.Errorf("package %s does not exist within project %s", pkg, a.a.id.errString()) 586 } 587 588 for _, ex := range ie.External { 589 exmap[ex] = struct{}{} 590 } 591 } 592 593 reach := make([]string, 0, len(exmap)) 594 for pkg := range exmap { 595 reach = append(reach, pkg) 596 } 597 sort.Strings(reach) 598 599 deps := s.rd.ovr.overrideAll(m.DependencyConstraints()) 600 cd, err := s.intersectConstraintsWithImports(deps, reach) 601 return pl, cd, err 602 } 603 604 // intersectConstraintsWithImports takes a list of constraints and a list of 605 // externally reached packages, and creates a []completeDep that is guaranteed 606 // to include all packages named by import reach, using constraints where they 607 // are available, or Any() where they are not. 608 func (s *solver) intersectConstraintsWithImports(deps []workingConstraint, reach []string) ([]completeDep, error) { 609 // Create a radix tree with all the projects we know from the manifest 610 xt := radix.New() 611 for _, dep := range deps { 612 xt.Insert(string(dep.Ident.ProjectRoot), dep) 613 } 614 615 // Step through the reached packages; if they have prefix matches in 616 // the trie, assume (mostly) it's a correct correspondence. 617 dmap := make(map[ProjectRoot]completeDep) 618 for _, rp := range reach { 619 // If it's a stdlib-shaped package, skip it. 620 if internal.IsStdLib(rp) { 621 continue 622 } 623 624 // Look for a prefix match; it'll be the root project/repo containing 625 // the reached package 626 if pre, idep, match := xt.LongestPrefix(rp); match && isPathPrefixOrEqual(pre, rp) { 627 // Match is valid; put it in the dmap, either creating a new 628 // completeDep or appending it to the existing one for this base 629 // project/prefix. 630 dep := idep.(workingConstraint) 631 if cdep, exists := dmap[dep.Ident.ProjectRoot]; exists { 632 cdep.pl = append(cdep.pl, rp) 633 dmap[dep.Ident.ProjectRoot] = cdep 634 } else { 635 dmap[dep.Ident.ProjectRoot] = completeDep{ 636 workingConstraint: dep, 637 pl: []string{rp}, 638 } 639 } 640 continue 641 } 642 643 // No match. Let the SourceManager try to figure out the root 644 root, err := s.b.DeduceProjectRoot(rp) 645 if err != nil { 646 // Nothing we can do if we can't suss out a root 647 return nil, err 648 } 649 650 // Make a new completeDep with an open constraint, respecting overrides 651 pd := s.rd.ovr.override(root, ProjectProperties{Constraint: Any()}) 652 653 // Insert the pd into the trie so that further deps from this 654 // project get caught by the prefix search 655 xt.Insert(string(root), pd) 656 // And also put the complete dep into the dmap 657 dmap[root] = completeDep{ 658 workingConstraint: pd, 659 pl: []string{rp}, 660 } 661 } 662 663 // Dump all the deps from the map into the expected return slice 664 cdeps := make([]completeDep, len(dmap)) 665 k := 0 666 for _, cdep := range dmap { 667 cdeps[k] = cdep 668 k++ 669 } 670 671 return cdeps, nil 672 } 673 674 func (s *solver) createVersionQueue(bmi bimodalIdentifier) (*versionQueue, error) { 675 id := bmi.id 676 // If on the root package, there's no queue to make 677 if s.rd.isRoot(id.ProjectRoot) { 678 return newVersionQueue(id, nil, nil, s.b) 679 } 680 681 exists, err := s.b.SourceExists(id) 682 if err != nil { 683 return nil, err 684 } 685 if !exists { 686 exists, err = s.b.vendorCodeExists(id) 687 if err != nil { 688 return nil, err 689 } 690 if exists { 691 // Project exists only in vendor 692 // FIXME(sdboyer) this just totally doesn't work at all right now 693 } else { 694 return nil, fmt.Errorf("project '%s' could not be located", id) 695 } 696 } 697 698 var lockv Version 699 if len(s.rd.rlm) > 0 { 700 lockv, err = s.getLockVersionIfValid(id) 701 if err != nil { 702 // Can only get an error here if an upgrade was expressly requested on 703 // code that exists only in vendor 704 return nil, err 705 } 706 } 707 708 var prefv Version 709 if bmi.fromRoot { 710 // If this bmi came from the root, then we want to search through things 711 // with a dependency on it in order to see if any have a lock that might 712 // express a prefv 713 // 714 // TODO(sdboyer) nested loop; prime candidate for a cache somewhere 715 for _, dep := range s.sel.getDependenciesOn(bmi.id) { 716 // Skip the root, of course 717 if s.rd.isRoot(dep.depender.id.ProjectRoot) { 718 continue 719 } 720 721 _, l, err := s.b.GetManifestAndLock(dep.depender.id, dep.depender.v, s.rd.an) 722 if err != nil || l == nil { 723 // err being non-nil really shouldn't be possible, but the lock 724 // being nil is quite likely 725 continue 726 } 727 728 for _, lp := range l.Projects() { 729 if lp.Ident().eq(bmi.id) { 730 prefv = lp.Version() 731 } 732 } 733 } 734 735 // OTHER APPROACH - WRONG, BUT MAYBE USEFUL FOR REFERENCE? 736 // If this bmi came from the root, then we want to search the unselected 737 // queue to see if anything *else* wants this ident, in which case we 738 // pick up that prefv 739 //for _, bmi2 := range s.unsel.sl { 740 //// Take the first thing from the queue that's for the same ident, 741 //// and has a non-nil prefv 742 //if bmi.id.eq(bmi2.id) { 743 //if bmi2.prefv != nil { 744 //prefv = bmi2.prefv 745 //} 746 //} 747 //} 748 749 } else { 750 // Otherwise, just use the preferred version expressed in the bmi 751 prefv = bmi.prefv 752 } 753 754 q, err := newVersionQueue(id, lockv, prefv, s.b) 755 if err != nil { 756 // TODO(sdboyer) this particular err case needs to be improved to be ONLY for cases 757 // where there's absolutely nothing findable about a given project name 758 return nil, err 759 } 760 761 // Hack in support for revisions. 762 // 763 // By design, revs aren't returned from ListVersion(). Thus, if the dep in 764 // the bmi was has a rev constraint, it is (almost) guaranteed to fail, even 765 // if that rev does exist in the repo. So, detect a rev and push it into the 766 // vq here, instead. 767 // 768 // Happily, the solver maintains the invariant that constraints on a given 769 // ident cannot be incompatible, so we know that if we find one rev, then 770 // any other deps will have to also be on that rev (or Any). 771 // 772 // TODO(sdboyer) while this does work, it bypasses the interface-implied guarantees 773 // of the version queue, and is therefore not a great strategy for API 774 // coherency. Folding this in to a formal interface would be better. 775 switch tc := s.sel.getConstraint(bmi.id).(type) { 776 case Revision: 777 // We know this is the only thing that could possibly match, so put it 778 // in at the front - if it isn't there already. 779 if q.pi[0] != tc { 780 // Existence of the revision is guaranteed by checkRevisionExists(). 781 q.pi = append([]Version{tc}, q.pi...) 782 } 783 } 784 785 // Having assembled the queue, search it for a valid version. 786 s.traceCheckQueue(q, bmi, false, 1) 787 return q, s.findValidVersion(q, bmi.pl) 788 } 789 790 // findValidVersion walks through a versionQueue until it finds a version that 791 // satisfies the constraints held in the current state of the solver. 792 // 793 // The satisfiability checks triggered from here are constrained to operate only 794 // on those dependencies induced by the list of packages given in the second 795 // parameter. 796 func (s *solver) findValidVersion(q *versionQueue, pl []string) error { 797 if nil == q.current() { 798 // this case should not be reachable, but reflects improper solver state 799 // if it is, so panic immediately 800 panic("version queue is empty, should not happen") 801 } 802 803 faillen := len(q.fails) 804 805 for { 806 cur := q.current() 807 s.traceInfo("try %s@%s", q.id.errString(), cur) 808 err := s.check(atomWithPackages{ 809 a: atom{ 810 id: q.id, 811 v: cur, 812 }, 813 pl: pl, 814 }, false) 815 if err == nil { 816 // we have a good version, can return safely 817 return nil 818 } 819 820 if q.advance(err) != nil { 821 // Error on advance, have to bail out 822 break 823 } 824 if q.isExhausted() { 825 // Queue is empty, bail with error 826 break 827 } 828 } 829 830 s.fail(s.sel.getDependenciesOn(q.id)[0].depender.id) 831 832 // Return a compound error of all the new errors encountered during this 833 // attempt to find a new, valid version 834 return &noVersionError{ 835 pn: q.id, 836 fails: q.fails[faillen:], 837 } 838 } 839 840 // getLockVersionIfValid finds an atom for the given ProjectIdentifier from the 841 // root lock, assuming: 842 // 843 // 1. A root lock was provided 844 // 2. The general flag to change all projects was not passed 845 // 3. A flag to change this particular ProjectIdentifier was not passed 846 // 847 // If any of these three conditions are true (or if the id cannot be found in 848 // the root lock), then no atom will be returned. 849 func (s *solver) getLockVersionIfValid(id ProjectIdentifier) (Version, error) { 850 // If the project is specifically marked for changes, then don't look for a 851 // locked version. 852 if _, explicit := s.rd.chng[id.ProjectRoot]; explicit || s.rd.chngall { 853 // For projects with an upstream or cache repository, it's safe to 854 // ignore what's in the lock, because there's presumably more versions 855 // to be found and attempted in the repository. If it's only in vendor, 856 // though, then we have to try to use what's in the lock, because that's 857 // the only version we'll be able to get. 858 if exist, _ := s.b.SourceExists(id); exist { 859 // Upgrades mean breaking the lock 860 s.b.breakLock() 861 return nil, nil 862 } 863 864 // However, if a change was *expressly* requested for something that 865 // exists only in vendor, then that guarantees we don't have enough 866 // information to complete a solution. In that case, error out. 867 if explicit { 868 return nil, &missingSourceFailure{ 869 goal: id, 870 prob: "Cannot upgrade %s, as no source repository could be found.", 871 } 872 } 873 } 874 875 lp, exists := s.rd.rlm[id.ProjectRoot] 876 if !exists { 877 return nil, nil 878 } 879 880 constraint := s.sel.getConstraint(id) 881 v := lp.Version() 882 if !constraint.Matches(v) { 883 var found bool 884 if tv, ok := v.(Revision); ok { 885 // If we only have a revision from the root's lock, allow matching 886 // against other versions that have that revision 887 for _, pv := range s.vUnify.pairRevision(id, tv) { 888 if constraint.Matches(pv) { 889 v = pv 890 found = true 891 break 892 } 893 } 894 //} else if _, ok := constraint.(Revision); ok { 895 //// If the current constraint is itself a revision, and the lock gave 896 //// an unpaired version, see if they match up 897 //// 898 //if u, ok := v.(UnpairedVersion); ok { 899 //pv := s.sm.pairVersion(id, u) 900 //if constraint.Matches(pv) { 901 //v = pv 902 //found = true 903 //} 904 //} 905 } 906 907 if !found { 908 // No match found, which means we're going to be breaking the lock 909 s.b.breakLock() 910 return nil, nil 911 } 912 } 913 914 return v, nil 915 } 916 917 // backtrack works backwards from the current failed solution to find the next 918 // solution to try. 919 func (s *solver) backtrack() bool { 920 if len(s.vqs) == 0 { 921 // nothing to backtrack to 922 return false 923 } 924 925 s.mtr.push("backtrack") 926 for { 927 for { 928 if len(s.vqs) == 0 { 929 // no more versions, nowhere further to backtrack 930 return false 931 } 932 if s.vqs[len(s.vqs)-1].failed { 933 break 934 } 935 936 s.vqs, s.vqs[len(s.vqs)-1] = s.vqs[:len(s.vqs)-1], nil 937 938 // Pop selections off until we get to a project. 939 var proj bool 940 var awp atomWithPackages 941 for !proj { 942 awp, proj = s.unselectLast() 943 s.traceBacktrack(awp.bmi(), !proj) 944 } 945 } 946 947 // Grab the last versionQueue off the list of queues 948 q := s.vqs[len(s.vqs)-1] 949 950 // Walk back to the next project 951 awp, proj := s.unselectLast() 952 if !proj { 953 panic("canary - *should* be impossible to have a pkg-only selection here") 954 } 955 956 if !q.id.eq(awp.a.id) { 957 panic("canary - version queue stack and selected project stack are misaligned") 958 } 959 960 // Advance the queue past the current version, which we know is bad 961 // TODO(sdboyer) is it feasible to make available the failure reason here? 962 if q.advance(nil) == nil && !q.isExhausted() { 963 // Search for another acceptable version of this failed dep in its queue 964 s.traceCheckQueue(q, awp.bmi(), true, 0) 965 if s.findValidVersion(q, awp.pl) == nil { 966 // Found one! Put it back on the selected queue and stop 967 // backtracking 968 969 // reusing the old awp is fine 970 awp.a.v = q.current() 971 s.selectAtom(awp, false) 972 break 973 } 974 } 975 976 s.traceBacktrack(awp.bmi(), false) 977 //s.traceInfo("no more versions of %s, backtracking", q.id.errString()) 978 979 // No solution found; continue backtracking after popping the queue 980 // we just inspected off the list 981 // GC-friendly pop pointer elem in slice 982 s.vqs, s.vqs[len(s.vqs)-1] = s.vqs[:len(s.vqs)-1], nil 983 } 984 985 s.mtr.pop() 986 // Backtracking was successful if loop ended before running out of versions 987 if len(s.vqs) == 0 { 988 return false 989 } 990 s.attempts++ 991 return true 992 } 993 994 func (s *solver) nextUnselected() (bimodalIdentifier, bool) { 995 if len(s.unsel.sl) > 0 { 996 return s.unsel.sl[0], true 997 } 998 999 return bimodalIdentifier{}, false 1000 } 1001 1002 func (s *solver) unselectedComparator(i, j int) bool { 1003 ibmi, jbmi := s.unsel.sl[i], s.unsel.sl[j] 1004 iname, jname := ibmi.id, jbmi.id 1005 1006 // Most important thing is pushing package additions ahead of project 1007 // additions. Package additions can't walk their version queue, so all they 1008 // do is narrow the possibility of success; better to find out early and 1009 // fast if they're going to fail than wait until after we've done real work 1010 // on a project and have to backtrack across it. 1011 1012 // FIXME the impl here is currently O(n) in the number of selections; it 1013 // absolutely cannot stay in a hot sorting path like this 1014 // FIXME while other solver invariants probably protect us from it, this 1015 // call-out means that it's possible for external state change to invalidate 1016 // heap invariants. 1017 _, isel := s.sel.selected(iname) 1018 _, jsel := s.sel.selected(jname) 1019 1020 if isel && !jsel { 1021 return true 1022 } 1023 if !isel && jsel { 1024 return false 1025 } 1026 1027 if iname.eq(jname) { 1028 return false 1029 } 1030 1031 _, ilock := s.rd.rlm[iname.ProjectRoot] 1032 _, jlock := s.rd.rlm[jname.ProjectRoot] 1033 1034 switch { 1035 case ilock && !jlock: 1036 return true 1037 case !ilock && jlock: 1038 return false 1039 case ilock && jlock: 1040 return iname.less(jname) 1041 } 1042 1043 // Now, sort by number of available versions. This will trigger network 1044 // activity, but at this point we know that the project we're looking at 1045 // isn't locked by the root. And, because being locked by root is the only 1046 // way avoid that call when making a version queue, we know we're gonna have 1047 // to pay that cost anyway. 1048 1049 // We can safely ignore an err from listVersions here because, if there is 1050 // an actual problem, it'll be noted and handled somewhere else saner in the 1051 // solving algorithm. 1052 ivl, _ := s.b.listVersions(iname) 1053 jvl, _ := s.b.listVersions(jname) 1054 iv, jv := len(ivl), len(jvl) 1055 1056 // Packages with fewer versions to pick from are less likely to benefit from 1057 // backtracking, so deal with them earlier in order to minimize the amount 1058 // of superfluous backtracking through them we do. 1059 switch { 1060 case iv == 0 && jv != 0: 1061 return true 1062 case iv != 0 && jv == 0: 1063 return false 1064 case iv != jv: 1065 return iv < jv 1066 } 1067 1068 // Finally, if all else fails, fall back to comparing by name 1069 return iname.less(jname) 1070 } 1071 1072 func (s *solver) fail(id ProjectIdentifier) { 1073 // TODO(sdboyer) does this need updating, now that we have non-project package 1074 // selection? 1075 1076 // skip if the root project 1077 if !s.rd.isRoot(id.ProjectRoot) { 1078 // just look for the first (oldest) one; the backtracker will necessarily 1079 // traverse through and pop off any earlier ones 1080 for _, vq := range s.vqs { 1081 if vq.id.eq(id) { 1082 vq.failed = true 1083 return 1084 } 1085 } 1086 } 1087 } 1088 1089 // selectAtom pulls an atom into the selection stack, alongside some of 1090 // its contained packages. New resultant dependency requirements are added to 1091 // the unselected priority queue. 1092 // 1093 // Behavior is slightly diffferent if pkgonly is true. 1094 func (s *solver) selectAtom(a atomWithPackages, pkgonly bool) { 1095 s.mtr.push("select-atom") 1096 s.unsel.remove(bimodalIdentifier{ 1097 id: a.a.id, 1098 pl: a.pl, 1099 }) 1100 1101 pl, deps, err := s.getImportsAndConstraintsOf(a) 1102 if err != nil { 1103 // This shouldn't be possible; other checks should have ensured all 1104 // packages and deps are present for any argument passed to this method. 1105 panic(fmt.Sprintf("canary - shouldn't be possible %s", err)) 1106 } 1107 // Assign the new internal package list into the atom, then push it onto the 1108 // selection stack 1109 a.pl = pl 1110 s.sel.pushSelection(a, pkgonly) 1111 1112 // If this atom has a lock, pull it out so that we can potentially inject 1113 // preferred versions into any bmis we enqueue 1114 // 1115 // TODO(sdboyer) making this call here could be the first thing to trigger 1116 // network activity...maybe? if so, can we mitigate by deferring the work to 1117 // queue consumption time? 1118 _, l, _ := s.b.GetManifestAndLock(a.a.id, a.a.v, s.rd.an) 1119 var lmap map[ProjectIdentifier]Version 1120 if l != nil { 1121 lmap = make(map[ProjectIdentifier]Version) 1122 for _, lp := range l.Projects() { 1123 lmap[lp.Ident()] = lp.Version() 1124 } 1125 } 1126 1127 for _, dep := range deps { 1128 // Root can come back up here if there's a project-level cycle. 1129 // Satisfiability checks have already ensured invariants are maintained, 1130 // so we know we can just skip it here. 1131 if s.rd.isRoot(dep.Ident.ProjectRoot) { 1132 continue 1133 } 1134 // If this is dep isn't in the lock, do some prefetching. (If it is, we 1135 // might be able to get away with zero network activity for it, so don't 1136 // prefetch). This provides an opportunity for some parallelism wins, on 1137 // two fronts: 1138 // 1139 // 1. Because this loop may have multiple deps in it, we could end up 1140 // simultaneously fetching both in the background while solving proceeds 1141 // 1142 // 2. Even if only one dep gets prefetched here, the worst case is that 1143 // that same dep comes out of the unselected queue next, and we gain a 1144 // few microseconds before blocking later. Best case, the dep doesn't 1145 // come up next, but some other dep comes up that wasn't prefetched, and 1146 // both fetches proceed in parallel. 1147 if s.rd.needVersionsFor(dep.Ident.ProjectRoot) { 1148 go s.b.SyncSourceFor(dep.Ident) 1149 } 1150 1151 s.sel.pushDep(dependency{depender: a.a, dep: dep}) 1152 // Go through all the packages introduced on this dep, selecting only 1153 // the ones where the only depper on them is what the preceding line just 1154 // pushed in. Then, put those into the unselected queue. 1155 rpm := s.sel.getRequiredPackagesIn(dep.Ident) 1156 var newp []string 1157 for _, pkg := range dep.pl { 1158 // Just one means that the dep we're visiting is the sole importer. 1159 if rpm[pkg] == 1 { 1160 newp = append(newp, pkg) 1161 } 1162 } 1163 1164 if len(newp) > 0 { 1165 bmi := bimodalIdentifier{ 1166 id: dep.Ident, 1167 pl: newp, 1168 // This puts in a preferred version if one's in the map, else 1169 // drops in the zero value (nil) 1170 prefv: lmap[dep.Ident], 1171 } 1172 heap.Push(s.unsel, bmi) 1173 } 1174 } 1175 1176 s.traceSelect(a, pkgonly) 1177 s.mtr.pop() 1178 } 1179 1180 func (s *solver) unselectLast() (atomWithPackages, bool) { 1181 s.mtr.push("unselect") 1182 awp, first := s.sel.popSelection() 1183 heap.Push(s.unsel, bimodalIdentifier{id: awp.a.id, pl: awp.pl}) 1184 1185 _, deps, err := s.getImportsAndConstraintsOf(awp) 1186 if err != nil { 1187 // This shouldn't be possible; other checks should have ensured all 1188 // packages and deps are present for any argument passed to this method. 1189 panic(fmt.Sprintf("canary - shouldn't be possible %s", err)) 1190 } 1191 1192 for _, dep := range deps { 1193 // Skip popping if the dep is the root project, which can occur if 1194 // there's a project-level import cycle. (This occurs frequently with 1195 // e.g. kubernetes and docker) 1196 if s.rd.isRoot(dep.Ident.ProjectRoot) { 1197 continue 1198 } 1199 s.sel.popDep(dep.Ident) 1200 1201 // if no parents/importers, remove from unselected queue 1202 if s.sel.depperCount(dep.Ident) == 0 { 1203 s.unsel.remove(bimodalIdentifier{id: dep.Ident, pl: dep.pl}) 1204 } 1205 } 1206 1207 s.mtr.pop() 1208 return awp, first 1209 } 1210 1211 // simple (temporary?) helper just to convert atoms into locked projects 1212 func pa2lp(pa atom, pkgs map[string]struct{}) LockedProject { 1213 lp := LockedProject{ 1214 pi: pa.id, 1215 } 1216 1217 switch v := pa.v.(type) { 1218 case UnpairedVersion: 1219 lp.v = v 1220 case Revision: 1221 lp.r = v 1222 case versionPair: 1223 lp.v = v.v 1224 lp.r = v.r 1225 default: 1226 panic("unreachable") 1227 } 1228 1229 lp.pkgs = make([]string, len(pkgs)) 1230 k := 0 1231 1232 pr := string(pa.id.ProjectRoot) 1233 trim := pr + "/" 1234 for pkg := range pkgs { 1235 if pkg == string(pa.id.ProjectRoot) { 1236 lp.pkgs[k] = "." 1237 } else { 1238 lp.pkgs[k] = strings.TrimPrefix(pkg, trim) 1239 } 1240 k++ 1241 } 1242 sort.Strings(lp.pkgs) 1243 1244 return lp 1245 }