1 |
Store circular dependency edges as backtracking parameters, and use them |
2 |
to adjust || preferences in order to solve circular dependencies. This |
3 |
extends direct dependency cycle avoidance to handle indirect dependency |
4 |
cycles, which solves the cmake-bootstrap test case for bug 703440. If any |
5 |
cycle(s) remain unsolved by the next backtracking run, then backtracking |
6 |
aborts and the cycle(s) are reported as usual. |
7 |
|
8 |
Bug: https://bugs.gentoo.org/384107 |
9 |
Bug: https://bugs.gentoo.org/703440 |
10 |
Signed-off-by: Zac Medico <zmedico@g.o> |
11 |
--- |
12 |
lib/_emerge/depgraph.py | 46 ++++++++++++++++++- |
13 |
lib/_emerge/resolver/backtracking.py | 10 +++- |
14 |
lib/portage/dep/dep_check.py | 10 ++++ |
15 |
.../tests/resolver/test_circular_choices.py | 7 +++ |
16 |
4 files changed, 69 insertions(+), 4 deletions(-) |
17 |
|
18 |
diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py |
19 |
index 1a5448c8f..84cf127c1 100644 |
20 |
--- a/lib/_emerge/depgraph.py |
21 |
+++ b/lib/_emerge/depgraph.py |
22 |
@@ -480,6 +480,7 @@ class _dynamic_depgraph_config(object): |
23 |
# of flags causing the rejection. |
24 |
self.ignored_binaries = {} |
25 |
|
26 |
+ self._circular_dependency = backtrack_parameters.circular_dependency |
27 |
self._needed_unstable_keywords = backtrack_parameters.needed_unstable_keywords |
28 |
self._needed_p_mask_changes = backtrack_parameters.needed_p_mask_changes |
29 |
self._needed_license_changes = backtrack_parameters.needed_license_changes |
30 |
@@ -4798,6 +4799,7 @@ class depgraph(object): |
31 |
mytrees.pop("pkg_use_enabled", None) |
32 |
mytrees.pop("parent", None) |
33 |
mytrees.pop("atom_graph", None) |
34 |
+ mytrees.pop("circular_dependency", None) |
35 |
mytrees.pop("priority", None) |
36 |
|
37 |
mytrees["pkg_use_enabled"] = self._pkg_use_enabled |
38 |
@@ -4805,6 +4807,7 @@ class depgraph(object): |
39 |
self._select_atoms_parent = parent |
40 |
mytrees["parent"] = parent |
41 |
mytrees["atom_graph"] = atom_graph |
42 |
+ mytrees["circular_dependency"] = self._dynamic_config._circular_dependency |
43 |
if priority is not None: |
44 |
mytrees["priority"] = priority |
45 |
|
46 |
@@ -4818,6 +4821,7 @@ class depgraph(object): |
47 |
mytrees.pop("pkg_use_enabled", None) |
48 |
mytrees.pop("parent", None) |
49 |
mytrees.pop("atom_graph", None) |
50 |
+ mytrees.pop("circular_dependency", None) |
51 |
mytrees.pop("priority", None) |
52 |
mytrees.update(backup_state) |
53 |
if not mycheck[0]: |
54 |
@@ -8215,7 +8219,34 @@ class depgraph(object): |
55 |
|
56 |
if not selected_nodes: |
57 |
self._dynamic_config._circular_deps_for_display = mygraph |
58 |
- self._dynamic_config._skip_restart = True |
59 |
+ |
60 |
+ unsolved_cycle = False |
61 |
+ if self._dynamic_config._allow_backtracking: |
62 |
+ |
63 |
+ backtrack_infos = self._dynamic_config._backtrack_infos |
64 |
+ backtrack_infos.setdefault("config", {}) |
65 |
+ circular_dependency = backtrack_infos["config"].setdefault("circular_dependency", {}) |
66 |
+ |
67 |
+ cycles = mygraph.get_cycles(ignore_priority=DepPrioritySatisfiedRange.ignore_medium_soft) |
68 |
+ for cycle in cycles: |
69 |
+ for node in cycle: |
70 |
+ if node in self._dynamic_config._circular_dependency: |
71 |
+ unsolved_cycle = True |
72 |
+ circular_child = None |
73 |
+ for other_node in cycle: |
74 |
+ if other_node == node: |
75 |
+ break |
76 |
+ circular_child = other_node |
77 |
+ else: |
78 |
+ circular_child = other_node |
79 |
+ if circular_child is not None: |
80 |
+ circular_dependency.setdefault(node, set()).add(circular_child) |
81 |
+ |
82 |
+ if unsolved_cycle or not self._dynamic_config._allow_backtracking: |
83 |
+ self._dynamic_config._skip_restart = True |
84 |
+ else: |
85 |
+ self._dynamic_config._need_restart = True |
86 |
+ |
87 |
raise self._unknown_internal_error() |
88 |
|
89 |
# At this point, we've succeeded in selecting one or more nodes, so |
90 |
@@ -9450,6 +9481,17 @@ class depgraph(object): |
91 |
return self._dynamic_config._need_restart and \ |
92 |
not self._dynamic_config._skip_restart |
93 |
|
94 |
+ def need_display_problems(self): |
95 |
+ """ |
96 |
+ Returns true if this depgraph has problems which need to be |
97 |
+ displayed to the user. |
98 |
+ """ |
99 |
+ if self.need_config_change(): |
100 |
+ return True |
101 |
+ if self._dynamic_config._circular_deps_for_display: |
102 |
+ return True |
103 |
+ return False |
104 |
+ |
105 |
def need_config_change(self): |
106 |
""" |
107 |
Returns true if backtracking should terminate due to a needed |
108 |
@@ -9878,7 +9920,7 @@ def _backtrack_depgraph(settings, trees, myopts, myparams, myaction, myfiles, sp |
109 |
elif backtracker: |
110 |
backtracked += 1 |
111 |
|
112 |
- if not (success or mydepgraph.need_config_change()) and backtracked: |
113 |
+ if backtracked and not success and not mydepgraph.need_display_problems(): |
114 |
|
115 |
if debug: |
116 |
writemsg_level( |
117 |
diff --git a/lib/_emerge/resolver/backtracking.py b/lib/_emerge/resolver/backtracking.py |
118 |
index 99e4565c8..e94ee2f1e 100644 |
119 |
--- a/lib/_emerge/resolver/backtracking.py |
120 |
+++ b/lib/_emerge/resolver/backtracking.py |
121 |
@@ -6,12 +6,14 @@ import copy |
122 |
class BacktrackParameter(object): |
123 |
|
124 |
__slots__ = ( |
125 |
+ "circular_dependency", |
126 |
"needed_unstable_keywords", "runtime_pkg_mask", "needed_use_config_changes", "needed_license_changes", |
127 |
"prune_rebuilds", "rebuild_list", "reinstall_list", "needed_p_mask_changes", |
128 |
"slot_operator_mask_built", "slot_operator_replace_installed" |
129 |
) |
130 |
|
131 |
def __init__(self): |
132 |
+ self.circular_dependency = {} |
133 |
self.needed_unstable_keywords = set() |
134 |
self.needed_p_mask_changes = set() |
135 |
self.runtime_pkg_mask = {} |
136 |
@@ -31,6 +33,7 @@ class BacktrackParameter(object): |
137 |
|
138 |
#Shallow copies are enough here, as we only need to ensure that nobody adds stuff |
139 |
#to our sets and dicts. The existing content is immutable. |
140 |
+ result.circular_dependency = copy.copy(self.circular_dependency) |
141 |
result.needed_unstable_keywords = copy.copy(self.needed_unstable_keywords) |
142 |
result.needed_p_mask_changes = copy.copy(self.needed_p_mask_changes) |
143 |
result.needed_use_config_changes = copy.copy(self.needed_use_config_changes) |
144 |
@@ -49,7 +52,8 @@ class BacktrackParameter(object): |
145 |
return result |
146 |
|
147 |
def __eq__(self, other): |
148 |
- return self.needed_unstable_keywords == other.needed_unstable_keywords and \ |
149 |
+ return self.circular_dependency == other.circular_dependency and \ |
150 |
+ self.needed_unstable_keywords == other.needed_unstable_keywords and \ |
151 |
self.needed_p_mask_changes == other.needed_p_mask_changes and \ |
152 |
self.runtime_pkg_mask == other.runtime_pkg_mask and \ |
153 |
self.needed_use_config_changes == other.needed_use_config_changes and \ |
154 |
@@ -195,7 +199,9 @@ class Backtracker(object): |
155 |
para = new_node.parameter |
156 |
|
157 |
for change, data in changes.items(): |
158 |
- if change == "needed_unstable_keywords": |
159 |
+ if change == "circular_dependency": |
160 |
+ para.circular_dependency.update(data) |
161 |
+ elif change == "needed_unstable_keywords": |
162 |
para.needed_unstable_keywords.update(data) |
163 |
elif change == "needed_p_mask_changes": |
164 |
para.needed_p_mask_changes.update(data) |
165 |
diff --git a/lib/portage/dep/dep_check.py b/lib/portage/dep/dep_check.py |
166 |
index 2896e2389..e1742aaeb 100644 |
167 |
--- a/lib/portage/dep/dep_check.py |
168 |
+++ b/lib/portage/dep/dep_check.py |
169 |
@@ -367,6 +367,7 @@ def dep_zapdeps(unreduced, reduced, myroot, use_binaries=0, trees=None, |
170 |
pkg_use_enabled = trees[myroot].get("pkg_use_enabled") |
171 |
want_update_pkg = trees[myroot].get("want_update_pkg") |
172 |
downgrade_probe = trees[myroot].get("downgrade_probe") |
173 |
+ circular_dependency = trees[myroot].get("circular_dependency") |
174 |
vardb = None |
175 |
if "vartree" in trees[myroot]: |
176 |
vardb = trees[myroot]["vartree"].dbapi |
177 |
@@ -589,6 +590,15 @@ def dep_zapdeps(unreduced, reduced, myroot, use_binaries=0, trees=None, |
178 |
if match_from_list(atom, cpv_slot_list): |
179 |
circular_atom = atom |
180 |
break |
181 |
+ else: |
182 |
+ for circular_child in circular_dependency.get(parent, []): |
183 |
+ for atom in atoms: |
184 |
+ if atom.match(circular_child): |
185 |
+ circular_atom = atom |
186 |
+ break |
187 |
+ if circular_atom is not None: |
188 |
+ break |
189 |
+ |
190 |
if circular_atom is not None: |
191 |
other.append(this_choice) |
192 |
else: |
193 |
diff --git a/lib/portage/tests/resolver/test_circular_choices.py b/lib/portage/tests/resolver/test_circular_choices.py |
194 |
index d963280b7..32b6ce293 100644 |
195 |
--- a/lib/portage/tests/resolver/test_circular_choices.py |
196 |
+++ b/lib/portage/tests/resolver/test_circular_choices.py |
197 |
@@ -33,9 +33,16 @@ class CircularJsoncppCmakeBootstrapTestCase(TestCase): |
198 |
# (dev-libs/jsoncpp-1.9.2:0/0::test_repo, ebuild scheduled for merge) (buildtime_slot_op) |
199 |
ResolverPlaygroundTestCase( |
200 |
['dev-util/cmake'], |
201 |
+ options = {"--backtrack": 0}, |
202 |
circular_dependency_solutions = {}, |
203 |
success = False, |
204 |
), |
205 |
+ # Demonstrate that backtracking adjusts || preferences in order to solve bug 703440. |
206 |
+ ResolverPlaygroundTestCase( |
207 |
+ ['dev-util/cmake'], |
208 |
+ mergelist = ['dev-util/cmake-bootstrap-3.16.2', 'dev-libs/jsoncpp-1.9.2', 'dev-util/cmake-3.16.2'], |
209 |
+ success = True, |
210 |
+ ), |
211 |
) |
212 |
|
213 |
playground = ResolverPlayground(ebuilds=ebuilds) |
214 |
-- |
215 |
2.21.0 |