def solve(self): # type: () -> SolverResult """ Finds a set of dependencies that match the root package's constraints, or raises an error if no such set is available. """ start = time.time() self._add_incompatibility( Incompatibility( [Term(Constraint(self._source.root, Range()), False)], RootCause())) self._propagate(self._source.root) packages_tried = 0 max_tries = -1 while True: if packages_tried == max_tries: raise SolverFailure( "Stopping, {} packages tried.".format(max_tries)) if not self._run(): break packages_tried += 1 logger.info("Version solving took {:.3f} seconds.".format(time.time() - start)) logger.info("Tried {} solutions.".format( self._solution.attempted_solutions)) return SolverResult(self._solution.decisions, self._solution.attempted_solutions)
def _choose_package_version(self): # type: () -> Union[Hashable, None] """ Tries to select a version of a required package. Returns the name of the package whose incompatibilities should be propagated by _propagate(), or None indicating that version solving is complete and a solution has been found. """ term = self._next_term_to_try() if not term: return versions = self._source.versions_for(term.package, term.constraint.constraint) if not versions: # If there are no versions that satisfy the constraint, # add an incompatibility that indicates that. self._add_incompatibility( Incompatibility([term], NoVersionsCause())) return term.package version = versions[0] conflict = False for incompatibility in self._source.incompatibilities_for( term.package, version): self._add_incompatibility(incompatibility) # If an incompatibility is already satisfied, then selecting version # would cause a conflict. # # We'll continue adding its dependencies, then go back to # unit propagation which will guide us to choose a better version. conflict = conflict or all([ iterm.package == term.package or self._solution.satisfies(iterm) for iterm in incompatibility.terms ]) if not conflict: self._solution.decide(term.package, version) logger.info("selecting {} ({})".format( term.package.req.extras_name, str(version))) return term.package
def incompatibilities_for( self, package, version ): # type: (Hashable, Any) -> List[Incompatibility] """ Returns the incompatibilities of a given package and version """ dependencies = self.dependencies_for(package, version) package_constraint = Constraint(package, Range(version, version, True, True)) incompatibilities = [] for dependency in dependencies: constraint = self.convert_dependency(dependency) if not isinstance(constraint, Constraint): constraint = Constraint(package, constraint) incompatibility = Incompatibility( [Term(package_constraint, True), Term(constraint, False)], cause=DependencyCause(), ) incompatibilities.append(incompatibility) return incompatibilities
def _resolve_conflict( self, incompatibility): # type: (Incompatibility) -> Incompatibility """ Given an incompatibility that's satisfied by _solution, The `conflict resolution`_ constructs a new incompatibility that encapsulates the root cause of the conflict and backtracks _solution until the new incompatibility will allow _propagate() to deduce new assignments. Adds the new incompatibility to _incompatibilities and returns it. .. _conflict resolution: https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution """ logger.info("conflict: {}".format(incompatibility)) new_incompatibility = False while not incompatibility.is_failure(): # The term in incompatibility.terms that was most recently satisfied by # _solution. most_recent_term = None # The earliest assignment in _solution such that incompatibility is # satisfied by _solution up to and including this assignment. most_recent_satisfier = None # The difference between most_recent_satisfier and most_recent_term; # that is, the versions that are allowed by most_recent_satisfier and not # by most_recent_term. This is None if most_recent_satisfier totally # satisfies most_recent_term. difference = None # The decision level of the earliest assignment in _solution *before* # most_recent_satisfier such that incompatibility is satisfied by # _solution up to and including this assignment plus # most_recent_satisfier. # # Decision level 1 is the level where the root package was selected. It's # safe to go back to decision level 0, but stopping at 1 tends to produce # better error messages, because references to the root package end up # closer to the final conclusion that no solution exists. previous_satisfier_level = 1 for term in incompatibility.terms: satisfier = self._solution.satisfier(term) if most_recent_satisfier is None: most_recent_term = term most_recent_satisfier = satisfier elif most_recent_satisfier.index < satisfier.index: previous_satisfier_level = max( previous_satisfier_level, most_recent_satisfier.decision_level) most_recent_term = term most_recent_satisfier = satisfier difference = None else: previous_satisfier_level = max(previous_satisfier_level, satisfier.decision_level) if most_recent_term == term: # If most_recent_satisfier doesn't satisfy most_recent_term on its # own, then the next-most-recent satisfier may be the one that # satisfies the remainder. difference = most_recent_satisfier.difference( most_recent_term) if difference is not None: previous_satisfier_level = max( previous_satisfier_level, self._solution.satisfier( difference.inverse).decision_level, ) # If most_recent_identifier is the only satisfier left at its decision # level, or if it has no cause (indicating that it's a decision rather # than a derivation), then incompatibility is the root cause. We then # backjump to previous_satisfier_level, where incompatibility is # guaranteed to allow _propagate to produce more assignments. if (previous_satisfier_level < most_recent_satisfier.decision_level or most_recent_satisfier.cause is None): self._solution.backtrack(previous_satisfier_level) if new_incompatibility: self._add_incompatibility(incompatibility) return incompatibility # Create a new incompatibility by combining incompatibility with the # incompatibility that caused most_recent_satisfier to be assigned. Doing # this iteratively constructs an incompatibility that's guaranteed to be # true (that is, we know for sure no solution will satisfy the # incompatibility) while also approximating the intuitive notion of the # "root cause" of the conflict. new_terms = [] for term in incompatibility.terms: if term != most_recent_term: new_terms.append(term) for term in most_recent_satisfier.cause.terms: if term.package != most_recent_satisfier.package: new_terms.append(term) # The most_recent_satisfier may not satisfy most_recent_term on its own # if there are a collection of constraints on most_recent_term that # only satisfy it together. For example, if most_recent_term is # `foo ^1.0.0` and _solution contains `[foo >=1.0.0, # foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even # though it doesn't totally satisfy `foo ^1.0.0`. # # In this case, we add `not (most_recent_satisfier \ most_recent_term)` to # the incompatibility as well, See the `algorithm documentation`_ for # details. # # .. _algorithm documentation: https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution if difference is not None: new_terms.append(difference.inverse) incompatibility = Incompatibility( new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause)) new_incompatibility = True partially = "" if difference is None else " partially" bang = "!" logger.info("{} {} is{} satisfied by {}".format( bang, most_recent_term, partially, most_recent_satisfier)) logger.info('{} which is caused by "{}"'.format( bang, most_recent_satisfier.cause)) logger.info("{} thus: {}".format(bang, incompatibility)) raise SolverFailure(incompatibility)