1 |
commit: 079f8c4a36ccc2ef5e25e7a57cd0707640f82592 |
2 |
Author: Zac Medico <zmedico <AT> gentoo <DOT> org> |
3 |
AuthorDate: Fri Feb 14 19:21:28 2020 +0000 |
4 |
Commit: Zac Medico <zmedico <AT> gentoo <DOT> org> |
5 |
CommitDate: Fri Feb 14 23:10:37 2020 +0000 |
6 |
URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=079f8c4a |
7 |
|
8 |
depclean: ensure consistency with update actions (bug 649622) |
9 |
|
10 |
Make depclean traverse dependencies in the same order as update |
11 |
actions, in order to ensure consistency in decisions which are |
12 |
dependent on the order of dependency evaluation due to inconsistent |
13 |
use of || preferences in different packages. |
14 |
|
15 |
In unit tests, update test_virtual_w3m_realistic to assert that |
16 |
the order of graph traversal is deterministic and consistent |
17 |
between update and removal (depclean) actions. |
18 |
|
19 |
Bug: https://bugs.gentoo.org/649622 |
20 |
Signed-off-by: Zac Medico <zmedico <AT> gentoo.org> |
21 |
|
22 |
lib/_emerge/actions.py | 33 +++++++--- |
23 |
lib/_emerge/depgraph.py | 21 +++++-- |
24 |
lib/portage/tests/resolver/ResolverPlayground.py | 77 +++++++++++++++--------- |
25 |
lib/portage/tests/resolver/test_or_choices.py | 12 +++- |
26 |
4 files changed, 96 insertions(+), 47 deletions(-) |
27 |
|
28 |
diff --git a/lib/_emerge/actions.py b/lib/_emerge/actions.py |
29 |
index 31252af16..4bf9ce425 100644 |
30 |
--- a/lib/_emerge/actions.py |
31 |
+++ b/lib/_emerge/actions.py |
32 |
@@ -1,8 +1,9 @@ |
33 |
-# Copyright 1999-2019 Gentoo Authors |
34 |
+# Copyright 1999-2020 Gentoo Authors |
35 |
# Distributed under the terms of the GNU General Public License v2 |
36 |
|
37 |
from __future__ import division, print_function, unicode_literals |
38 |
|
39 |
+import collections |
40 |
import errno |
41 |
import logging |
42 |
import operator |
43 |
@@ -741,7 +742,19 @@ def action_depclean(settings, trees, ldpath_mtimes, |
44 |
|
45 |
return rval |
46 |
|
47 |
+ |
48 |
def calc_depclean(settings, trees, ldpath_mtimes, |
49 |
+ myopts, action, args_set, spinner): |
50 |
+ result = _calc_depclean(settings, trees, ldpath_mtimes, |
51 |
+ myopts, action, args_set, spinner) |
52 |
+ return result.returncode, result.cleanlist, result.ordered, result.req_pkg_count |
53 |
+ |
54 |
+ |
55 |
+_depclean_result = collections.namedtuple('_depclean_result', |
56 |
+ ('returncode', 'cleanlist', 'ordered', 'req_pkg_count', 'depgraph')) |
57 |
+ |
58 |
+ |
59 |
+def _calc_depclean(settings, trees, ldpath_mtimes, |
60 |
myopts, action, args_set, spinner): |
61 |
allow_missing_deps = bool(args_set) |
62 |
|
63 |
@@ -805,7 +818,7 @@ def calc_depclean(settings, trees, ldpath_mtimes, |
64 |
writemsg_level(_("!!! Aborting due to set configuration " |
65 |
"errors displayed above.\n"), |
66 |
level=logging.ERROR, noiselevel=-1) |
67 |
- return 1, [], False, 0 |
68 |
+ return _depclean_result(1, [], False, 0, None) |
69 |
|
70 |
if action == "depclean": |
71 |
emergelog(xterm_titles, " >>> depclean") |
72 |
@@ -920,7 +933,7 @@ def calc_depclean(settings, trees, ldpath_mtimes, |
73 |
resolver.display_problems() |
74 |
|
75 |
if not success: |
76 |
- return 1, [], False, 0 |
77 |
+ return _depclean_result(1, [], False, 0, resolver) |
78 |
|
79 |
def unresolved_deps(): |
80 |
|
81 |
@@ -1020,7 +1033,7 @@ def calc_depclean(settings, trees, ldpath_mtimes, |
82 |
return False |
83 |
|
84 |
if unresolved_deps(): |
85 |
- return 1, [], False, 0 |
86 |
+ return _depclean_result(1, [], False, 0, resolver) |
87 |
|
88 |
graph = resolver._dynamic_config.digraph.copy() |
89 |
required_pkgs_total = 0 |
90 |
@@ -1321,7 +1334,7 @@ def calc_depclean(settings, trees, ldpath_mtimes, |
91 |
runtime_slot_op=True), |
92 |
root=pkg.root)): |
93 |
resolver.display_problems() |
94 |
- return 1, [], False, 0 |
95 |
+ return _depclean_result(1, [], False, 0, resolver) |
96 |
|
97 |
writemsg_level("\nCalculating dependencies ") |
98 |
success = resolver._complete_graph( |
99 |
@@ -1329,9 +1342,9 @@ def calc_depclean(settings, trees, ldpath_mtimes, |
100 |
writemsg_level("\b\b... done!\n") |
101 |
resolver.display_problems() |
102 |
if not success: |
103 |
- return 1, [], False, 0 |
104 |
+ return _depclean_result(1, [], False, 0, resolver) |
105 |
if unresolved_deps(): |
106 |
- return 1, [], False, 0 |
107 |
+ return _depclean_result(1, [], False, 0, resolver) |
108 |
|
109 |
graph = resolver._dynamic_config.digraph.copy() |
110 |
required_pkgs_total = 0 |
111 |
@@ -1340,7 +1353,7 @@ def calc_depclean(settings, trees, ldpath_mtimes, |
112 |
required_pkgs_total += 1 |
113 |
cleanlist = create_cleanlist() |
114 |
if not cleanlist: |
115 |
- return 0, [], False, required_pkgs_total |
116 |
+ return _depclean_result(0, [], False, required_pkgs_total, resolver) |
117 |
clean_set = set(cleanlist) |
118 |
|
119 |
if clean_set: |
120 |
@@ -1458,8 +1471,8 @@ def calc_depclean(settings, trees, ldpath_mtimes, |
121 |
graph.remove(node) |
122 |
cleanlist.append(node.cpv) |
123 |
|
124 |
- return 0, cleanlist, ordered, required_pkgs_total |
125 |
- return 0, [], False, required_pkgs_total |
126 |
+ return _depclean_result(0, cleanlist, ordered, required_pkgs_total, resolver) |
127 |
+ return _depclean_result(0, [], False, required_pkgs_total, resolver) |
128 |
|
129 |
def action_deselect(settings, trees, opts, atoms): |
130 |
enter_invalid = '--ask-enter-invalid' in opts |
131 |
|
132 |
diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py |
133 |
index 8e0d79e29..27696ad40 100644 |
134 |
--- a/lib/_emerge/depgraph.py |
135 |
+++ b/lib/_emerge/depgraph.py |
136 |
@@ -6955,9 +6955,18 @@ class depgraph(object): |
137 |
# Removal actions may override sets with temporary |
138 |
# replacements that have had atoms removed in order |
139 |
# to implement --deselect behavior. |
140 |
- required_set_names = set(required_sets[root]) |
141 |
depgraph_sets.sets.clear() |
142 |
depgraph_sets.sets.update(required_sets[root]) |
143 |
+ if 'world' in depgraph_sets.sets: |
144 |
+ # For consistent order of traversal for both update |
145 |
+ # and removal (depclean) actions, sets other that |
146 |
+ # world are always nested under the world set. |
147 |
+ world_atoms = list(depgraph_sets.sets['world']) |
148 |
+ world_atoms.extend(SETPREFIX + s for s in required_sets[root] if s != 'world') |
149 |
+ depgraph_sets.sets['world'] = InternalPackageSet(initial_atoms=world_atoms) |
150 |
+ required_set_names = {'world'} |
151 |
+ else: |
152 |
+ required_set_names = set(required_sets[root]) |
153 |
if "remove" not in self._dynamic_config.myparams and \ |
154 |
root == self._frozen_config.target_root and \ |
155 |
already_deep: |
156 |
@@ -6967,7 +6976,7 @@ class depgraph(object): |
157 |
not self._dynamic_config._dep_stack: |
158 |
continue |
159 |
root_config = self._frozen_config.roots[root] |
160 |
- for s in required_set_names: |
161 |
+ for s in sorted(required_set_names): |
162 |
pset = depgraph_sets.sets.get(s) |
163 |
if pset is None: |
164 |
pset = root_config.sets[s] |
165 |
@@ -6977,10 +6986,10 @@ class depgraph(object): |
166 |
|
167 |
self._set_args(args) |
168 |
for arg in self._expand_set_args(args, add_to_digraph=True): |
169 |
- for atom in sorted(arg.pset.getAtoms(), reverse=True): |
170 |
- self._dynamic_config._dep_stack.append( |
171 |
- Dependency(atom=atom, root=arg.root_config.root, |
172 |
- parent=arg, depth=self._UNREACHABLE_DEPTH)) |
173 |
+ for atom in sorted(arg.pset.getAtoms()): |
174 |
+ if not self._add_dep(Dependency(atom=atom, root=arg.root_config.root, |
175 |
+ parent=arg, depth=self._UNREACHABLE_DEPTH), allow_unsatisfied=True): |
176 |
+ return 0 |
177 |
|
178 |
if True: |
179 |
if self._dynamic_config._ignored_deps: |
180 |
|
181 |
diff --git a/lib/portage/tests/resolver/ResolverPlayground.py b/lib/portage/tests/resolver/ResolverPlayground.py |
182 |
index cc0aa46e9..0456ce2e2 100644 |
183 |
--- a/lib/portage/tests/resolver/ResolverPlayground.py |
184 |
+++ b/lib/portage/tests/resolver/ResolverPlayground.py |
185 |
@@ -1,4 +1,4 @@ |
186 |
-# Copyright 2010-2019 Gentoo Authors |
187 |
+# Copyright 2010-2020 Gentoo Authors |
188 |
# Distributed under the terms of the GNU General Public License v2 |
189 |
|
190 |
import bz2 |
191 |
@@ -22,9 +22,10 @@ from portage.util import ensure_dirs, normalize_path |
192 |
from portage.versions import catsplit |
193 |
|
194 |
import _emerge |
195 |
-from _emerge.actions import calc_depclean |
196 |
+from _emerge.actions import _calc_depclean |
197 |
from _emerge.Blocker import Blocker |
198 |
from _emerge.create_depgraph_params import create_depgraph_params |
199 |
+from _emerge.DependencyArg import DependencyArg |
200 |
from _emerge.depgraph import backtrack_depgraph |
201 |
from _emerge.RootConfig import RootConfig |
202 |
|
203 |
@@ -593,11 +594,16 @@ class ResolverPlayground(object): |
204 |
_emerge.emergelog._disable = True |
205 |
|
206 |
if action in ("depclean", "prune"): |
207 |
- rval, cleanlist, ordered, req_pkg_count = \ |
208 |
- calc_depclean(self.settings, self.trees, None, |
209 |
+ depclean_result = _calc_depclean(self.settings, self.trees, None, |
210 |
options, action, InternalPackageSet(initial_atoms=atoms, allow_wildcard=True), None) |
211 |
result = ResolverPlaygroundDepcleanResult( |
212 |
- atoms, rval, cleanlist, ordered, req_pkg_count) |
213 |
+ atoms, |
214 |
+ depclean_result.returncode, |
215 |
+ depclean_result.cleanlist, |
216 |
+ depclean_result.ordered, |
217 |
+ depclean_result.req_pkg_count, |
218 |
+ depclean_result.depgraph, |
219 |
+ ) |
220 |
else: |
221 |
params = create_depgraph_params(options, action) |
222 |
success, depgraph, favorites = backtrack_depgraph( |
223 |
@@ -780,18 +786,46 @@ class ResolverPlaygroundTestCase(object): |
224 |
return False |
225 |
return True |
226 |
|
227 |
+ |
228 |
+def _mergelist_str(x, depgraph): |
229 |
+ if isinstance(x, DependencyArg): |
230 |
+ mergelist_str = x.arg |
231 |
+ elif isinstance(x, Blocker): |
232 |
+ mergelist_str = x.atom |
233 |
+ else: |
234 |
+ repo_str = "" |
235 |
+ if x.repo != "test_repo": |
236 |
+ repo_str = _repo_separator + x.repo |
237 |
+ build_id_str = "" |
238 |
+ if (x.type_name == "binary" and |
239 |
+ x.cpv.build_id is not None): |
240 |
+ build_id_str = "-%s" % x.cpv.build_id |
241 |
+ mergelist_str = x.cpv + build_id_str + repo_str |
242 |
+ if x.built: |
243 |
+ if x.operation == "merge": |
244 |
+ desc = x.type_name |
245 |
+ else: |
246 |
+ desc = x.operation |
247 |
+ mergelist_str = "[%s]%s" % (desc, mergelist_str) |
248 |
+ if x.root != depgraph._frozen_config._running_root.root: |
249 |
+ mergelist_str += "{targetroot}" |
250 |
+ return mergelist_str |
251 |
+ |
252 |
+ |
253 |
class ResolverPlaygroundResult(object): |
254 |
|
255 |
checks = ( |
256 |
"success", "mergelist", "use_changes", "license_changes", |
257 |
"unstable_keywords", "slot_collision_solutions", |
258 |
"circular_dependency_solutions", "needed_p_mask_changes", |
259 |
- "unsatisfied_deps", "forced_rebuilds", "required_use_unsatisfied" |
260 |
+ "unsatisfied_deps", "forced_rebuilds", "required_use_unsatisfied", |
261 |
+ "graph_order", |
262 |
) |
263 |
optional_checks = ( |
264 |
"forced_rebuilds", |
265 |
"required_use_unsatisfied", |
266 |
- "unsatisfied_deps" |
267 |
+ "unsatisfied_deps", |
268 |
+ "graph_order", |
269 |
) |
270 |
|
271 |
def __init__(self, atoms, success, mydepgraph, favorites): |
272 |
@@ -810,30 +844,12 @@ class ResolverPlaygroundResult(object): |
273 |
self.forced_rebuilds = None |
274 |
self.required_use_unsatisfied = None |
275 |
|
276 |
+ self.graph_order = [_mergelist_str(node, self.depgraph) for node in self.depgraph._dynamic_config.digraph] |
277 |
+ |
278 |
if self.depgraph._dynamic_config._serialized_tasks_cache is not None: |
279 |
self.mergelist = [] |
280 |
- host_root = self.depgraph._frozen_config._running_root.root |
281 |
for x in self.depgraph._dynamic_config._serialized_tasks_cache: |
282 |
- if isinstance(x, Blocker): |
283 |
- self.mergelist.append(x.atom) |
284 |
- else: |
285 |
- repo_str = "" |
286 |
- if x.repo != "test_repo": |
287 |
- repo_str = _repo_separator + x.repo |
288 |
- build_id_str = "" |
289 |
- if (x.type_name == "binary" and |
290 |
- x.cpv.build_id is not None): |
291 |
- build_id_str = "-%s" % x.cpv.build_id |
292 |
- mergelist_str = x.cpv + build_id_str + repo_str |
293 |
- if x.built: |
294 |
- if x.operation == "merge": |
295 |
- desc = x.type_name |
296 |
- else: |
297 |
- desc = x.operation |
298 |
- mergelist_str = "[%s]%s" % (desc, mergelist_str) |
299 |
- if x.root != host_root: |
300 |
- mergelist_str += "{targetroot}" |
301 |
- self.mergelist.append(mergelist_str) |
302 |
+ self.mergelist.append(_mergelist_str(x, self.depgraph)) |
303 |
|
304 |
if self.depgraph._dynamic_config._needed_use_config_changes: |
305 |
self.use_changes = {} |
306 |
@@ -894,14 +910,17 @@ class ResolverPlaygroundDepcleanResult(object): |
307 |
|
308 |
checks = ( |
309 |
"success", "cleanlist", "ordered", "req_pkg_count", |
310 |
+ "graph_order", |
311 |
) |
312 |
optional_checks = ( |
313 |
"ordered", "req_pkg_count", |
314 |
+ "graph_order", |
315 |
) |
316 |
|
317 |
- def __init__(self, atoms, rval, cleanlist, ordered, req_pkg_count): |
318 |
+ def __init__(self, atoms, rval, cleanlist, ordered, req_pkg_count, depgraph): |
319 |
self.atoms = atoms |
320 |
self.success = rval == 0 |
321 |
self.cleanlist = cleanlist |
322 |
self.ordered = ordered |
323 |
self.req_pkg_count = req_pkg_count |
324 |
+ self.graph_order = [_mergelist_str(node, depgraph) for node in depgraph._dynamic_config.digraph] |
325 |
|
326 |
diff --git a/lib/portage/tests/resolver/test_or_choices.py b/lib/portage/tests/resolver/test_or_choices.py |
327 |
index f31a5ff22..5c6803784 100644 |
328 |
--- a/lib/portage/tests/resolver/test_or_choices.py |
329 |
+++ b/lib/portage/tests/resolver/test_or_choices.py |
330 |
@@ -667,12 +667,16 @@ class OrChoicesTestCase(TestCase): |
331 |
|
332 |
# Test for bug 649622 (with www-client/w3m installed via |
333 |
# xorg-server dependency), where virtual/w3m was pulled in |
334 |
- # only to be removed by the next emerge --depclean. |
335 |
+ # only to be removed by the next emerge --depclean. Note |
336 |
+ # that graph_order must be deterministic in order to achieve |
337 |
+ # deterministic results which are consistent between both |
338 |
+ # update and removal (depclean) actions. |
339 |
ResolverPlaygroundTestCase( |
340 |
['@world'], |
341 |
options = {'--update': True, '--deep': True}, |
342 |
success = True, |
343 |
mergelist=['virtual/w3m-0'], |
344 |
+ graph_order=['@world', '@system', '@selected', '@profile', '[nomerge]app-misc/neofetch-6.1.0', '[nomerge]mail-client/neomutt-20191207', '[nomerge]www-client/lynx-2.9.0_pre4', '[nomerge]x11-base/xorg-server-1.20.7', '[nomerge]app-text/xmlto-0.0.28-r1', '[nomerge]www-client/w3m-0.5.3_p20190105', 'virtual/w3m-0'], |
345 |
), |
346 |
|
347 |
) |
348 |
@@ -702,12 +706,16 @@ class OrChoicesTestCase(TestCase): |
349 |
# Test for bug 649622, where virtual/w3m is removed by |
350 |
# emerge --depclean immediately after it's installed |
351 |
# by a world update. Since virtual/w3m-0 is not removed |
352 |
- # here, this case fails to reproduce bug 649622. |
353 |
+ # here, this case fails to reproduce bug 649622. Note |
354 |
+ # that graph_order must be deterministic in order to achieve |
355 |
+ # deterministic results which are consistent between both |
356 |
+ # update and removal (depclean) actions. |
357 |
ResolverPlaygroundTestCase( |
358 |
[], |
359 |
options={'--depclean': True}, |
360 |
success=True, |
361 |
cleanlist=[], |
362 |
+ graph_order=['@world', '@system', '@selected', '@profile', '@____depclean_protected_set____', '[nomerge]app-misc/neofetch-6.1.0', '[nomerge]mail-client/neomutt-20191207', '[nomerge]www-client/lynx-2.9.0_pre4', '[nomerge]x11-base/xorg-server-1.20.7', '[nomerge]app-text/xmlto-0.0.28-r1', '[nomerge]www-client/w3m-0.5.3_p20190105', '[nomerge]virtual/w3m-0'], |
363 |
), |
364 |
|
365 |
) |