| // Copyright 2017 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| 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") |
| } |
| |
| s.mtr.push("satisfy") |
| var err error |
| defer func() { |
| if err != nil { |
| s.traceInfo(err) |
| } |
| s.mtr.pop() |
| }() |
| |
| // 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 { |
| return err |
| } |
| } |
| |
| if err = s.checkRequiredPackagesExist(a); err != nil { |
| return err |
| } |
| |
| var deps []completeDep |
| _, deps, err = s.getImportsAndConstraintsOf(a) |
| if err != nil { |
| // An err here would be from the package fetcher; pass it straight back |
| 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 { |
| return err |
| } |
| if err = s.checkRootCaseConflicts(a, dep); err != nil { |
| return err |
| } |
| if err = s.checkDepsConstraintsAllowable(a, dep); err != nil { |
| return err |
| } |
| if err = s.checkDepsDisallowsSelected(a, dep); err != nil { |
| return err |
| } |
| if err = s.checkRevisionExists(a, dep); err != nil { |
| return err |
| } |
| if err = s.checkPackageImportsFromDepExist(a, dep); err != nil { |
| 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 constraint.Matches(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 !dep.dep.Constraint.Matches(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 constraint.MatchesAny(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 !sibling.dep.Constraint.MatchesAny(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 && !dep.Constraint.Matches(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 Source 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 curid, has := s.sel.getIdentFor(dep.Ident.ProjectRoot); has && !curid.equiv(dep.Ident) { |
| 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: curid.normalizedSource(), |
| mismatch: dep.Ident.normalizedSource(), |
| prob: a.a, |
| } |
| } |
| |
| return nil |
| } |
| |
| // checkRootCaseConflicts ensures that the ProjectRoot specified in the completeDep |
| // does not have case conflicts with any existing dependencies. |
| // |
| // We only need to check the ProjectRoot, rather than any packages therein, as |
| // the later check for package existence is case-sensitive. |
| func (s *solver) checkRootCaseConflicts(a atomWithPackages, cdep completeDep) error { |
| pr := cdep.workingConstraint.Ident.ProjectRoot |
| hasConflict, current := s.sel.findCaseConflicts(pr) |
| if !hasConflict { |
| return nil |
| } |
| |
| curid, _ := s.sel.getIdentFor(current) |
| deps := s.sel.getDependenciesOn(curid) |
| for _, d := range deps { |
| s.fail(d.depender.id) |
| } |
| |
| // If a project has multiple packages that import each other, we treat that |
| // as establishing a canonical case variant for the ProjectRoot. It's possible, |
| // however, that that canonical variant is not the same one that others |
| // imported it under. If that's the situation, then we'll have arrived here |
| // when visiting the project, not its dependers, having misclassified its |
| // internal imports as external. That means the atomWithPackages will |
| // be the wrong case variant induced by the importers, and the cdep will be |
| // a link pointing back at the canonical case variant. |
| // |
| // If this is the case, use a special failure, wrongCaseFailure, that |
| // makes a stronger statement as to the correctness of case variants. |
| // |
| // TODO(sdboyer) This approach to marking failure is less than great, as |
| // this will mark the current atom as failed, as well, causing the |
| // backtracker to work through it. While that could prove fruitful, it's |
| // quite likely just to be wasted effort. Addressing this - if that's a good |
| // idea - would entail creating another path back out of checking to enable |
| // backjumping directly to the incorrect importers. |
| if current == a.a.id.ProjectRoot { |
| return &wrongCaseFailure{ |
| correct: pr, |
| goal: dependency{depender: a.a, dep: cdep}, |
| badcase: deps, |
| } |
| } |
| |
| return &caseMismatchFailure{ |
| goal: dependency{depender: a.a, dep: cdep}, |
| current: current, |
| failsib: deps, |
| } |
| } |
| |
| // 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, |
| } |
| } |