зеркало из https://github.com/golang/dep.git
372 строки
9.3 KiB
Go
372 строки
9.3 KiB
Go
|
package vsolver
|
||
|
|
||
|
import (
|
||
|
"container/heap"
|
||
|
"fmt"
|
||
|
|
||
|
"github.com/Masterminds/semver"
|
||
|
)
|
||
|
|
||
|
type SolveFailure uint
|
||
|
|
||
|
const (
|
||
|
// Indicates that no version solution could be found
|
||
|
NoVersionSolution SolveFailure = 1 << iota
|
||
|
IncompatibleVersionType
|
||
|
)
|
||
|
|
||
|
func NewSolver(pf PackageFetcher) Solver {
|
||
|
return &solver{
|
||
|
pf: pf,
|
||
|
sel: &selection{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type solver struct {
|
||
|
pf PackageFetcher
|
||
|
latest map[ProjectIdentifier]struct{}
|
||
|
sel *selection
|
||
|
unsel *unselected
|
||
|
rs Spec
|
||
|
rl Lock
|
||
|
versions []*VersionQueue
|
||
|
}
|
||
|
|
||
|
func (s *solver) Solve(rootSpec Spec, rootLock Lock, toUpgrade []ProjectIdentifier) Result {
|
||
|
// local overrides would need to be handled first. ofc, these don't exist yet
|
||
|
|
||
|
for _, v := range toUpgrade {
|
||
|
s.latest[v] = struct{}{}
|
||
|
}
|
||
|
|
||
|
s.unsel = &unselected{
|
||
|
sl: make([]ProjectIdentifier, 0),
|
||
|
cmp: s.unselectedComparator,
|
||
|
}
|
||
|
heap.Init(s.unsel)
|
||
|
|
||
|
s.rs = rootSpec
|
||
|
s.rl = rootLock
|
||
|
|
||
|
_, err := s.doSolve()
|
||
|
}
|
||
|
|
||
|
func (s *solver) doSolve() ([]ProjectID, error) {
|
||
|
for {
|
||
|
ref := s.sel.nextUnselected()
|
||
|
if ref == "" {
|
||
|
// no more packages to select - we're done. bail out
|
||
|
// TODO compile things in s.sel into a list of ProjectIDs, and return
|
||
|
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
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *solver) createVersionQueue(ref ProjectIdentifier) (*VersionQueue, error) {
|
||
|
// If on the root package, there's no queue to make
|
||
|
if ref == s.rs.ID {
|
||
|
return NewVersionQueue(ref, nil, nil), nil
|
||
|
}
|
||
|
|
||
|
if !s.pf.ProjectExists(ref) {
|
||
|
// 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)
|
||
|
|
||
|
versions, err := s.pf.ListVersions(ref)
|
||
|
if err != nil {
|
||
|
// TODO can there actually be an err here? probably just e.g. an
|
||
|
// fs-level err
|
||
|
return nil, err // pass it straight back up
|
||
|
}
|
||
|
|
||
|
// TODO probably use an actual container/list
|
||
|
// TODO should probably just make the fetcher return semver already, and
|
||
|
// update ProjectID to suit
|
||
|
var list []*ProjectID
|
||
|
for _, pi := range versions {
|
||
|
_, err := semver.NewVersion(pi.Version)
|
||
|
if err != nil {
|
||
|
// couldn't parse version; moving on
|
||
|
// TODO log this at all? would be info/debug-type, at best
|
||
|
continue
|
||
|
}
|
||
|
// this is the lockv, push it to the front
|
||
|
if lockv.Version == pi.Version {
|
||
|
list = append([]*ProjectID{&pi}, list...)
|
||
|
} else {
|
||
|
list = append(list, &pi)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
q := NewVersionQueue(ref, s.checkVersion, list)
|
||
|
return q, s.findValidVersion(q)
|
||
|
}
|
||
|
|
||
|
// findValidVersion walks through a VersionQueue until it finds a version that's
|
||
|
// valid, as adjudged by the current constraints.
|
||
|
func (s *solver) findValidVersion(q *VersionQueue) error {
|
||
|
var err error
|
||
|
if q.current() == nil {
|
||
|
// TODO this case shouldn't be reachable, but panic here as a canary
|
||
|
panic("version queue is empty, should not happen")
|
||
|
}
|
||
|
|
||
|
for {
|
||
|
err = s.checkVersion(q.current())
|
||
|
if err == nil {
|
||
|
// we have a good version, can return safely
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
q.next()
|
||
|
}
|
||
|
|
||
|
s.fail(s.sel.getDependenciesOn(q.current().ID)[0].Depender.ID)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func (s *solver) getLockVersionIfValid(ref ProjectIdentifier) *ProjectID {
|
||
|
lockver := s.rl.GetProject(ref)
|
||
|
if lockver == nil {
|
||
|
// Nothing in the lock about this version, so nothing to validate
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
constraint := s.sel.getConstraint(ref)
|
||
|
if !constraint.Allows(lockver.Version) {
|
||
|
// TODO msg?
|
||
|
return nil
|
||
|
//} else {
|
||
|
// TODO msg?
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// createProjectRevisionIterator creates an iterator that retrieves metadata for
|
||
|
// a given ref, one version at a time.
|
||
|
func (s *solver) createProjectRevisionIterator(ref ProjectIdentifier, lockv *ProjectID) (*projectRevisionIterator, error) {
|
||
|
|
||
|
// TODO keep track of all the available revs, as reported by the pf?
|
||
|
return &projectRevisionIterator{
|
||
|
cur: lockv,
|
||
|
haslock: lockv == nil,
|
||
|
ref: ref,
|
||
|
pf: s.pf,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (s *solver) checkVersion(pi *ProjectID) error {
|
||
|
if pi == nil {
|
||
|
// TODO we should protect against this case elsewhere, but for now panic
|
||
|
// to canary when it's a problem
|
||
|
panic("checking version of nil ProjectID pointer")
|
||
|
}
|
||
|
|
||
|
constraint := s.sel.getConstraint(pi.ID)
|
||
|
if !constraint.Allows(pi.Version) {
|
||
|
deps := s.sel.getDependenciesOn(pi.ID)
|
||
|
for _, dep := range deps {
|
||
|
// TODO grok why this check is needed
|
||
|
if !dep.Dep.Constraint.Allows(pi.Version) {
|
||
|
s.fail(dep.Depender.ID)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TODO msg
|
||
|
return &noVersionError{
|
||
|
pi: *pi,
|
||
|
c: constraint,
|
||
|
deps: deps,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if !s.pf.ProjectExists(pi.ID) {
|
||
|
// 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/
|
||
|
return newSolveError(fmt.Sprintf("Project '%s' could not be located.", pi.ID), cannotResolve)
|
||
|
}
|
||
|
|
||
|
deps, err := s.getDependenciesOf(pi)
|
||
|
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
|
||
|
selfAndSiblings := append(s.sel.getDependenciesOn(dep.ID), Dependency{Depender: *pi, Dep: dep})
|
||
|
|
||
|
constraint = s.sel.getConstraint(dep.ID)
|
||
|
// Ensure the constraint expressed by the dep has at least some possible
|
||
|
// overlap with existing constraints.
|
||
|
if !constraint.Intersects(dep.Constraint) {
|
||
|
// No match - visit all siblings and identify the disagreement(s)
|
||
|
for _, sibling := range selfAndSiblings[:len(selfAndSiblings)-1] {
|
||
|
if !sibling.Dep.Constraint.Intersects(dep.Constraint) {
|
||
|
s.fail(sibling.Depender.ID)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TODO msg
|
||
|
return &disjointConstraintFailure{
|
||
|
id: dep.ID,
|
||
|
deps: selfAndSiblings,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
selected := s.sel.selected(dep.ID)
|
||
|
if selected != nil && !dep.Constraint.Allows(selected.Version) {
|
||
|
s.fail(dep.ID)
|
||
|
|
||
|
// TODO msg
|
||
|
return &noVersionError{
|
||
|
pi: dep.ProjectID,
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// getDependenciesOf returns the dependencies of the given ProjectID, mediated
|
||
|
// through any overrides dictated by the root project.
|
||
|
//
|
||
|
// If it's the root project, also includes dev dependencies, etc.
|
||
|
func (s *solver) getDependenciesOf(pi *ProjectID) ([]ProjectDep, error) {
|
||
|
info, err := s.pf.GetProjectInfo(pi.ID)
|
||
|
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()
|
||
|
if s.rs.ID == pi.ID {
|
||
|
// Root package has more things to pull in
|
||
|
deps = append(deps, info.GetDevDependencies())
|
||
|
|
||
|
// 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.sel.unselectLast()
|
||
|
}
|
||
|
|
||
|
var pi *ProjectID
|
||
|
var q *VersionQueue
|
||
|
|
||
|
q := s.versions[len(s.versions)-1]
|
||
|
id := q.current().ID
|
||
|
// another assert that the last in s.sel's ids is == q.current
|
||
|
s.sel.unselectLast()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *solver) unselectedComparator(i, j int) bool {
|
||
|
iname, jname := s.unsel.sl[i], s.unsel.sl[j]
|
||
|
|
||
|
if iname == jname {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// *always* put root project first
|
||
|
if iname == s.rs.ID {
|
||
|
return true
|
||
|
}
|
||
|
if jname == s.rs.ID {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
ilock, jlock := s.rl.GetProject(iname) == nil, s.rl.GetProject(jname) == nil
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
func (s *solver) fail(id ProjectIdentifier) {
|
||
|
|
||
|
}
|
||
|
|
||
|
func (s *solver) choose(id ProjectID) {
|
||
|
|
||
|
}
|
||
|
|
||
|
type projectRevisionIterator struct {
|
||
|
cur *ProjectID
|
||
|
haslock bool
|
||
|
ref ProjectIdentifier
|
||
|
pf PackageFetcher
|
||
|
}
|
||
|
|
||
|
func (pri *projectRevisionIterator) next() bool {
|
||
|
// TODO pull the next item from the pf and put it into the current item
|
||
|
}
|
||
|
|
||
|
func (pri *projectRevisionIterator) current() *ProjectID {
|
||
|
return pri.cur
|
||
|
}
|