1 |
If a package has a buildtime dependency on a previous version that |
2 |
it will replace, then do not treat it as a slot conflict. This |
3 |
solves inappropriate behavior for dev-lang/rust[system-bootstrap]. |
4 |
|
5 |
This requires adjustments to package selection logic in several |
6 |
locations, in order to ensure that an installed package instance |
7 |
will be selected to satisfy a buildtime dependency when |
8 |
appropriate. Dependencies of the installed package will be |
9 |
entirely ignored, but that has already been the case when using |
10 |
installed package to break cycles, as discussed in bug 199856. |
11 |
|
12 |
Bug: https://bugs.gentoo.org/756961 |
13 |
Signed-off-by: Zac Medico <zmedico@g.o> |
14 |
--- |
15 |
lib/_emerge/depgraph.py | 68 ++++++++++++++---- |
16 |
lib/portage/dep/dep_check.py | 24 ++++--- |
17 |
.../resolver/test_circular_choices_rust.py | 69 +++++++++++++++++++ |
18 |
3 files changed, 139 insertions(+), 22 deletions(-) |
19 |
create mode 100644 lib/portage/tests/resolver/test_circular_choices_rust.py |
20 |
|
21 |
diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py |
22 |
index d10474ab3..1271bda3e 100644 |
23 |
--- a/lib/_emerge/depgraph.py |
24 |
+++ b/lib/_emerge/depgraph.py |
25 |
@@ -85,6 +85,8 @@ from _emerge.resolver.output import Display, format_unmatched_atom |
26 |
|
27 |
# Exposes a depgraph interface to dep_check. |
28 |
_dep_check_graph_interface = collections.namedtuple('_dep_check_graph_interface',( |
29 |
+ # Checks if parent package will replace child. |
30 |
+ 'will_replace_child', |
31 |
# Indicates a removal action, like depclean or prune. |
32 |
'removal_action', |
33 |
# Checks if update is desirable for a given package. |
34 |
@@ -507,6 +509,7 @@ class _dynamic_depgraph_config: |
35 |
# Track missed updates caused by solved conflicts. |
36 |
self._conflict_missed_update = collections.defaultdict(dict) |
37 |
dep_check_iface = _dep_check_graph_interface( |
38 |
+ will_replace_child=depgraph._will_replace_child, |
39 |
removal_action="remove" in myparams, |
40 |
want_update_pkg=depgraph._want_update_pkg, |
41 |
) |
42 |
@@ -3104,6 +3107,22 @@ class depgraph: |
43 |
self._frozen_config.myopts, |
44 |
modified_use=self._pkg_use_enabled(pkg))), |
45 |
level=logging.DEBUG, noiselevel=-1) |
46 |
+ elif (pkg.installed and myparent and |
47 |
+ pkg.root == myparent.root and |
48 |
+ pkg.slot_atom == myparent.slot_atom): |
49 |
+ # If the parent package is replacing the child package then |
50 |
+ # there's no slot conflict. Since the child will be replaced, |
51 |
+ # do not add it to the graph. No attempt will be made to |
52 |
+ # satisfy its dependencies, which is unsafe if it has any |
53 |
+ # missing dependencies, as discussed in bug 199856. |
54 |
+ if debug: |
55 |
+ writemsg_level( |
56 |
+ "%s%s %s\n" % ("Replace Child:".ljust(15), |
57 |
+ pkg, pkg_use_display(pkg, |
58 |
+ self._frozen_config.myopts, |
59 |
+ modified_use=self._pkg_use_enabled(pkg))), |
60 |
+ level=logging.DEBUG, noiselevel=-1) |
61 |
+ return 1 |
62 |
|
63 |
else: |
64 |
if debug: |
65 |
@@ -5877,6 +5896,27 @@ class depgraph: |
66 |
(arg_atoms or update) and |
67 |
not self._too_deep(depth)) |
68 |
|
69 |
+ def _will_replace_child(self, parent, root, atom): |
70 |
+ """ |
71 |
+ Check if a given parent package will replace a child package |
72 |
+ for the given root and atom. |
73 |
+ |
74 |
+ @param parent: parent package |
75 |
+ @type parent: Package |
76 |
+ @param root: child root |
77 |
+ @type root: str |
78 |
+ @param atom: child atom |
79 |
+ @type atom: Atom |
80 |
+ @rtype: Package |
81 |
+ @return: child package to replace, or None |
82 |
+ """ |
83 |
+ if parent.root != root or parent.cp != atom.cp: |
84 |
+ return None |
85 |
+ for child in self._iter_match_pkgs(self._frozen_config.roots[root], "installed", atom): |
86 |
+ if parent.slot_atom == child.slot_atom: |
87 |
+ return child |
88 |
+ return None |
89 |
+ |
90 |
def _too_deep(self, depth): |
91 |
""" |
92 |
Check if a package depth is deeper than the max allowed depth. |
93 |
@@ -6440,19 +6480,21 @@ class depgraph: |
94 |
# Calculation of USE for unbuilt ebuilds is relatively |
95 |
# expensive, so it is only performed lazily, after the |
96 |
# above visibility checks are complete. |
97 |
- |
98 |
- myarg = None |
99 |
- try: |
100 |
- for myarg, myarg_atom in self._iter_atoms_for_pkg(pkg): |
101 |
- if myarg.force_reinstall: |
102 |
- reinstall = True |
103 |
- break |
104 |
- except InvalidDependString: |
105 |
- if not installed: |
106 |
- # masked by corruption |
107 |
- continue |
108 |
- if not installed and myarg: |
109 |
- found_available_arg = True |
110 |
+ effective_parent = parent or self._select_atoms_parent |
111 |
+ if not (effective_parent and self._will_replace_child( |
112 |
+ effective_parent, root, atom)): |
113 |
+ myarg = None |
114 |
+ try: |
115 |
+ for myarg, myarg_atom in self._iter_atoms_for_pkg(pkg): |
116 |
+ if myarg.force_reinstall: |
117 |
+ reinstall = True |
118 |
+ break |
119 |
+ except InvalidDependString: |
120 |
+ if not installed: |
121 |
+ # masked by corruption |
122 |
+ continue |
123 |
+ if not installed and myarg: |
124 |
+ found_available_arg = True |
125 |
|
126 |
if atom.package and atom.unevaluated_atom.use: |
127 |
#Make sure we don't miss a 'missing IUSE'. |
128 |
diff --git a/lib/portage/dep/dep_check.py b/lib/portage/dep/dep_check.py |
129 |
index b89d5d651..3bed6c348 100644 |
130 |
--- a/lib/portage/dep/dep_check.py |
131 |
+++ b/lib/portage/dep/dep_check.py |
132 |
@@ -405,9 +405,15 @@ def dep_zapdeps(unreduced, reduced, myroot, use_binaries=0, trees=None, |
133 |
for atom in atoms: |
134 |
if atom.blocker: |
135 |
continue |
136 |
+ |
137 |
+ # It's not a downgrade if parent is replacing child. |
138 |
+ replacing = (parent and graph_interface and |
139 |
+ graph_interface.will_replace_child(parent, myroot, atom)) |
140 |
# Ignore USE dependencies here since we don't want USE |
141 |
# settings to adversely affect || preference evaluation. |
142 |
avail_pkg = mydbapi_match_pkgs(atom.without_use) |
143 |
+ if not avail_pkg and replacing: |
144 |
+ avail_pkg = [replacing] |
145 |
if avail_pkg: |
146 |
avail_pkg = avail_pkg[-1] # highest (ascending order) |
147 |
avail_slot = Atom("%s:%s" % (atom.cp, avail_pkg.slot)) |
148 |
@@ -416,7 +422,7 @@ def dep_zapdeps(unreduced, reduced, myroot, use_binaries=0, trees=None, |
149 |
all_use_satisfied = False |
150 |
break |
151 |
|
152 |
- if graph_db is not None and downgrade_probe is not None: |
153 |
+ if not replacing and graph_db is not None and downgrade_probe is not None: |
154 |
slot_matches = graph_db.match_pkgs(avail_slot) |
155 |
if (len(slot_matches) > 1 and |
156 |
avail_pkg < slot_matches[-1] and |
157 |
@@ -463,7 +469,7 @@ def dep_zapdeps(unreduced, reduced, myroot, use_binaries=0, trees=None, |
158 |
avail_pkg = avail_pkg_use |
159 |
avail_slot = Atom("%s:%s" % (atom.cp, avail_pkg.slot)) |
160 |
|
161 |
- if downgrade_probe is not None and graph is not None: |
162 |
+ if not replacing and downgrade_probe is not None and graph is not None: |
163 |
highest_in_slot = mydbapi_match_pkgs(avail_slot) |
164 |
highest_in_slot = (highest_in_slot[-1] |
165 |
if highest_in_slot else None) |
166 |
@@ -576,14 +582,14 @@ def dep_zapdeps(unreduced, reduced, myroot, use_binaries=0, trees=None, |
167 |
this_choice.all_in_graph = all_in_graph |
168 |
|
169 |
circular_atom = None |
170 |
- if not (parent is None or priority is None) and \ |
171 |
- (parent.onlydeps or |
172 |
- (priority.buildtime and not priority.satisfied and not priority.optional)): |
173 |
+ if parent and parent.onlydeps: |
174 |
# Check if the atom would result in a direct circular |
175 |
- # dependency and try to avoid that if it seems likely |
176 |
- # to be unresolvable. This is only relevant for |
177 |
- # buildtime deps that aren't already satisfied by an |
178 |
- # installed package. |
179 |
+ # dependency and avoid that for --onlydeps arguments |
180 |
+ # since it can defeat the purpose of --onlydeps. |
181 |
+ # This check should only be used for --onlydeps |
182 |
+ # arguments, since it can interfere with circular |
183 |
+ # dependency backtracking choices, causing the test |
184 |
+ # case for bug 756961 to fail. |
185 |
cpv_slot_list = [parent] |
186 |
for atom in atoms: |
187 |
if atom.blocker: |
188 |
diff --git a/lib/portage/tests/resolver/test_circular_choices_rust.py b/lib/portage/tests/resolver/test_circular_choices_rust.py |
189 |
new file mode 100644 |
190 |
index 000000000..5da3e59aa |
191 |
--- /dev/null |
192 |
+++ b/lib/portage/tests/resolver/test_circular_choices_rust.py |
193 |
@@ -0,0 +1,69 @@ |
194 |
+# Copyright 2020 Gentoo Authors |
195 |
+# Distributed under the terms of the GNU General Public License v2 |
196 |
+ |
197 |
+from portage.tests import TestCase |
198 |
+from portage.tests.resolver.ResolverPlayground import ( |
199 |
+ ResolverPlayground, |
200 |
+ ResolverPlaygroundTestCase, |
201 |
+) |
202 |
+ |
203 |
+ |
204 |
+class CircularRustTestCase(TestCase): |
205 |
+ def testCircularPypyExe(self): |
206 |
+ |
207 |
+ ebuilds = { |
208 |
+ "dev-lang/rust-1.47.0-r2": { |
209 |
+ "EAPI": "7", |
210 |
+ "SLOT": "stable/1.47", |
211 |
+ "BDEPEND": "|| ( =dev-lang/rust-1.46* =dev-lang/rust-bin-1.46* =dev-lang/rust-1.47* =dev-lang/rust-bin-1.47* )", |
212 |
+ }, |
213 |
+ "dev-lang/rust-1.46.0": { |
214 |
+ "EAPI": "7", |
215 |
+ "SLOT": "stable/1.46", |
216 |
+ "BDEPEND": "|| ( =dev-lang/rust-1.45* =dev-lang/rust-bin-1.45* =dev-lang/rust-1.46* =dev-lang/rust-bin-1.46* )", |
217 |
+ }, |
218 |
+ "dev-lang/rust-bin-1.47.0": { |
219 |
+ "EAPI": "7", |
220 |
+ }, |
221 |
+ "dev-lang/rust-bin-1.46.0": { |
222 |
+ "EAPI": "7", |
223 |
+ }, |
224 |
+ } |
225 |
+ |
226 |
+ installed = { |
227 |
+ "dev-lang/rust-1.46.0": { |
228 |
+ "EAPI": "7", |
229 |
+ "SLOT": "stable/1.46", |
230 |
+ "BDEPEND": "|| ( =dev-lang/rust-1.45* =dev-lang/rust-bin-1.45* =dev-lang/rust-1.46* =dev-lang/rust-bin-1.46* )", |
231 |
+ }, |
232 |
+ } |
233 |
+ |
234 |
+ test_cases = ( |
235 |
+ # Test bug 756961, where a circular dependency was reported |
236 |
+ # when a package would replace its own builtime dependency. |
237 |
+ # This needs to be tested with and without --update, since |
238 |
+ # that affects package selection logic significantly, |
239 |
+ # expecially for packages given as arguments. |
240 |
+ ResolverPlaygroundTestCase( |
241 |
+ ["dev-lang/rust"], |
242 |
+ mergelist=["dev-lang/rust-1.47.0-r2"], |
243 |
+ success=True, |
244 |
+ ), |
245 |
+ ResolverPlaygroundTestCase( |
246 |
+ ["dev-lang/rust"], |
247 |
+ options={"--update": True}, |
248 |
+ mergelist=["dev-lang/rust-1.47.0-r2"], |
249 |
+ success=True, |
250 |
+ ), |
251 |
+ ) |
252 |
+ |
253 |
+ playground = ResolverPlayground( |
254 |
+ ebuilds=ebuilds, installed=installed, debug=False |
255 |
+ ) |
256 |
+ try: |
257 |
+ for test_case in test_cases: |
258 |
+ playground.run_TestCase(test_case) |
259 |
+ self.assertEqual(test_case.test_success, True, test_case.fail_msg) |
260 |
+ finally: |
261 |
+ playground.debug = False |
262 |
+ playground.cleanup() |
263 |
-- |
264 |
2.26.2 |