Gentoo Archives: gentoo-portage-dev

From: Zac Medico <zmedico@g.o>
To: gentoo-portage-dev@l.g.o
Cc: Zac Medico <zmedico@g.o>
Subject: [gentoo-portage-dev] [PATCH] Allow a package to replace its own buildtime dependency
Date: Sat, 28 Nov 2020 10:27:04
Message-Id: 20201128102436.2786532-1-zmedico@gentoo.org
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