1 |
When autounmask is enabled, make depgraph retry _select_atoms calls when |
2 |
necessary to prefer || choices with existing packages. For example, |
3 |
if package C is masked and package B does not exist, then autounmask |
4 |
should choose C when given the choice || ( B C ), as shown in the |
5 |
included unit tests. The unit tests also show that autounmask still |
6 |
prefers choices containing packages that are not masked (if available). |
7 |
|
8 |
Bug: https://bugs.gentoo.org/327177 |
9 |
Signed-off-by: Zac Medico <zmedico@g.o> |
10 |
--- |
11 |
lib/_emerge/depgraph.py | 152 ++++++++++++------ |
12 |
.../resolver/test_autounmask_or_choices.py | 71 ++++++++ |
13 |
2 files changed, 176 insertions(+), 47 deletions(-) |
14 |
create mode 100644 lib/portage/tests/resolver/test_autounmask_or_choices.py |
15 |
|
16 |
diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py |
17 |
index 32b2d7da3..7a54aeb34 100644 |
18 |
--- a/lib/_emerge/depgraph.py |
19 |
+++ b/lib/_emerge/depgraph.py |
20 |
@@ -365,6 +365,14 @@ class _use_changes(tuple): |
21 |
return obj |
22 |
|
23 |
|
24 |
+_select_atoms_deps = collections.namedtuple('_select_atoms_deps', ( |
25 |
+ 'normal_deps', |
26 |
+ 'virt_deps', |
27 |
+ 'ignored_deps', |
28 |
+ 'unsatisfied_deps', |
29 |
+)) |
30 |
+ |
31 |
+ |
32 |
class _dynamic_depgraph_config(object): |
33 |
|
34 |
""" |
35 |
@@ -3479,8 +3487,13 @@ class depgraph(object): |
36 |
dep.child.root, dep.child.slot_atom, installed=False)) and \ |
37 |
not slot_operator_rebuild |
38 |
|
39 |
- def _wrapped_add_pkg_dep_string(self, pkg, dep_root, dep_priority, |
40 |
- dep_string, allow_unsatisfied): |
41 |
+ def _select_atoms_deps(self, pkg, dep_root, dep_priority, selected_atoms): |
42 |
+ """ |
43 |
+ Create Dependency instances from a _select_atoms result, and create a |
44 |
+ list of unsatisfied dependencies which is useful for deciding when |
45 |
+ to retry a _select_atoms call with autounmask enabled. |
46 |
+ """ |
47 |
+ |
48 |
if isinstance(pkg.depth, int): |
49 |
depth = pkg.depth + 1 |
50 |
else: |
51 |
@@ -3489,38 +3502,13 @@ class depgraph(object): |
52 |
deep = self._dynamic_config.myparams.get("deep", 0) |
53 |
recurse_satisfied = deep is True or depth <= deep |
54 |
debug = "--debug" in self._frozen_config.myopts |
55 |
- strict = pkg.type_name != "installed" |
56 |
- |
57 |
- if debug: |
58 |
- writemsg_level("\nParent: %s\n" % (pkg,), |
59 |
- noiselevel=-1, level=logging.DEBUG) |
60 |
- dep_repr = portage.dep.paren_enclose(dep_string, |
61 |
- unevaluated_atom=True, opconvert=True) |
62 |
- writemsg_level("Depstring: %s\n" % (dep_repr,), |
63 |
- noiselevel=-1, level=logging.DEBUG) |
64 |
- writemsg_level("Priority: %s\n" % (dep_priority,), |
65 |
- noiselevel=-1, level=logging.DEBUG) |
66 |
- |
67 |
- try: |
68 |
- selected_atoms = self._select_atoms(dep_root, |
69 |
- dep_string, myuse=self._pkg_use_enabled(pkg), parent=pkg, |
70 |
- strict=strict, priority=dep_priority) |
71 |
- except portage.exception.InvalidDependString: |
72 |
- if pkg.installed: |
73 |
- self._dynamic_config._masked_installed.add(pkg) |
74 |
- return 1 |
75 |
- |
76 |
- # should have been masked before it was selected |
77 |
- raise |
78 |
- |
79 |
- if debug: |
80 |
- writemsg_level("Candidates: %s\n" % \ |
81 |
- ([str(x) for x in selected_atoms[pkg]],), |
82 |
- noiselevel=-1, level=logging.DEBUG) |
83 |
- |
84 |
root_config = self._frozen_config.roots[dep_root] |
85 |
vardb = root_config.trees["vartree"].dbapi |
86 |
traversed_virt_pkgs = set() |
87 |
+ normal_deps = [] |
88 |
+ virt_deps = [] |
89 |
+ ignored_deps = [] |
90 |
+ unsatisfied_deps = [] |
91 |
|
92 |
reinstall_atoms = self._frozen_config.reinstall_atoms |
93 |
for atom, child in self._minimize_children( |
94 |
@@ -3586,7 +3574,7 @@ class depgraph(object): |
95 |
# mode may select a different child later. |
96 |
ignored = True |
97 |
dep.child = None |
98 |
- self._dynamic_config._ignored_deps.append(dep) |
99 |
+ ignored_deps.append(dep) |
100 |
|
101 |
if not ignored: |
102 |
if dep_priority.ignored and \ |
103 |
@@ -3594,11 +3582,11 @@ class depgraph(object): |
104 |
if is_virt and dep.child is not None: |
105 |
traversed_virt_pkgs.add(dep.child) |
106 |
dep.child = None |
107 |
- self._dynamic_config._ignored_deps.append(dep) |
108 |
+ ignored_deps.append(dep) |
109 |
else: |
110 |
- if not self._add_dep(dep, |
111 |
- allow_unsatisfied=allow_unsatisfied): |
112 |
- return 0 |
113 |
+ if dep.child is None and not dep.blocker: |
114 |
+ unsatisfied_deps.append(dep) |
115 |
+ normal_deps.append(dep) |
116 |
if is_virt and dep.child is not None: |
117 |
traversed_virt_pkgs.add(dep.child) |
118 |
|
119 |
@@ -3637,8 +3625,7 @@ class depgraph(object): |
120 |
# none visible, so use highest |
121 |
virt_dep.priority.satisfied = inst_pkgs[0] |
122 |
|
123 |
- if not self._add_pkg(virt_pkg, virt_dep): |
124 |
- return 0 |
125 |
+ virt_deps.append(virt_dep) |
126 |
|
127 |
for atom, child in self._minimize_children( |
128 |
pkg, self._priority(runtime=True), root_config, atoms): |
129 |
@@ -3686,7 +3673,7 @@ class depgraph(object): |
130 |
if myarg is None: |
131 |
ignored = True |
132 |
dep.child = None |
133 |
- self._dynamic_config._ignored_deps.append(dep) |
134 |
+ ignored_deps.append(dep) |
135 |
|
136 |
if not ignored: |
137 |
if dep_priority.ignored and \ |
138 |
@@ -3694,14 +3681,82 @@ class depgraph(object): |
139 |
if is_virt and dep.child is not None: |
140 |
traversed_virt_pkgs.add(dep.child) |
141 |
dep.child = None |
142 |
- self._dynamic_config._ignored_deps.append(dep) |
143 |
+ ignored_deps.append(dep) |
144 |
else: |
145 |
- if not self._add_dep(dep, |
146 |
- allow_unsatisfied=allow_unsatisfied): |
147 |
- return 0 |
148 |
+ if dep.child is None and not dep.blocker: |
149 |
+ unsatisfied_deps.append(dep) |
150 |
+ normal_deps.append(dep) |
151 |
if is_virt and dep.child is not None: |
152 |
traversed_virt_pkgs.add(dep.child) |
153 |
|
154 |
+ return _select_atoms_deps(normal_deps, virt_deps, ignored_deps, unsatisfied_deps) |
155 |
+ |
156 |
+ def _wrapped_add_pkg_dep_string(self, pkg, dep_root, dep_priority, |
157 |
+ dep_string, allow_unsatisfied): |
158 |
+ |
159 |
+ debug = "--debug" in self._frozen_config.myopts |
160 |
+ strict = pkg.type_name != "installed" |
161 |
+ |
162 |
+ if debug: |
163 |
+ writemsg_level("\nParent: %s\n" % (pkg,), |
164 |
+ noiselevel=-1, level=logging.DEBUG) |
165 |
+ dep_repr = portage.dep.paren_enclose(dep_string, |
166 |
+ unevaluated_atom=True, opconvert=True) |
167 |
+ writemsg_level("Depstring: %s\n" % (dep_repr,), |
168 |
+ noiselevel=-1, level=logging.DEBUG) |
169 |
+ writemsg_level("Priority: %s\n" % (dep_priority,), |
170 |
+ noiselevel=-1, level=logging.DEBUG) |
171 |
+ |
172 |
+ autounmask_states = [False] |
173 |
+ if self._dynamic_config._autounmask: |
174 |
+ autounmask_states.append(True) |
175 |
+ |
176 |
+ choices = [] |
177 |
+ for autounmask in autounmask_states: |
178 |
+ if autounmask: |
179 |
+ # Clear the package selection cache so that autounmask |
180 |
+ # can make new selections. |
181 |
+ self._dynamic_config._filtered_trees[dep_root]["porttree"].dbapi._clear_cache() |
182 |
+ try: |
183 |
+ selected_atoms = self._select_atoms(dep_root, |
184 |
+ dep_string, myuse=self._pkg_use_enabled(pkg), parent=pkg, |
185 |
+ strict=strict, priority=dep_priority, autounmask=autounmask) |
186 |
+ except portage.exception.InvalidDependString: |
187 |
+ if pkg.installed: |
188 |
+ self._dynamic_config._masked_installed.add(pkg) |
189 |
+ return 1 |
190 |
+ |
191 |
+ # should have been masked before it was selected |
192 |
+ raise |
193 |
+ |
194 |
+ if debug: |
195 |
+ writemsg_level("Candidates (autounmask=%s): %s\n" % \ |
196 |
+ (autounmask, [str(x) for x in selected_atoms[pkg]],), |
197 |
+ noiselevel=-1, level=logging.DEBUG) |
198 |
+ |
199 |
+ choice = self._select_atoms_deps(pkg, dep_root, dep_priority, selected_atoms) |
200 |
+ choices.append(choice) |
201 |
+ if not choice.unsatisfied_deps: |
202 |
+ break |
203 |
+ else: |
204 |
+ # If all choices have unsatisfied deps, fall back to default |
205 |
+ # autounmask=False behavior. |
206 |
+ choice = choices[0] |
207 |
+ |
208 |
+ if autounmask: |
209 |
+ # An autounmask choice has been rejected, so clear its |
210 |
+ # package selections from the cache. |
211 |
+ self._dynamic_config._filtered_trees[dep_root]["porttree"].dbapi._clear_cache() |
212 |
+ |
213 |
+ for dep in choice.normal_deps: |
214 |
+ if not self._add_dep(dep, |
215 |
+ allow_unsatisfied=allow_unsatisfied): |
216 |
+ return 0 |
217 |
+ for virt_dep in choice.virt_deps: |
218 |
+ if not self._add_pkg(virt_dep.child, virt_dep): |
219 |
+ return 0 |
220 |
+ self._dynamic_config._ignored_deps.extend(choice.ignored_deps) |
221 |
+ |
222 |
if debug: |
223 |
writemsg_level("\nExiting... %s\n" % (pkg,), |
224 |
noiselevel=-1, level=logging.DEBUG) |
225 |
@@ -4699,7 +4754,8 @@ class depgraph(object): |
226 |
return self._select_atoms_highest_available(*pargs, **kwargs) |
227 |
|
228 |
def _select_atoms_highest_available(self, root, depstring, |
229 |
- myuse=None, parent=None, strict=True, trees=None, priority=None): |
230 |
+ myuse=None, parent=None, strict=True, trees=None, priority=None, |
231 |
+ autounmask=False): |
232 |
"""This will raise InvalidDependString if necessary. If trees is |
233 |
None then self._dynamic_config._filtered_trees is used.""" |
234 |
|
235 |
@@ -4727,8 +4783,9 @@ class depgraph(object): |
236 |
if True: |
237 |
# Temporarily disable autounmask so that || preferences |
238 |
# account for masking and USE settings. |
239 |
- _autounmask_backup = self._dynamic_config._autounmask |
240 |
- self._dynamic_config._autounmask = False |
241 |
+ if not autounmask: |
242 |
+ _autounmask_backup = self._dynamic_config._autounmask |
243 |
+ self._dynamic_config._autounmask = False |
244 |
# backup state for restoration, in case of recursive |
245 |
# calls to this method |
246 |
backup_parent = self._select_atoms_parent |
247 |
@@ -4756,7 +4813,8 @@ class depgraph(object): |
248 |
myroot=root, trees=trees) |
249 |
finally: |
250 |
# restore state |
251 |
- self._dynamic_config._autounmask = _autounmask_backup |
252 |
+ if not autounmask: |
253 |
+ self._dynamic_config._autounmask = _autounmask_backup |
254 |
self._select_atoms_parent = backup_parent |
255 |
mytrees.pop("pkg_use_enabled", None) |
256 |
mytrees.pop("parent", None) |
257 |
diff --git a/lib/portage/tests/resolver/test_autounmask_or_choices.py b/lib/portage/tests/resolver/test_autounmask_or_choices.py |
258 |
new file mode 100644 |
259 |
index 000000000..b5f2044e3 |
260 |
--- /dev/null |
261 |
+++ b/lib/portage/tests/resolver/test_autounmask_or_choices.py |
262 |
@@ -0,0 +1,71 @@ |
263 |
+# Copyright 2019 Gentoo Authors |
264 |
+# Distributed under the terms of the GNU General Public License v2 |
265 |
+ |
266 |
+from portage.tests import TestCase |
267 |
+from portage.tests.resolver.ResolverPlayground import ( |
268 |
+ ResolverPlayground, |
269 |
+ ResolverPlaygroundTestCase, |
270 |
+) |
271 |
+ |
272 |
+class AutounmaskOrChoicesTestCase(TestCase): |
273 |
+ |
274 |
+ def testAutounmaskOrChoices(self): |
275 |
+ ebuilds = { |
276 |
+ 'dev-libs/A-1': { |
277 |
+ 'EAPI': '7', |
278 |
+ 'RDEPEND': '|| ( dev-libs/B dev-libs/C )', |
279 |
+ }, |
280 |
+ 'dev-libs/C-1': { |
281 |
+ 'EAPI': '7', |
282 |
+ 'KEYWORDS': '~x86', |
283 |
+ }, |
284 |
+ 'dev-libs/D-1': { |
285 |
+ 'EAPI': '7', |
286 |
+ 'RDEPEND': '|| ( dev-libs/E dev-libs/F )', |
287 |
+ }, |
288 |
+ 'dev-libs/E-1': { |
289 |
+ 'EAPI': '7', |
290 |
+ 'KEYWORDS': '~x86', |
291 |
+ }, |
292 |
+ 'dev-libs/F-1': { |
293 |
+ 'EAPI': '7', |
294 |
+ 'KEYWORDS': 'x86', |
295 |
+ }, |
296 |
+ } |
297 |
+ |
298 |
+ test_cases = ( |
299 |
+ # Test bug 327177, where we want to prefer choices with masked |
300 |
+ # packages over those with nonexisting packages. |
301 |
+ ResolverPlaygroundTestCase( |
302 |
+ ['dev-libs/A'], |
303 |
+ options={"--autounmask": True}, |
304 |
+ success=False, |
305 |
+ mergelist=[ |
306 |
+ 'dev-libs/C-1', |
307 |
+ 'dev-libs/A-1', |
308 |
+ ], |
309 |
+ unstable_keywords = ('dev-libs/C-1',), |
310 |
+ ), |
311 |
+ # Test that autounmask prefers choices with packages that |
312 |
+ # are not masked. |
313 |
+ ResolverPlaygroundTestCase( |
314 |
+ ['dev-libs/D'], |
315 |
+ options={"--autounmask": True}, |
316 |
+ success=True, |
317 |
+ mergelist=[ |
318 |
+ 'dev-libs/F-1', |
319 |
+ 'dev-libs/D-1', |
320 |
+ ], |
321 |
+ ), |
322 |
+ ) |
323 |
+ |
324 |
+ playground = ResolverPlayground(ebuilds=ebuilds, debug=False) |
325 |
+ |
326 |
+ try: |
327 |
+ for test_case in test_cases: |
328 |
+ playground.run_TestCase(test_case) |
329 |
+ self.assertEqual(test_case.test_success, True, |
330 |
+ test_case.fail_msg) |
331 |
+ finally: |
332 |
+ playground.debug = False |
333 |
+ playground.cleanup() |
334 |
-- |
335 |
2.21.0 |