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  }