Gentoo Archives: gentoo-portage-dev

From: Sebastian Luther <SebastianLuther@×××.de>
To: gentoo-portage-dev@l.g.o
Subject: [gentoo-portage-dev] [PATCH 01/10] Add resolver/package_tracker
Date: Wed, 29 Jan 2014 15:33:39
Message-Id: 1391009594-22750-2-git-send-email-SebastianLuther@gmx.de
In Reply to: [gentoo-portage-dev] [PATCH 00/10] First steps to get rid of backtracking by Sebastian Luther
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