dep/solver.go

457 строки
12 KiB
Go
Исходник Обычный вид История

2016-03-15 06:36:42 +03:00
package vsolver
import (
"container/heap"
"fmt"
)
2016-03-17 19:45:36 +03:00
//type SolveFailure uint
2016-03-15 06:36:42 +03:00
2016-03-17 19:45:36 +03:00
//const (
// Indicates that no version solution could be found
//NoVersionSolution SolveFailure = 1 << iota
//IncompatibleVersionType
//)
2016-03-15 06:36:42 +03:00
2016-03-16 23:34:09 +03:00
func NewSolver(sm SourceManager) Solver {
2016-03-15 06:36:42 +03:00
return &solver{
sm: sm,
2016-03-15 06:36:42 +03:00
}
}
2016-03-16 23:34:09 +03:00
// solver is a backtracking-style SAT solver.
2016-03-15 06:36:42 +03:00
type solver struct {
2016-03-16 23:34:09 +03:00
sm SourceManager
2016-03-30 20:51:23 +03:00
latest map[ProjectName]struct{}
2016-03-15 06:36:42 +03:00
sel *selection
unsel *unselected
2016-03-17 19:45:36 +03:00
versions []*versionQueue
rp ProjectInfo
attempts int
2016-03-15 06:36:42 +03:00
}
2016-03-30 20:51:23 +03:00
func (s *solver) Solve(root ProjectInfo, toUpgrade []ProjectName) Result {
// local overrides would need to be handled first.
// TODO local overrides! heh
s.rp = root
2016-03-15 06:36:42 +03:00
for _, v := range toUpgrade {
s.latest[v] = struct{}{}
}
// Initialize queues
s.sel = &selection{
2016-03-30 20:51:23 +03:00
deps: make(map[ProjectName][]Dependency),
}
2016-03-15 06:36:42 +03:00
s.unsel = &unselected{
2016-03-30 20:51:23 +03:00
sl: make([]ProjectName, 0),
2016-03-15 06:36:42 +03:00
cmp: s.unselectedComparator,
}
heap.Init(s.unsel)
// Prime the queues with the root project
2016-03-30 20:51:23 +03:00
s.selectVersion(s.rp.pa)
2016-03-15 06:36:42 +03:00
// Prep is done; actually run the solver
var r Result
r.Projects, r.SolveFailure = s.solve()
return r
2016-03-15 06:36:42 +03:00
}
2016-03-30 20:51:23 +03:00
func (s *solver) solve() ([]ProjectAtom, error) {
2016-03-15 06:36:42 +03:00
for {
ref, has := s.nextUnselected()
if !has {
2016-03-15 06:36:42 +03:00
// no more packages to select - we're done. bail out
break
}
queue, err := s.createVersionQueue(ref)
if err != nil {
// Err means a failure somewhere down the line; try backtracking.
if s.backtrack() {
// backtracking succeeded, move to the next unselected ref
continue
}
// TODO handle different failure types appropriately, lolzies
2016-03-16 23:34:09 +03:00
return nil, err
2016-03-15 06:36:42 +03:00
}
if queue.current() == emptyVersion {
2016-03-24 04:25:35 +03:00
panic("canary - queue is empty, but flow indicates success")
}
s.selectVersion(ProjectAtom{
Name: queue.ref,
Version: queue.current(),
})
s.versions = append(s.versions, queue)
2016-03-15 06:36:42 +03:00
}
// Getting this far means we successfully found a solution
2016-03-30 20:51:23 +03:00
var projs []ProjectAtom
for _, p := range s.sel.projects {
projs = append(projs, p)
}
return projs, nil
2016-03-15 06:36:42 +03:00
}
2016-03-30 20:51:23 +03:00
func (s *solver) createVersionQueue(ref ProjectName) (*versionQueue, error) {
//pretty.Printf("Creating VersionQueue for %q\n", ref)
2016-03-15 06:36:42 +03:00
// If on the root package, there's no queue to make
2016-03-30 20:51:23 +03:00
if ref == s.rp.Name() {
2016-03-17 19:45:36 +03:00
return newVersionQueue(ref, nil, s.sm)
2016-03-15 06:36:42 +03:00
}
2016-03-16 23:34:09 +03:00
if !s.sm.ProjectExists(ref) {
2016-03-15 06:36:42 +03:00
// TODO this check needs to incorporate/admit the possibility that the
// upstream no longer exists, but there's something valid in vendor/
return nil, newSolveError(fmt.Sprintf("Project '%s' could not be located.", ref), cannotResolve)
}
lockv := s.getLockVersionIfValid(ref)
2016-03-17 19:45:36 +03:00
q, err := newVersionQueue(ref, lockv, s.sm)
if err != nil {
// TODO this particular err case needs to be improved to be ONLY for cases
// where there's absolutely nothing findable about a given project name
return nil, err
2016-03-15 06:36:42 +03:00
}
return q, s.findValidVersion(q)
}
// findValidVersion walks through a VersionQueue until it finds a version that's
// valid, as adjudged by the current constraints.
2016-03-17 19:45:36 +03:00
func (s *solver) findValidVersion(q *versionQueue) error {
2016-03-15 06:36:42 +03:00
var err error
if emptyVersion == q.current() {
2016-03-15 06:36:42 +03:00
// TODO this case shouldn't be reachable, but panic here as a canary
panic("version queue is empty, should not happen")
}
//var name ProjectName
2016-03-15 06:36:42 +03:00
for {
//pretty.Printf("Checking next version for %q\n", q.ref)
err = s.checkVersion(ProjectAtom{
Name: q.ref,
Version: q.current(),
})
2016-03-15 06:36:42 +03:00
if err == nil {
// we have a good version, can return safely
//pretty.Printf("Found valid version %q for %q\n", q.current().Name, q.current().Version.Info)
2016-03-15 06:36:42 +03:00
return nil
}
// store name so we can fail on it if it turns out to be the last
// possible version in the queue
//name = q.current().Name
err = q.advance()
if err != nil {
2016-03-24 04:26:18 +03:00
// Error on advance, have to bail out
break
}
if q.isExhausted() {
// Queue is empty, bail with error
err = newSolveError(fmt.Sprintf("Exhausted queue for %q without finding a satisfactory version.", q.ref), mustResolve)
break
}
2016-03-15 06:36:42 +03:00
}
2016-03-30 20:51:23 +03:00
s.fail(s.sel.getDependenciesOn(q.ref)[0].Depender.Name)
2016-03-15 06:36:42 +03:00
return err
}
2016-03-30 20:51:23 +03:00
func (s *solver) getLockVersionIfValid(ref ProjectName) *ProjectAtom {
lockver := s.rp.GetProjectAtom(ref)
2016-03-15 06:36:42 +03:00
if lockver == nil {
// Nothing in the lock about this version, so nothing to validate
return nil
}
constraint := s.sel.getConstraint(ref)
if !constraint.Admits(lockver.Version) {
2016-03-15 06:36:42 +03:00
// TODO msg?
return nil
//} else {
// TODO msg?
}
return nil
}
2016-03-30 20:51:23 +03:00
func (s *solver) checkVersion(pi ProjectAtom) error {
if emptyProjectAtom == pi {
2016-03-15 06:36:42 +03:00
// TODO we should protect against this case elsewhere, but for now panic
// to canary when it's a problem
2016-03-30 20:51:23 +03:00
panic("checking version of empty ProjectAtom")
2016-03-15 06:36:42 +03:00
}
2016-03-30 20:51:23 +03:00
constraint := s.sel.getConstraint(pi.Name)
if !constraint.Admits(pi.Version) {
2016-03-30 20:51:23 +03:00
deps := s.sel.getDependenciesOn(pi.Name)
2016-03-15 06:36:42 +03:00
for _, dep := range deps {
// TODO grok why this check is needed
if !dep.Dep.Constraint.Admits(pi.Version) {
2016-03-30 20:51:23 +03:00
s.fail(dep.Depender.Name)
2016-03-15 06:36:42 +03:00
}
}
// TODO msg
return &noVersionError{
2016-03-30 20:51:23 +03:00
pn: pi.Name,
2016-03-15 06:36:42 +03:00
c: constraint,
deps: deps,
}
}
2016-03-30 20:51:23 +03:00
if !s.sm.ProjectExists(pi.Name) {
2016-03-15 06:36:42 +03:00
// Can get here if the lock file specifies a now-nonexistent project
// TODO this check needs to incorporate/accept the possibility that the
// upstream no longer exists, but there's something valid in vendor/
2016-03-30 20:51:23 +03:00
return newSolveError(fmt.Sprintf("Project '%s' could not be located.", pi.Name), cannotResolve)
2016-03-15 06:36:42 +03:00
}
deps, err := s.getDependenciesOf(pi)
2016-03-15 06:36:42 +03:00
if err != nil {
// An err here would be from the package fetcher; pass it straight back
return err
}
for _, dep := range deps {
// TODO dart skips "magic" deps here; do we need that?
// TODO maybe differentiate between the confirmed items on the list, and
// the one we're speculatively adding? or it may be fine b/c we know
// it's the last one
2016-03-30 20:51:23 +03:00
selfAndSiblings := append(s.sel.getDependenciesOn(dep.Name), Dependency{Depender: pi, Dep: dep})
2016-03-15 06:36:42 +03:00
2016-03-30 20:51:23 +03:00
constraint = s.sel.getConstraint(dep.Name)
2016-03-15 06:36:42 +03:00
// Ensure the constraint expressed by the dep has at least some possible
// overlap with existing constraints.
if !constraint.AdmitsAny(dep.Constraint) {
2016-03-15 06:36:42 +03:00
// No match - visit all siblings and identify the disagreement(s)
for _, sibling := range selfAndSiblings[:len(selfAndSiblings)-1] {
if !sibling.Dep.Constraint.AdmitsAny(dep.Constraint) {
2016-03-30 20:51:23 +03:00
s.fail(sibling.Depender.Name)
2016-03-15 06:36:42 +03:00
}
}
// TODO msg
return &disjointConstraintFailure{
2016-03-30 20:51:23 +03:00
pn: dep.Name,
2016-03-15 06:36:42 +03:00
deps: selfAndSiblings,
}
}
2016-03-30 20:51:23 +03:00
selected, exists := s.sel.selected(dep.Name)
if exists && !dep.Constraint.Admits(selected.Version) {
2016-03-30 20:51:23 +03:00
s.fail(dep.Name)
2016-03-15 06:36:42 +03:00
// TODO msg
return &noVersionError{
2016-03-30 20:51:23 +03:00
pn: dep.Name,
2016-03-15 06:36:42 +03:00
c: dep.Constraint,
deps: selfAndSiblings,
}
}
// At this point, dart/pub do things related to 'required' dependencies,
// which is about solving loops (i think) and so mostly not something we
// have to care about.
}
return nil
}
2016-03-30 20:51:23 +03:00
// getDependenciesOf returns the dependencies of the given ProjectAtom, mediated
2016-03-15 06:36:42 +03:00
// through any overrides dictated by the root project.
//
// If it's the root project, also includes dev dependencies, etc.
2016-03-30 20:51:23 +03:00
func (s *solver) getDependenciesOf(pi ProjectAtom) ([]ProjectDep, error) {
info, err := s.sm.GetProjectInfo(pi)
2016-03-15 06:36:42 +03:00
if err != nil {
// TODO revisit this once a decision is made about better-formed errors;
// question is, do we expect the fetcher to pass back simple errors, or
// well-typed solver errors?
return nil, err
}
deps := info.GetDependencies()
2016-03-30 20:51:23 +03:00
if s.rp.Name() == pi.Name {
2016-03-15 06:36:42 +03:00
// Root package has more things to pull in
deps = append(deps, info.GetDevDependencies()...)
2016-03-15 06:36:42 +03:00
// TODO add overrides here...if we impl the concept (which we should)
}
// TODO we have to validate well-formedness of a project's manifest
// somewhere. this may be a good spot. alternatively, the fetcher may
// validate well-formedness, whereas here we validate availability of the
// named deps here. (the latter is sorta what pub does here)
return deps, nil
}
// backtrack works backwards from the current failed solution to find the next
// solution to try.
func (s *solver) backtrack() bool {
if len(s.versions) == 0 {
// nothing to backtrack to
return false
}
for {
for {
if len(s.versions) == 0 {
// no more versions, nowhere further to backtrack
return false
}
if s.versions[len(s.versions)-1].failed {
break
}
// pop last vqueue off of versions
//q, s.versions := s.versions[len(s.versions)-1], s.versions[:len(s.versions)-1]
// pub asserts here that the last in s.sel's ids is == q.current
s.versions = s.versions[:len(s.versions)-1]
s.unselectLast()
2016-03-15 06:36:42 +03:00
}
// Grab the last VersionQueue off the list of queues
q := s.versions[len(s.versions)-1]
2016-03-15 06:36:42 +03:00
// another assert that the last in s.sel's ids is == q.current
s.unselectLast()
// Search for another acceptable version of this failed dep in its queue
if err := s.findValidVersion(q); err == nil {
// Found one! Put it back on the selected queue and stop
// backtracking
s.selectVersion(ProjectAtom{
Name: q.ref,
Version: q.current(),
})
break
}
// No solution found; continue backtracking after popping the last
// version off the list
// GC-friendly pop pointer elem in slice
s.versions, s.versions[len(s.versions)-1] = s.versions[:len(s.versions)-1], nil
}
// Backtracking was successful if loop ended before running out of versions
if len(s.versions) == 0 {
return false
2016-03-15 06:36:42 +03:00
}
s.attempts++
return true
2016-03-15 06:36:42 +03:00
}
2016-03-30 20:51:23 +03:00
func (s *solver) nextUnselected() (ProjectName, bool) {
if len(s.unsel.sl) > 0 {
return s.unsel.sl[0], true
}
return "", false
}
2016-03-15 06:36:42 +03:00
func (s *solver) unselectedComparator(i, j int) bool {
iname, jname := s.unsel.sl[i], s.unsel.sl[j]
if iname == jname {
return false
}
2016-03-30 20:51:23 +03:00
rname := s.rp.Name()
2016-03-15 06:36:42 +03:00
// *always* put root project first
2016-03-30 20:51:23 +03:00
if iname == rname {
2016-03-15 06:36:42 +03:00
return true
}
2016-03-30 20:51:23 +03:00
if jname == rname {
2016-03-15 06:36:42 +03:00
return false
}
2016-03-30 20:51:23 +03:00
ilock, jlock := s.rp.GetProjectAtom(iname) == nil, s.rp.GetProjectAtom(jname) == nil
2016-03-15 06:36:42 +03:00
if ilock && !jlock {
return true
}
if !ilock && jlock {
return false
}
//if ilock && jlock {
//return iname < jname
//}
// TODO impl version-counting for next set of checks. but until then...
return iname < jname
}
2016-03-30 20:51:23 +03:00
func (s *solver) fail(name ProjectName) {
// skip if the root project
2016-03-30 20:51:23 +03:00
if s.rp.Name() == name {
return
}
for _, vq := range s.versions {
2016-03-30 20:51:23 +03:00
if vq.ref == name {
vq.failed = true
// just look for the first (oldest) one; the backtracker will
// necessarily traverse through and pop off any earlier ones
// TODO ...right?
return
}
}
}
2016-03-30 20:51:23 +03:00
func (s *solver) selectVersion(pa ProjectAtom) {
s.unsel.remove(pa.Name)
s.sel.projects = append(s.sel.projects, pa)
2016-03-15 06:36:42 +03:00
2016-03-30 20:51:23 +03:00
deps, err := s.getDependenciesOf(pa)
if err != nil {
// if we're choosing a package that has errors getting its deps, there's
// a bigger problem
// TODO try to create a test that hits this
panic("shouldn't be possible")
}
for _, dep := range deps {
2016-03-30 20:51:23 +03:00
siblingsAndSelf := append(s.sel.getDependenciesOn(dep.Name), Dependency{Depender: pa, Dep: dep})
s.sel.deps[dep.Name] = siblingsAndSelf
// add project to unselected queue if this is the first dep on it -
// otherwise it's already in there, or been selected
if len(siblingsAndSelf) == 1 {
2016-03-30 20:51:23 +03:00
//pretty.Printf("pushing %q onto unselected queue\n", dep.Name)
heap.Push(s.unsel, dep.Name)
//pretty.Println("unsel after push:", s.unsel.sl)
}
}
2016-03-15 06:36:42 +03:00
}
func (s *solver) unselectLast() {
2016-03-30 20:51:23 +03:00
var pa ProjectAtom
pa, s.sel.projects = s.sel.projects[len(s.sel.projects)-1], s.sel.projects[:len(s.sel.projects)-1]
heap.Push(s.unsel, pa.Name)
//pretty.Println("unsel after restore:", s.unsel.sl)
2016-03-15 06:36:42 +03:00
2016-03-30 20:51:23 +03:00
deps, err := s.getDependenciesOf(pa)
if err != nil {
// if we're choosing a package that has errors getting its deps, there's
// a bigger problem
// TODO try to create a test that hits this
panic("shouldn't be possible")
}
for _, dep := range deps {
2016-03-30 20:51:23 +03:00
siblings := s.sel.getDependenciesOn(pa.Name)
s.sel.deps[pa.Name] = siblings[:len(siblings)-1]
// if no siblings, remove from unselected queue
if len(siblings) == 0 {
2016-03-30 20:51:23 +03:00
s.unsel.remove(dep.Name)
}
}
2016-03-15 06:36:42 +03:00
}