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] backtracking: solve circular dependencies
Date: Mon, 23 Dec 2019 13:32:39
Message-Id: 20191223133022.22365-1-zmedico@gentoo.org
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