commit: 4b885b9ca063c990b1e218c73a786e9d434717e8 Author: Zac Medico <zmedico <AT> gentoo <DOT> org> AuthorDate: Mon Jan 8 06:04:37 2024 +0000 Commit: Zac Medico <zmedico <AT> gentoo <DOT> org> CommitDate: Mon Jan 8 08:08:51 2024 +0000 URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=4b885b9c
_calc_depclean: add dep_check action Add a dep_check action which can be used to check the dependencies of all installed packages. The plan is for depgraph to use this action to check for broken dependencies prior to the merge order calculation. The new frozen_config parameter will allow depgraph to pass a shared frozen config to _calc_depclean. The result of the dep_check action becomes stale as soon as there is any change to the installed packages. So, in order to account for dependencies that may become broken or satisfied during the process of updating installed packages, the merge order calculation will need to refresh the dep_check calculation for every merge order choice that it makes. This refresh will need to be optimized to identify the portion of the graph that would become stale due to a given change, so that it can avoid unnecessary repetition of work. Bug: https://bugs.gentoo.org/921333 Signed-off-by: Zac Medico <zmedico <AT> gentoo.org> lib/_emerge/actions.py | 21 ++++++- lib/_emerge/depgraph.py | 7 ++- lib/portage/tests/resolver/ResolverPlayground.py | 43 ++++++++++++-- lib/portage/tests/resolver/meson.build | 1 + lib/portage/tests/resolver/test_broken_deps.py | 76 ++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 10 deletions(-) diff --git a/lib/_emerge/actions.py b/lib/_emerge/actions.py index 20f3978f77..2710c4856c 100644 --- a/lib/_emerge/actions.py +++ b/lib/_emerge/actions.py @@ -909,7 +909,16 @@ _depclean_result = collections.namedtuple( ) -def _calc_depclean(settings, trees, ldpath_mtimes, myopts, action, args_set, spinner): +def _calc_depclean( + settings, + trees, + ldpath_mtimes, + myopts, + action, + args_set, + spinner, + frozen_config=None, +): allow_missing_deps = bool(args_set) debug = "--debug" in myopts @@ -988,12 +997,14 @@ def _calc_depclean(settings, trees, ldpath_mtimes, myopts, action, args_set, spi writemsg_level("\nCalculating dependencies ") resolver_params = create_depgraph_params(myopts, "remove") - resolver = depgraph(settings, trees, myopts, resolver_params, spinner) + resolver = depgraph( + settings, trees, myopts, resolver_params, spinner, frozen_config=frozen_config + ) resolver._load_vdb() vardb = resolver._frozen_config.trees[eroot]["vartree"].dbapi real_vardb = trees[eroot]["vartree"].dbapi - if action == "depclean": + if action in ("dep_check", "depclean"): if args_set: if deselect: # Start with an empty set. @@ -1002,6 +1013,7 @@ def _calc_depclean(settings, trees, ldpath_mtimes, myopts, action, args_set, spi # Pull in any sets nested within the selected set. selected_set.update(psets["selected"].getNonAtoms()) + if args_set or action == "dep_check": # Pull in everything that's installed but not matched # by an argument atom since we don't want to clean any # package if something depends on it. @@ -1098,6 +1110,9 @@ def _calc_depclean(settings, trees, ldpath_mtimes, myopts, action, args_set, spi if not success: return _depclean_result(1, [], False, 0, resolver) + if action == "dep_check": + return _depclean_result(0, [], False, 0, resolver) + def unresolved_deps(): soname_deps = set() unresolvable = set() diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py index a2865cad23..b859e68224 100644 --- a/lib/_emerge/depgraph.py +++ b/lib/_emerge/depgraph.py @@ -11723,6 +11723,7 @@ def backtrack_depgraph( myaction: Optional[str], myfiles: list[str], spinner: "_emerge.stdout_spinner.stdout_spinner", + frozen_config: Optional[_frozen_depgraph_config] = None, ) -> tuple[Any, depgraph, list[str]]: """ @@ -11747,6 +11748,7 @@ def _backtrack_depgraph( myaction: Optional[str], myfiles: list[str], spinner: "_emerge.stdout_spinner.stdout_spinner", + frozen_config: Optional[_frozen_depgraph_config] = None, ) -> tuple[Any, depgraph, list[str], int, int]: debug = "--debug" in myopts mydepgraph = None @@ -11756,7 +11758,10 @@ def _backtrack_depgraph( backtracker = Backtracker(max_depth) backtracked = 0 - frozen_config = _frozen_depgraph_config(settings, trees, myopts, myparams, spinner) + if frozen_config is None: + frozen_config = _frozen_depgraph_config( + settings, trees, myopts, myparams, spinner + ) while backtracker: if debug and mydepgraph is not None: diff --git a/lib/portage/tests/resolver/ResolverPlayground.py b/lib/portage/tests/resolver/ResolverPlayground.py index 592f5cc5dc..2d26012873 100644 --- a/lib/portage/tests/resolver/ResolverPlayground.py +++ b/lib/portage/tests/resolver/ResolverPlayground.py @@ -1,4 +1,4 @@ -# Copyright 2010-2023 Gentoo Authors +# Copyright 2010-2024 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 import bz2 @@ -33,7 +33,11 @@ from _emerge.actions import _calc_depclean from _emerge.Blocker import Blocker from _emerge.create_depgraph_params import create_depgraph_params from _emerge.DependencyArg import DependencyArg -from _emerge.depgraph import backtrack_depgraph +from _emerge.depgraph import ( + _frozen_depgraph_config, + backtrack_depgraph, +) +from _emerge.Package import Package from _emerge.RootConfig import RootConfig @@ -732,7 +736,16 @@ class ResolverPlayground: portage.util.noiselimit = -2 _emerge.emergelog._disable = True - if action in ("depclean", "prune"): + # NOTE: frozen_config could be cached and reused if options and params were constant. + params_action = ( + "remove" if action in ("dep_check", "depclean", "prune") else action + ) + params = create_depgraph_params(options, params_action) + frozen_config = _frozen_depgraph_config( + self.settings, self.trees, options, params, None + ) + + if params_action == "remove": depclean_result = _calc_depclean( self.settings, self.trees, @@ -741,6 +754,7 @@ class ResolverPlayground: action, InternalPackageSet(initial_atoms=atoms, allow_wildcard=True), None, + frozen_config=frozen_config, ) result = ResolverPlaygroundDepcleanResult( atoms, @@ -751,9 +765,15 @@ class ResolverPlayground: depclean_result.depgraph, ) else: - params = create_depgraph_params(options, action) success, depgraph, favorites = backtrack_depgraph( - self.settings, self.trees, options, params, action, atoms, None + self.settings, + self.trees, + options, + params, + action, + atoms, + None, + frozen_config=frozen_config, ) depgraph._show_merge_list() depgraph.display_problems() @@ -939,7 +959,8 @@ class ResolverPlaygroundTestCase: ) and expected is not None ): - expected = set(expected) + # unsatisfied_deps can be a dict for depclean-like actions + expected = expected if isinstance(expected, dict) else set(expected) elif key == "forced_rebuilds" and expected is not None: expected = {k: set(v) for k, v in expected.items()} @@ -1109,11 +1130,14 @@ class ResolverPlaygroundDepcleanResult: "ordered", "req_pkg_count", "graph_order", + "unsatisfied_deps", ) optional_checks = ( + "cleanlist", "ordered", "req_pkg_count", "graph_order", + "unsatisfied_deps", ) def __init__(self, atoms, rval, cleanlist, ordered, req_pkg_count, depgraph): @@ -1125,3 +1149,10 @@ class ResolverPlaygroundDepcleanResult: self.graph_order = [ _mergelist_str(node, depgraph) for node in depgraph._dynamic_config.digraph ] + self.unsatisfied_deps = {} + for dep in depgraph._dynamic_config._initially_unsatisfied_deps: + if isinstance(dep.parent, Package): + parent_repr = dep.parent.cpv + else: + parent_repr = dep.parent.arg + self.unsatisfied_deps.setdefault(parent_repr, set()).add(dep.atom) diff --git a/lib/portage/tests/resolver/meson.build b/lib/portage/tests/resolver/meson.build index 77c65a511e..8892c78131 100644 --- a/lib/portage/tests/resolver/meson.build +++ b/lib/portage/tests/resolver/meson.build @@ -15,6 +15,7 @@ py.install_sources( 'test_bdeps.py', 'test_binary_pkg_ebuild_visibility.py', 'test_blocker.py', + 'test_broken_deps.py', 'test_changed_deps.py', 'test_circular_choices.py', 'test_circular_choices_rust.py', diff --git a/lib/portage/tests/resolver/test_broken_deps.py b/lib/portage/tests/resolver/test_broken_deps.py new file mode 100644 index 0000000000..8ca7809d34 --- /dev/null +++ b/lib/portage/tests/resolver/test_broken_deps.py @@ -0,0 +1,76 @@ +# Copyright 2024 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 BrokenDepsTestCase(TestCase): + def testBrokenDeps(self): + """ + Test the _calc_depclean "dep_check" action which will eventually + be used to check for unsatisfied deps of installed packages + for bug 921333. + """ + ebuilds = { + "dev-qt/qtcore-5.15.12": { + "EAPI": "8", + }, + "dev-qt/qtcore-5.15.11-r1": { + "EAPI": "8", + }, + "dev-qt/qtxmlpatterns-5.15.12": { + "EAPI": "8", + "DEPEND": "=dev-qt/qtcore-5.15.12*", + "RDEPEND": "=dev-qt/qtcore-5.15.12*", + }, + "dev-qt/qtxmlpatterns-5.15.11": { + "EAPI": "8", + "DEPEND": "=dev-qt/qtcore-5.15.11*", + "RDEPEND": "=dev-qt/qtcore-5.15.11*", + }, + "kde-frameworks/syntax-highlighting-5.113.0": { + "EAPI": "8", + "DEPEND": ">=dev-qt/qtxmlpatterns-5.15.9:5", + }, + } + installed = { + "dev-qt/qtcore-5.15.12": { + "EAPI": "8", + }, + "dev-qt/qtxmlpatterns-5.15.11": { + "EAPI": "8", + "DEPEND": "=dev-qt/qtcore-5.15.11*", + "RDEPEND": "=dev-qt/qtcore-5.15.11*", + }, + "kde-frameworks/syntax-highlighting-5.113.0": { + "EAPI": "8", + "DEPEND": ">=dev-qt/qtxmlpatterns-5.15.9:5", + }, + } + + world = ("kde-frameworks/syntax-highlighting",) + + test_cases = ( + ResolverPlaygroundTestCase( + [], + action="dep_check", + success=True, + unsatisfied_deps={ + "dev-qt/qtxmlpatterns-5.15.11": {"=dev-qt/qtcore-5.15.11*"} + }, + ), + ) + + playground = ResolverPlayground( + ebuilds=ebuilds, installed=installed, world=world + ) + 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.cleanup()
