dep/satisfy.go

282 строки
7.9 KiB
Go

package gps
// check performs constraint checks on the provided atom. The set of checks
// differ slightly depending on whether the atom is pkgonly, or if it's the
// entire project being added for the first time.
//
// The goal is to determine whether selecting the atom would result in a state
// where all the solver requirements are still satisfied.
func (s *solver) check(a atomWithPackages, pkgonly bool) error {
pa := a.a
if nilpa == pa {
// This shouldn't be able to happen, but if it does, it unequivocally
// indicates a logical bug somewhere, so blowing up is preferable
panic("canary - checking version of empty ProjectAtom")
}
// If we're pkgonly, then base atom was already determined to be allowable,
// so we can skip the checkAtomAllowable step.
if !pkgonly {
if err := s.checkAtomAllowable(pa); err != nil {
s.traceInfo(err)
return err
}
}
if err := s.checkRequiredPackagesExist(a); err != nil {
s.traceInfo(err)
return err
}
deps, err := s.getImportsAndConstraintsOf(a)
if err != nil {
// An err here would be from the package fetcher; pass it straight back
// TODO(sdboyer) can we traceInfo this?
return err
}
// TODO(sdboyer) this deps list contains only packages not already selected
// from the target atom (assuming one is selected at all). It's fine for
// now, but won't be good enough when we get around to doing static
// analysis.
for _, dep := range deps {
if err := s.checkIdentMatches(a, dep); err != nil {
s.traceInfo(err)
return err
}
if err := s.checkDepsConstraintsAllowable(a, dep); err != nil {
s.traceInfo(err)
return err
}
if err := s.checkDepsDisallowsSelected(a, dep); err != nil {
s.traceInfo(err)
return err
}
// TODO(sdboyer) decide how to refactor in order to re-enable this. Checking for
// revision existence is important...but kinda obnoxious.
//if err := s.checkRevisionExists(a, dep); err != nil {
//s.traceInfo(err)
//return err
//}
if err := s.checkPackageImportsFromDepExist(a, dep); err != nil {
s.traceInfo(err)
return err
}
// TODO(sdboyer) add check that fails if adding this atom would create a loop
}
return nil
}
// checkAtomAllowable ensures that an atom itself is acceptable with respect to
// the constraints established by the current solution.
func (s *solver) checkAtomAllowable(pa atom) error {
constraint := s.sel.getConstraint(pa.id)
if s.b.matches(pa.id, constraint, pa.v) {
return nil
}
// TODO(sdboyer) collect constraint failure reason (wait...aren't we, below?)
deps := s.sel.getDependenciesOn(pa.id)
var failparent []dependency
for _, dep := range deps {
if !s.b.matches(pa.id, dep.dep.Constraint, pa.v) {
s.fail(dep.depender.id)
failparent = append(failparent, dep)
}
}
err := &versionNotAllowedFailure{
goal: pa,
failparent: failparent,
c: constraint,
}
return err
}
// checkRequiredPackagesExist ensures that all required packages enumerated by
// existing dependencies on this atom are actually present in the atom.
func (s *solver) checkRequiredPackagesExist(a atomWithPackages) error {
ptree, err := s.b.ListPackages(a.a.id, a.a.v)
if err != nil {
// TODO(sdboyer) handle this more gracefully
return err
}
deps := s.sel.getDependenciesOn(a.a.id)
fp := make(map[string]errDeppers)
// We inspect these in a bit of a roundabout way, in order to incrementally
// build up the failure we'd return if there is, indeed, a missing package.
// TODO(sdboyer) rechecking all of these every time is wasteful. Is there a shortcut?
for _, dep := range deps {
for _, pkg := range dep.dep.pl {
if errdep, seen := fp[pkg]; seen {
errdep.deppers = append(errdep.deppers, dep.depender)
fp[pkg] = errdep
} else {
perr, has := ptree.Packages[pkg]
if !has || perr.Err != nil {
fp[pkg] = errDeppers{
err: perr.Err,
deppers: []atom{dep.depender},
}
}
}
}
}
if len(fp) > 0 {
return &checkeeHasProblemPackagesFailure{
goal: a.a,
failpkg: fp,
}
}
return nil
}
// checkDepsConstraintsAllowable checks that the constraints of an atom on a
// given dep are valid with respect to existing constraints.
func (s *solver) checkDepsConstraintsAllowable(a atomWithPackages, cdep completeDep) error {
dep := cdep.workingConstraint
constraint := s.sel.getConstraint(dep.Ident)
// Ensure the constraint expressed by the dep has at least some possible
// intersection with the intersection of existing constraints.
if s.b.matchesAny(dep.Ident, constraint, dep.Constraint) {
return nil
}
siblings := s.sel.getDependenciesOn(dep.Ident)
// No admissible versions - visit all siblings and identify the disagreement(s)
var failsib []dependency
var nofailsib []dependency
for _, sibling := range siblings {
if !s.b.matchesAny(dep.Ident, sibling.dep.Constraint, dep.Constraint) {
s.fail(sibling.depender.id)
failsib = append(failsib, sibling)
} else {
nofailsib = append(nofailsib, sibling)
}
}
return &disjointConstraintFailure{
goal: dependency{depender: a.a, dep: cdep},
failsib: failsib,
nofailsib: nofailsib,
c: constraint,
}
}
// checkDepsDisallowsSelected ensures that an atom's constraints on a particular
// dep are not incompatible with the version of that dep that's already been
// selected.
func (s *solver) checkDepsDisallowsSelected(a atomWithPackages, cdep completeDep) error {
dep := cdep.workingConstraint
selected, exists := s.sel.selected(dep.Ident)
if exists && !s.b.matches(dep.Ident, dep.Constraint, selected.a.v) {
s.fail(dep.Ident)
return &constraintNotAllowedFailure{
goal: dependency{depender: a.a, dep: cdep},
v: selected.a.v,
}
}
return nil
}
// checkIdentMatches ensures that the LocalName of a dep introduced by an atom,
// has the same NetworkName as what's already been selected (assuming anything's
// been selected).
//
// In other words, this ensures that the solver never simultaneously selects two
// identifiers with the same local name, but that disagree about where their
// network source is.
func (s *solver) checkIdentMatches(a atomWithPackages, cdep completeDep) error {
dep := cdep.workingConstraint
if cur, exists := s.names[dep.Ident.ProjectRoot]; exists {
if cur != dep.Ident.netName() {
deps := s.sel.getDependenciesOn(a.a.id)
// Fail all the other deps, as there's no way atom can ever be
// compatible with them
for _, d := range deps {
s.fail(d.depender.id)
}
return &sourceMismatchFailure{
shared: dep.Ident.ProjectRoot,
sel: deps,
current: cur,
mismatch: dep.Ident.netName(),
prob: a.a,
}
}
}
return nil
}
// checkPackageImportsFromDepExist ensures that, if the dep is already selected,
// the newly-required set of packages being placed on it exist and are valid.
func (s *solver) checkPackageImportsFromDepExist(a atomWithPackages, cdep completeDep) error {
sel, is := s.sel.selected(cdep.workingConstraint.Ident)
if !is {
// dep is not already selected; nothing to do
return nil
}
ptree, err := s.b.ListPackages(sel.a.id, sel.a.v)
if err != nil {
// TODO(sdboyer) handle this more gracefully
return err
}
e := &depHasProblemPackagesFailure{
goal: dependency{
depender: a.a,
dep: cdep,
},
v: sel.a.v,
prob: make(map[string]error),
}
for _, pkg := range cdep.pl {
perr, has := ptree.Packages[pkg]
if !has || perr.Err != nil {
if has {
e.prob[pkg] = perr.Err
} else {
e.prob[pkg] = nil
}
}
}
if len(e.prob) > 0 {
return e
}
return nil
}
// checkRevisionExists ensures that if a dependency is constrained by a
// revision, that that revision actually exists.
func (s *solver) checkRevisionExists(a atomWithPackages, cdep completeDep) error {
r, isrev := cdep.Constraint.(Revision)
if !isrev {
// Constraint is not a revision; nothing to do
return nil
}
present, _ := s.b.RevisionPresentIn(cdep.Ident, r)
if present {
return nil
}
return &nonexistentRevisionFailure{
goal: dependency{
depender: a.a,
dep: cdep,
},
r: r,
}
}