diff --git a/britney2/installability/builder.py b/britney2/installability/builder.py index 924b751..65ca548 100644 --- a/britney2/installability/builder.py +++ b/britney2/installability/builder.py @@ -18,6 +18,7 @@ from itertools import product from britney2.utils import ifilter_except, iter_except, get_dependency_solvers from britney2.installability.solver import InstallabilitySolver +from britney2.installability.universe import BinaryPackageRelation, BinaryPackageUniverse def build_installability_tester(suite_info, archs): @@ -305,10 +306,11 @@ class InstallabilityTesterBuilder(object): if b in reverse_package_table: del reverse_package_table[b] - eqv_table = self._build_eqv_packages_table(package_table, reverse_package_table) + relations, eqv_table = self._build_eqv_packages_table(package_table, reverse_package_table) - return InstallabilitySolver(package_table, - reverse_package_table, + universe = BinaryPackageUniverse(relations) + + return InstallabilitySolver(universe, self._testing, self._broken, self._essentials, @@ -369,6 +371,8 @@ class InstallabilityTesterBuilder(object): find_eqv_table = defaultdict(list) eqv_table = {} + relations = {} + emptyset = frozenset() for pkg in reverse_package_table: rdeps = reverse_package_table[pkg][2] @@ -380,12 +384,21 @@ class InstallabilityTesterBuilder(object): ekey = (deps, con, rdeps) find_eqv_table[ekey].append(pkg) - for pkg_list in find_eqv_table.values(): + for pkg_relations, pkg_list in find_eqv_table.items(): + rel = BinaryPackageRelation(pkg_relations[0], pkg_relations[1], reverse_package_table[pkg_list[0]][0]) if len(pkg_list) < 2: + relations[pkg_list[0]] = rel continue eqv_set = frozenset(pkg_list) for pkg in pkg_list: eqv_table[pkg] = eqv_set + relations[pkg] = rel + + for pkg, forward_relations in package_table.items(): + if pkg in relations: + continue + rel = BinaryPackageRelation(forward_relations[0], forward_relations[1], emptyset) + relations[pkg] = rel - return eqv_table + return relations, eqv_table diff --git a/britney2/installability/solver.py b/britney2/installability/solver.py index 81f414f..4a9238b 100644 --- a/britney2/installability/solver.py +++ b/britney2/installability/solver.py @@ -132,15 +132,10 @@ def compute_scc(graph): class InstallabilitySolver(InstallabilityTester): - def __init__(self, universe, revuniverse, testing, broken, essentials, - eqv_table): + def __init__(self, universe, testing, broken, essentials, eqv_table): """Create a new installability solver - universe is a dict mapping package tuples to their - dependencies and conflicts. - - revuniverse is a dict mapping package tuples to their reverse - dependencies and reverse conflicts. + universe is a BinaryPackageUniverse. testing is a (mutable) set of package tuples that determines which of the packages in universe are currently in testing. @@ -152,13 +147,11 @@ class InstallabilitySolver(InstallabilityTester): - NB: arch:all packages are "re-mapped" to given architecture. (simplifies caches and dependency checking) """ - super().__init__(universe, revuniverse, testing, - broken, essentials, eqv_table) + super().__init__(universe, testing, broken, essentials, eqv_table) def solve_groups(self, groups): sat_in_testing = self._testing.isdisjoint universe = self._universe - revuniverse = self._revuniverse result = [] emitted = set() queue = deque() @@ -191,9 +184,9 @@ class InstallabilitySolver(InstallabilityTester): oldcons = set() newcons = set() for r in rms: - oldcons.update(universe[r][1]) + oldcons.update(universe.negative_dependencies_of(r)) for a in adds: - newcons.update(universe[a][1]) + newcons.update(universe.negative_dependencies_of(a)) current = newcons & oldcons oldcons -= current newcons -= current @@ -213,11 +206,11 @@ class InstallabilitySolver(InstallabilityTester): order[key]['before'].add(other) order[other]['after'].add(key) - for r in ifilter_only(revuniverse, rms): + for r in rms: # The binaries have reverse dependencies in testing; # check if we can/should migrate them first. - for rdep in revuniverse[r][0]: - for depgroup in universe[rdep][0]: + for rdep in universe.reverse_dependencies_of(r): + for depgroup in universe.dependencies_of(rdep): rigid = depgroup - going_out if not sat_in_testing(rigid): # (partly) satisfied by testing, assume it is okay @@ -236,7 +229,7 @@ class InstallabilitySolver(InstallabilityTester): # Check if this item should migrate before others # (e.g. because they depend on a new [version of a] # binary provided by this item). - for depgroup in universe[a][0]: + for depgroup in universe.dependencies_of(a): rigid = depgroup - going_out if not sat_in_testing(rigid): # (partly) satisfied by testing, assume it is okay diff --git a/britney2/installability/tester.py b/britney2/installability/tester.py index e0a3355..b6a2bab 100644 --- a/britney2/installability/tester.py +++ b/britney2/installability/tester.py @@ -22,15 +22,10 @@ from britney2.utils import iter_except class InstallabilityTester(object): - def __init__(self, universe, revuniverse, testing, broken, essentials, - eqv_table): + def __init__(self, universe, testing, broken, essentials, eqv_table): """Create a new installability tester - universe is a dict mapping package ids to their - dependencies and conflicts. - - revuniverse is a table containing all packages with reverse - relations mapping them to their reverse relations. + universe is a BinaryPackageUniverse testing is a (mutable) set of package ids that determines which of the packages in universe are currently in testing. @@ -49,7 +44,6 @@ class InstallabilityTester(object): self._testing = testing self._broken = broken self._essentials = essentials - self._revuniverse = revuniverse self._eqv_table = eqv_table self._stats = InstallabilityStats() logger_name = ".".join((self.__class__.__module__, self.__class__.__name__)) @@ -122,10 +116,7 @@ class InstallabilityTester(object): :return: A set containing the package ids all of the reverse dependencies of the input package. The result is suite agnostic. """ - revuniverse = self._revuniverse - if pkg_id not in revuniverse: - return frozenset() - return revuniverse[pkg_id][0] + return self._universe.reverse_dependencies_of(pkg_id) def negative_dependencies_of(self, pkg_id): """Returns the set of negative dependencies of a given package @@ -138,7 +129,7 @@ class InstallabilityTester(object): :return: A set containing the package ids all of the negative dependencies of the input package. The result is suite agnostic. """ - return self._universe[pkg_id][1] + return self._universe.negative_dependencies_of(pkg_id) def dependencies_of(self, pkg_id): """Returns the set of dependencies of a given package @@ -147,7 +138,7 @@ class InstallabilityTester(object): :return: A set containing the package ids all of the dependencies of the input package. The result is suite agnostic. """ - return self._universe[pkg_id][0] + return self._universe.dependencies_of(pkg_id) def any_of_these_are_in_testing(self, pkgs): """Test if at least one package of a given set is in testing @@ -214,7 +205,7 @@ class InstallabilityTester(object): # Removes a package from the "pseudo-essential set" del self._cache_ess[pkg_id.architecture] - if pkg_id not in self._revuniverse: + if not self._universe.reverse_dependencies_of(pkg_id): # no reverse relations - safe return True if pkg_id not in self._broken and pkg_id in self._cache_inst: @@ -483,9 +474,9 @@ class InstallabilityTester(object): # While we have guaranteed dependencies (in check), examine all # of them. for cur in iter_except(check.pop, IndexError): - (deps, cons) = universe[cur] + relations = universe.relations_of(cur) - if cons: + if relations.negative_dependencies: # Conflicts? if cur in never: # cur adds a (reverse) conflict, so check if cur @@ -498,11 +489,11 @@ class InstallabilityTester(object): return False # We must install cur for the package to be installable, # so "obviously" we can never choose any of its conflicts - never.update(cons & testing) + never.update(relations.negative_dependencies & testing) # depgroup can be satisfied by picking something that is # already in musts - lets pick that (again). :) - for depgroup in not_satisfied(deps): + for depgroup in not_satisfied(relations.dependencies): # Of all the packages listed in the relation remove those that # are either: @@ -587,8 +578,9 @@ class InstallabilityTester(object): for choice in not_satisfied(ess_choices): b = False for c in choice: - if universe[c][1] <= ess_never and \ - not any(not_satisfied(universe[c][0])): + relations = universe.relations_of(c) + if relations.negative_dependencies <= ess_never and \ + not any(not_satisfied(relations.dependencies)): ess_base.append(c) b = True break @@ -599,7 +591,7 @@ class InstallabilityTester(object): break for x in start: - ess_never.update(universe[x][1]) + ess_never.update(universe.negative_dependencies_of(x)) self._cache_ess[arch] = (frozenset(start), frozenset(ess_never), frozenset(ess_choices)) return self._cache_ess[arch] @@ -612,7 +604,7 @@ class InstallabilityTester(object): for pkg in universe: (pkg_name, pkg_version, pkg_arch) = pkg - deps, con = universe[pkg] + relations = universe.relations_of(pkg) arch_stats = graph_stats[pkg_arch] arch_stats.nodes += 1 @@ -621,8 +613,8 @@ class InstallabilityTester(object): eqv = [e for e in eqv_table[pkg] if e.architecture == pkg_arch] arch_stats.eqv_nodes += len(eqv) - arch_stats.add_dep_edges(deps) - arch_stats.add_con_edges(con) + arch_stats.add_dep_edges(relations.dependencies) + arch_stats.add_con_edges(relations.negative_dependencies) for stat in graph_stats.values(): stat.compute_all() diff --git a/britney2/installability/universe.py b/britney2/installability/universe.py new file mode 100644 index 0000000..09bf474 --- /dev/null +++ b/britney2/installability/universe.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012 Niels Thykier + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + + +class BinaryPackageRelation(object): + """All relations of a given binary package""" + + __slots__ = ['dependencies', 'negative_dependencies', 'reverse_dependencies'] + + def __init__(self, dependencies, negative_dependencies, reverse_dependencies): + self.dependencies = dependencies + self.negative_dependencies = negative_dependencies + self.reverse_dependencies = reverse_dependencies + + +class BinaryPackageUniverse(object): + """A "universe" of all binary packages and their relations + + The package universe is a read-only ("immutable") data structure + that knows of all binary packages and their internal relations. + The relations are either in Conjunctive Normal Form (CNF) represented + via sets of sets of the package ids or simply sets of package ids. + + Being immutable, the universe does *not* track stateful data such + as "which package is in what suite?" nor "is this package installable + in that suite?". + """ + + def __init__(self, relations): + self._relations = relations + + def dependencies_of(self, pkg_id): + """Returns the set of dependencies of a given package + + :param pkg_id: The BinaryPackageId of a binary package. + :return: A set containing the package ids all of the dependencies + of the input package in CNF. + """ + return self._relations[pkg_id].dependencies + + def negative_dependencies_of(self, pkg_id): + """Returns the set of negative dependencies of a given package + + Note that there is no "reverse_negative_dependencies_of" method, + since negative dependencies have no "direction" unlike positive + dependencies. + + :param pkg_id: The BinaryPackageId of a binary package. + :return: A set containing the package ids all of the negative + dependencies of the input package. + """ + return self._relations[pkg_id].negative_dependencies + + def reverse_dependencies_of(self, pkg_id): + """Returns the set of reverse dependencies of a given package + + Note that a package is considered a reverse dependency of the + given package as long as at least one of its dependency relations + *could* be satisfied by the given package. + + :param pkg_id: The BinaryPackageId of a binary package. + :return: A set containing the package ids all of the reverse + dependencies of the input package. + """ + return self._relations[pkg_id].reverse_dependencies + + def are_equivalent(self, pkg_id1, pkg_id2): + """Test if pkg_id1 and pkg_id2 are equivalent + + :param pkg_id1 The id of the first package + :param pkg_id2 The id of the second package + :return: True if pkg_id1 and pkg_id2 have the same "signature" in + the package dependency graph (i.e. relations can not tell + them apart semantically except for their name). Otherwise False. + + Note that this can return True even if pkg_id1 and pkg_id2 can + tell each other apart. + """ + return pkg_id2 in self.packages_equivalent_to(pkg_id1) + + def packages_equivalent_to(self, pkg_id): + """Determine which packages are equivalent to a given package + + :param pkg_id: The BinaryPackageId of a binary package. + :return: A frozenset of all package ids that are equivalent to the + input package. Note that this set always includes the input + package assuming it is a known package. + """ + return NotImplemented + + def relations_of(self, pkg_id): + """Get the direct relations of a given packge + + :param pkg_id: The BinaryPackageId of a binary package. + :return: A BinaryPackageRelation describing all known direct + relations for the package. + """ + return self._relations[pkg_id] + + def __contains__(self, pkg_id): + return pkg_id in self._relations + + def __iter__(self): + yield from self._relations