1 |
--- |
2 |
pym/_emerge/resolver/package_tracker.py | 310 ++++++++++++++++++++++++++++++++ |
3 |
1 file changed, 310 insertions(+) |
4 |
create mode 100644 pym/_emerge/resolver/package_tracker.py |
5 |
|
6 |
diff --git a/pym/_emerge/resolver/package_tracker.py b/pym/_emerge/resolver/package_tracker.py |
7 |
new file mode 100644 |
8 |
index 0000000..4aee4ea |
9 |
--- /dev/null |
10 |
+++ b/pym/_emerge/resolver/package_tracker.py |
11 |
@@ -0,0 +1,310 @@ |
12 |
+# Copyright 2014 Gentoo Foundation |
13 |
+# Distributed under the terms of the GNU General Public License v2 |
14 |
+ |
15 |
+from __future__ import print_function |
16 |
+ |
17 |
+import collections |
18 |
+ |
19 |
+import portage |
20 |
+portage.proxy.lazyimport.lazyimport(globals(), |
21 |
+ 'portage:OrderedDict', |
22 |
+ 'portage.dep:Atom,match_from_list', |
23 |
+ 'portage.util:cmp_sort_key', |
24 |
+ 'portage.versions:vercmp', |
25 |
+) |
26 |
+ |
27 |
+_PackageConflict = collections.namedtuple("_PackageConflict", ["root", "pkgs", "atom", "description"]) |
28 |
+ |
29 |
+class PackageConflict(_PackageConflict): |
30 |
+ """ |
31 |
+ Class to track the reason for a conflict and the conflicting packages. |
32 |
+ """ |
33 |
+ def __iter__(self): |
34 |
+ return iter(self.pkgs) |
35 |
+ |
36 |
+ def __contains__(self, pkg): |
37 |
+ return pkg in self.pkgs |
38 |
+ |
39 |
+ def __len__(self): |
40 |
+ return len(self.pkgs) |
41 |
+ |
42 |
+ |
43 |
+class PackageTracker(object): |
44 |
+ """ |
45 |
+ This class tracks packages which are currently |
46 |
+ installed and packages which have been pulled into |
47 |
+ the dependency graph. |
48 |
+ |
49 |
+ It automatically tracks conflicts between packages. |
50 |
+ |
51 |
+ Possible conflicts: |
52 |
+ 1) Packages that share the same SLOT. |
53 |
+ 2) Packages with the same cpv. |
54 |
+ Not yet implemented: |
55 |
+ 3) Packages that block each other. |
56 |
+ """ |
57 |
+ |
58 |
+ def __init__(self): |
59 |
+ # Mapping from package keys to set of packages. |
60 |
+ self._cp_pkg_map = collections.defaultdict(list) |
61 |
+ self._cp_vdb_pkg_map = collections.defaultdict(list) |
62 |
+ # List of package keys that may contain conflicts. |
63 |
+ # The insetation order must be preserved. |
64 |
+ self._multi_pkgs = [] |
65 |
+ |
66 |
+ # Cache for result of conflicts(). |
67 |
+ self._conflicts_cache = None |
68 |
+ |
69 |
+ # Records for each pulled package which installed package |
70 |
+ # are replaced. |
71 |
+ self._replacing = collections.defaultdict(list) |
72 |
+ # Records which pulled packages replace this package. |
73 |
+ self._replaced_by = collections.defaultdict(list) |
74 |
+ |
75 |
+ self._match_cache = collections.defaultdict(dict) |
76 |
+ |
77 |
+ def add_pkg(self, pkg): |
78 |
+ """ |
79 |
+ Add a new package to the tracker. Records conflicts as necessary. |
80 |
+ """ |
81 |
+ cp_key = pkg.root, pkg.cp |
82 |
+ |
83 |
+ try: |
84 |
+ if any(other is pkg for other in self._cp_pkg_map[cp_key]): |
85 |
+ return |
86 |
+ except KeyError: |
87 |
+ self._cp_pkg_map[cp_key] = [pkg] |
88 |
+ else: |
89 |
+ self._cp_pkg_map[cp_key].append(pkg) |
90 |
+ if len(self._cp_pkg_map[cp_key]) == 2: |
91 |
+ self._multi_pkgs.append(cp_key) |
92 |
+ self._conflicts_cache = None |
93 |
+ |
94 |
+ self._replacing[pkg] = [] |
95 |
+ for installed in self._cp_vdb_pkg_map[cp_key]: |
96 |
+ if installed.slot_atom == pkg.slot_atom or \ |
97 |
+ installed.cpv == pkg.cpv: |
98 |
+ self._replacing[pkg].append(installed) |
99 |
+ self._replaced_by[installed].append(pkg) |
100 |
+ |
101 |
+ self._match_cache.pop((pkg.root, pkg.cp)) |
102 |
+ |
103 |
+ def add_installed_pkg(self, installed): |
104 |
+ """ |
105 |
+ Add an installed package during vdb load. These packages |
106 |
+ are not returned by matched_pull as long as add_pkg hasn't |
107 |
+ been called with them. They are only returned by match_final. |
108 |
+ """ |
109 |
+ cp_key = installed.root, installed.cp |
110 |
+ try: |
111 |
+ if any(other is installed for other in self._cp_vdb_pkg_map[cp_key]): |
112 |
+ return |
113 |
+ except KeyError: |
114 |
+ self._cp_vdb_pkg_map[cp_key] = [installed] |
115 |
+ else: |
116 |
+ self._cp_vdb_pkg_map[cp_key].append(installed) |
117 |
+ |
118 |
+ for pkg in self._cp_pkg_map[cp_key]: |
119 |
+ if installed.slot_atom == pkg.slot_atom or \ |
120 |
+ installed.cpv == pkg.cpv: |
121 |
+ self._replacing[pkg].append(installed) |
122 |
+ self._replaced_by[installed].append(pkg) |
123 |
+ |
124 |
+ def remove_pkg(self, pkg): |
125 |
+ """ |
126 |
+ Removes the package from the tracker. |
127 |
+ Raises KeyError if it isn't present. |
128 |
+ """ |
129 |
+ cp_key = pkg.root, pkg.cp |
130 |
+ try: |
131 |
+ self._cp_pkg_map[cp_key].remove(pkg) |
132 |
+ except ValueError: |
133 |
+ raise KeyError |
134 |
+ |
135 |
+ if self._cp_pkg_map[cp_key]: |
136 |
+ self._conflicts_cache = None |
137 |
+ |
138 |
+ if not self._cp_pkg_map[cp_key]: |
139 |
+ del self._cp_pkg_map[cp_key] |
140 |
+ elif len(self._cp_pkg_map[cp_key]) == 1: |
141 |
+ self._multi_pkgs = [other_cp_key for other_cp_key in self._multi_pkgs \ |
142 |
+ if other_cp_key != cp_key] |
143 |
+ |
144 |
+ for installed in self._replacing[pkg]: |
145 |
+ self._replaced_by[installed].remove(pkg) |
146 |
+ if not self._replaced_by[installed]: |
147 |
+ del self._replaced_by[installed] |
148 |
+ del self._replacing[pkg] |
149 |
+ |
150 |
+ self._match_cache.pop((pkg.root, pkg.cp)) |
151 |
+ |
152 |
+ def discard_pkg(self, pkg): |
153 |
+ """ |
154 |
+ Removes the package from the tracker. |
155 |
+ Does not raises KeyError if it is not present. |
156 |
+ """ |
157 |
+ try: |
158 |
+ self.remove_pkg(pkg) |
159 |
+ except KeyError: |
160 |
+ pass |
161 |
+ |
162 |
+ def match(self, root, atom, installed=True): |
163 |
+ """ |
164 |
+ Iterates over the packages matching 'atom'. |
165 |
+ If 'installed' is True, installed non-replaced |
166 |
+ packages may also be returned. |
167 |
+ """ |
168 |
+ cache_key = (root, atom, installed) |
169 |
+ try: |
170 |
+ return iter(self._match_cache[(root, atom.cp)][cache_key]) |
171 |
+ except KeyError: |
172 |
+ pass |
173 |
+ |
174 |
+ cp_key = root, atom.cp |
175 |
+ try: |
176 |
+ candidates = self._cp_pkg_map[cp_key][:] |
177 |
+ except KeyError: |
178 |
+ candidates = [] |
179 |
+ |
180 |
+ if installed: |
181 |
+ try: |
182 |
+ for installed in self._cp_vdb_pkg_map[cp_key]: |
183 |
+ if installed not in self._replaced_by: |
184 |
+ candidates.append(installed) |
185 |
+ except KeyError: |
186 |
+ pass |
187 |
+ |
188 |
+ ret = match_from_list(atom, candidates) |
189 |
+ ret.sort(key=cmp_sort_key(lambda x, y: vercmp(x.version, y.version))) |
190 |
+ self._match_cache[(root, atom.cp)][cache_key] = ret |
191 |
+ |
192 |
+ return iter(ret) |
193 |
+ |
194 |
+ def conflicts(self): |
195 |
+ """ |
196 |
+ Iterates over the curently existing conflicts. |
197 |
+ """ |
198 |
+ if self._conflicts_cache is None: |
199 |
+ self._conflicts_cache = [] |
200 |
+ |
201 |
+ for cp_key in self._multi_pkgs: |
202 |
+ |
203 |
+ # Categorize packages according to cpv and slot. |
204 |
+ slot_map = collections.defaultdict(set) |
205 |
+ cpv_map = collections.defaultdict(set) |
206 |
+ for pkg in self._cp_pkg_map[cp_key]: |
207 |
+ slot_key = pkg.root, pkg.slot_atom |
208 |
+ cpv_key = pkg.root, pkg.cpv |
209 |
+ slot_map[slot_key].add(pkg) |
210 |
+ cpv_map[cpv_key].add(pkg) |
211 |
+ |
212 |
+ # Slot conflicts. |
213 |
+ for slot_key in slot_map: |
214 |
+ slot_pkgs = slot_map[slot_key] |
215 |
+ if len(slot_pkgs) > 1: |
216 |
+ self._conflicts_cache.append(PackageConflict( |
217 |
+ description = "slot conflict", |
218 |
+ root = slot_key[0], |
219 |
+ atom = slot_key[1], |
220 |
+ pkgs = tuple(slot_pkgs), |
221 |
+ )) |
222 |
+ |
223 |
+ # CPV conflicts. |
224 |
+ for cpv_key in cpv_map: |
225 |
+ cpv_pkgs = cpv_map[cpv_key] |
226 |
+ if len(cpv_pkgs) > 1: |
227 |
+ # Make sure this cpv conflict is not a slot conflict at the same time. |
228 |
+ # Ignore it if it is. |
229 |
+ slots = set(pkg.slot for pkg in cpv_pkgs) |
230 |
+ if len(slots) > 1: |
231 |
+ self._conflicts_cache.append(PackageConflict( |
232 |
+ description = "cpv conflict", |
233 |
+ root = cpv_pkgs[0], |
234 |
+ atom = cpv_pkgs[1], |
235 |
+ pkgs = tuple(cpv_pkgs), |
236 |
+ )) |
237 |
+ |
238 |
+ return iter(self._conflicts_cache) |
239 |
+ |
240 |
+ def slot_conflicts(self): |
241 |
+ """ |
242 |
+ Iterates over present slot conflicts. |
243 |
+ This is only intended for consumers that haven't been |
244 |
+ updated to deal with other kinds of conflicts. |
245 |
+ This funcion should be removed once all consumers are updated. |
246 |
+ """ |
247 |
+ return (conflict for conflict in self.conflicts() \ |
248 |
+ if conflict.description == "slot conflict") |
249 |
+ |
250 |
+ def all_pkgs(self, root): |
251 |
+ """ |
252 |
+ Iterates over all packages for the given root |
253 |
+ present in the tracker, including the installed |
254 |
+ packages. |
255 |
+ """ |
256 |
+ for cp_key in self._cp_pkg_map: |
257 |
+ if cp_key[0] == root: |
258 |
+ for pkg in self._cp_pkg_map[cp_key]: |
259 |
+ yield pkg |
260 |
+ |
261 |
+ for cp_key in self._cp_vdb_pkg_map: |
262 |
+ if cp_key[0] == root: |
263 |
+ for installed in self._cp_vdb_pkg_map[cp_key]: |
264 |
+ if installed not in self._replaced_by: |
265 |
+ yield installed |
266 |
+ |
267 |
+ def contains(self, pkg, installed=True): |
268 |
+ """ |
269 |
+ Checks if the package is in the tracker. |
270 |
+ If 'installed' is True, returns True for |
271 |
+ non-replaced installed packages. |
272 |
+ """ |
273 |
+ cp_key = pkg.root, pkg.cp |
274 |
+ for other in self._cp_pkg_map[cp_key]: |
275 |
+ if other is pkg: |
276 |
+ return True |
277 |
+ |
278 |
+ if installed: |
279 |
+ for installed in self._cp_vdb_pkg_map[cp_key]: |
280 |
+ if installed is pkg and \ |
281 |
+ installed not in self._replaced_by: |
282 |
+ return True |
283 |
+ |
284 |
+ return False |
285 |
+ |
286 |
+ def __contains__(self, pkg): |
287 |
+ """ |
288 |
+ Checks if the package is in the tracker. |
289 |
+ Returns True for non-replaced installed packages. |
290 |
+ """ |
291 |
+ return self.contains(pkg, installed=True) |
292 |
+ |
293 |
+ |
294 |
+class PackageTrackerDbapiWrapper(object): |
295 |
+ """ |
296 |
+ A wrpper class that provides parts of the legacy |
297 |
+ dbapi interface. Remove it once all consumers have |
298 |
+ died. |
299 |
+ """ |
300 |
+ def __init__(self, root, package_tracker): |
301 |
+ self._root = root |
302 |
+ self._package_tracker = package_tracker |
303 |
+ |
304 |
+ def cpv_inject(self, pkg): |
305 |
+ self._package_tracker.add_pkg(pkg) |
306 |
+ |
307 |
+ def match_pkgs(self, atom): |
308 |
+ if not isinstance(atom, Atom): |
309 |
+ atom = Atom(atom) |
310 |
+ ret = sorted(self._package_tracker.match(self._root, atom), |
311 |
+ key=cmp_sort_key(lambda x, y: vercmp(x.version, y.version))) |
312 |
+ return ret |
313 |
+ |
314 |
+ def __iter__(self): |
315 |
+ return self._package_tracker.all_pkgs(self._root) |
316 |
+ |
317 |
+ def match(self, atom, use_cache=None): |
318 |
+ return self.match_pkgs(atom) |
319 |
+ |
320 |
+ def cp_list(self, cp): |
321 |
+ return self.match_pkgs(cp) |
322 |
-- |
323 |
1.8.3.2 |