commit: 35a737cc9c93f4c38c65a57a9f70948ddd416714 Author: Zac Medico <zmedico <AT> gentoo <DOT> org> AuthorDate: Mon Nov 10 02:31:20 2025 +0000 Commit: Zac Medico <zmedico <AT> gentoo <DOT> org> CommitDate: Mon Nov 10 02:31:20 2025 +0000 URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=35a737cc
depgraph: avoid RecursionError for virtual cycle The included test cases report these errors: !!! virtual cycle detected: virtual/gzip-1::test_repo !!! virtual cycle detected: virtual/A-1::test_repo virtual/B-1::test_repo virtual/C-1::test_repo Bug: https://bugs.gentoo.org/965570 Signed-off-by: Zac Medico <zmedico <AT> gentoo.org> lib/_emerge/depgraph.py | 34 ++++++++++++- lib/portage/tests/resolver/ResolverPlayground.py | 5 ++ lib/portage/tests/resolver/meson.build | 1 + lib/portage/tests/resolver/test_virtual_cycle.py | 63 ++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py index 87d090ae3b..d3d97b59bc 100644 --- a/lib/_emerge/depgraph.py +++ b/lib/_emerge/depgraph.py @@ -1,4 +1,4 @@ -# Copyright 1999-2024 Gentoo Authors +# Copyright 1999-2025 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 import errno @@ -694,6 +694,9 @@ class depgraph: maxsize=1000 )(self._slot_operator_check_reverse_dependencies) + self._virt_deps_visible_recursion = set() + self._virtual_cycle = None + def _index_binpkgs(self): for root in self._frozen_config.trees: bindb = self._frozen_config.trees[root]["bintree"].dbapi @@ -4828,6 +4831,18 @@ class depgraph: if spinner is not None and spinner.update is not spinner.update_quiet: spinner_cb.handle = self._event_loop.call_soon(spinner_cb) return self._select_files(args) + except self._virtual_cycle_error as e: + self._virtual_cycle = e.value + + msg = ["\n\n!!! virtual cycle detected:\n\n"] + for pkg in sorted(self._virtual_cycle): + msg.append(f" {pkg.cpv}::{pkg.repo}\n") + msg.append("\n") + + for chunk in msg: + writemsg(chunk, noiselevel=-1) + self._dynamic_config._skip_restart = True + return 0, [] finally: if spinner_cb.handle is not None: spinner_cb.handle.cancel() @@ -5998,6 +6013,17 @@ class depgraph: useful for checking if it will be necessary to expand virtual slots, for cases like bug #382557. """ + if pkg in self._virt_deps_visible_recursion: + raise self._virtual_cycle_error(list(self._virt_deps_visible_recursion)) + + self._virt_deps_visible_recursion.add(pkg) + try: + return self._virt_deps_visible_imp(pkg, ignore_use) + finally: + self._virt_deps_visible_recursion.remove(pkg) + + def _virt_deps_visible_imp(self, pkg, ignore_use): + try: rdepend = self._select_atoms( pkg.root, @@ -11334,6 +11360,12 @@ class depgraph: been disqualified due to autounmask changes. """ + class _virtual_cycle_error(_internal_exception): + """ + This is raised by _virt_deps_visible when a virtual cycle is + detected. + """ + def need_restart(self): return ( self._dynamic_config._need_restart diff --git a/lib/portage/tests/resolver/ResolverPlayground.py b/lib/portage/tests/resolver/ResolverPlayground.py index 47d93274f8..3e2cc6ec70 100644 --- a/lib/portage/tests/resolver/ResolverPlayground.py +++ b/lib/portage/tests/resolver/ResolverPlayground.py @@ -1034,6 +1034,7 @@ class ResolverPlaygroundResult: "forced_rebuilds", "required_use_unsatisfied", "graph_order", + "virtual_cycle", ) optional_checks = ( "forced_rebuilds", @@ -1057,6 +1058,7 @@ class ResolverPlaygroundResult: self.unsatisfied_deps = frozenset() self.forced_rebuilds = None self.required_use_unsatisfied = None + self.virtual_cycle = None self.graph_order = [ _mergelist_str(node, self.depgraph) @@ -1135,6 +1137,9 @@ class ResolverPlaygroundResult: if required_use_unsatisfied: self.required_use_unsatisfied = set(required_use_unsatisfied) + if self.depgraph._virtual_cycle: + self.virtual_cycle = {pkg.cpv for pkg in self.depgraph._virtual_cycle} + class ResolverPlaygroundDepcleanResult: checks = ( diff --git a/lib/portage/tests/resolver/meson.build b/lib/portage/tests/resolver/meson.build index 7569af8cd0..653b0536e1 100644 --- a/lib/portage/tests/resolver/meson.build +++ b/lib/portage/tests/resolver/meson.build @@ -90,6 +90,7 @@ py.install_sources( 'test_use_dep_defaults.py', 'test_virtual_minimize_children.py', 'test_virtual_slot.py', + 'test_virtual_cycle.py', 'test_with_test_deps.py', '__init__.py', '__test__.py', diff --git a/lib/portage/tests/resolver/test_virtual_cycle.py b/lib/portage/tests/resolver/test_virtual_cycle.py new file mode 100644 index 0000000000..720f3c461a --- /dev/null +++ b/lib/portage/tests/resolver/test_virtual_cycle.py @@ -0,0 +1,63 @@ +# Copyright 2025 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +from portage.tests import TestCase +from portage.tests.resolver.ResolverPlayground import ( + ResolverPlayground, + ResolverPlaygroundTestCase, +) + + +class VirtualCycleTestCase(TestCase): + def testVirtualCycle(self): + ebuilds = { + "app-misc/foo-1": { + "EAPI": "8", + "RDEPEND": "virtual/A", + }, + "virtual/A-1": { + "EAPI": "8", + "RDEPEND": "virtual/B", + }, + "virtual/B-1": { + "EAPI": "8", + "RDEPEND": "virtual/C", + }, + "virtual/C-1": { + "EAPI": "8", + "RDEPEND": "virtual/A", + }, + "app-misc/bar-1": { + "EAPI": "8", + "RDEPEND": "virtual/gzip", + }, + "virtual/gzip-1": { + "EAPI": "8", + "RDEPEND": "virtual/gzip", + }, + } + + test_cases = ( + # Test direct virtual cycle for bug 965570. + ResolverPlaygroundTestCase( + ["app-misc/bar"], + success=False, + virtual_cycle={"virtual/gzip-1"}, + ), + # Test indirect virtual cycle for bug 965570. + ResolverPlaygroundTestCase( + ["app-misc/foo"], + success=False, + virtual_cycle={"virtual/A-1", "virtual/B-1", "virtual/C-1"}, + ), + ) + + playground = ResolverPlayground(debug=False, ebuilds=ebuilds) + + try: + for test_case in test_cases: + playground.run_TestCase(test_case) + self.assertEqual(test_case.test_success, True, test_case.fail_msg) + finally: + playground.debug = False + playground.cleanup()
