commit: 5cebd32552c21d48dbe66f89abe7ca4d0bf2dda1 Author: Sam James <sam <AT> gentoo <DOT> org> AuthorDate: Tue Sep 26 23:12:57 2023 +0000 Commit: Sam James <sam <AT> gentoo <DOT> org> CommitDate: Sat Feb 14 03:46:22 2026 +0000 URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=5cebd325
emerge: warn on installed packages in @world without ebuilds available This is an outrageously common issue for users: they emerge something a while ago, it gets treecleaned (possibly without the user ever seeing the p.mask entry if they don't use the particular system often), but Portage diligently works to keep the package installed and its dependencies satisfied. Portage is completely in the right here, but it's very easy for users to not be aware that this is still a constraint they've asked for that is being honoured. This is particularly bad if that package has a slot-operator dependency (:=) on something which has just changed subslot, as it can't be rebuilt (because no ebuild), and users get confusing conflicts as a result. Anyway, for the actual changes: * Use _replace_installed_atom which is already used for solving slot-operator conflicts as a proxy for whether we have an ebuild (or something close enough). * Warn loudly, as we do for other broken things in the world file, if an entry in there has no ebuilds available. At one point, I did also have: * Make _replace_installed_atom's scanning for similar slots optional to speed things up for our "is an ebuild available for thing in world" check. It remains on for all existing consumers. but Zac pointed out this may lead to noise where dependency resolution would then succeed and indeed I can't convince myself it was right, so I've dropped that. Bug: https://bugs.gentoo.org/911180 Signed-off-by: Sam James <sam <AT> gentoo.org> lib/_emerge/depgraph.py | 16 ++ lib/portage/tests/resolver/meson.build | 1 + lib/portage/tests/resolver/test_world_warning.py | 273 +++++++++++++++++++++++ 3 files changed, 290 insertions(+) diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py index b6220ba38a..4c5293cc63 100644 --- a/lib/_emerge/depgraph.py +++ b/lib/_emerge/depgraph.py @@ -5357,6 +5357,22 @@ class depgraph: if not package_is_installed: continue + # If we're emerging @selected or @world, we want to loudly warn about + # no ebuilds being available for packages (bug #911180). + if ( + pkg + and pkg.installed + and pkg.operation == "nomerge" + and isinstance(arg, SetArg) + and arg.name in ("selected, world") + and not self._replace_installed_atom(pkg) + ): + self._dynamic_config._missing_args.append((arg, atom)) + + # But here, we don't warn unlike for @selected or @world because the + # user might be emerging something else and a package instead gets + # dragged in. We may want to warn about this at some point, but one + # step at a time. if not pkg: pprovided_match = False for virt_choice in virtuals.get(atom.cp, []): diff --git a/lib/portage/tests/resolver/meson.build b/lib/portage/tests/resolver/meson.build index 7ae73630be..6787ad3029 100644 --- a/lib/portage/tests/resolver/meson.build +++ b/lib/portage/tests/resolver/meson.build @@ -95,6 +95,7 @@ py.install_sources( 'test_virtual_slot.py', 'test_virtual_cycle.py', 'test_with_test_deps.py', + 'test_world_warning.py', '__init__.py', '__test__.py', ], diff --git a/lib/portage/tests/resolver/test_world_warning.py b/lib/portage/tests/resolver/test_world_warning.py new file mode 100644 index 0000000000..ea728cd15e --- /dev/null +++ b/lib/portage/tests/resolver/test_world_warning.py @@ -0,0 +1,273 @@ +# Copyright 2023-2026 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 WorldWarningTestCase(TestCase): + def testWorldWarningEmerge(self): + """ + Test that we warn about a package in @world with no ebuild. + """ + installed = { + "app-misc/i-do-not-exist-1": {}, + } + ebuilds = {} + + playground = ResolverPlayground( + world=["app-misc/i-do-not-exist"], + ebuilds=ebuilds, + installed=installed, + ) + + test_case = ResolverPlaygroundTestCase( + ["@world"], + mergelist=[], + options={ + "--update": True, + "--deep": True, + }, + success=True, + ) + + try: + # Just to make sure we don't freak out on the general case + # without worrying about the specific output first. + playground.run_TestCase(test_case) + self.assertEqual(test_case.test_success, True, test_case.fail_msg) + + # We need access to the depgraph object to check for missing_args + # so we run again manually. + depgraph = playground.run( + ["@world"], test_case.options, test_case.action + ).depgraph + + self.assertIsNotNone(depgraph._dynamic_config._missing_args) + self.assertTrue( + len(depgraph._dynamic_config._missing_args) > 0, + "Ebuild-less packages did not raise an error", + ) + finally: + playground.cleanup() + + def testWorldWarningVsSubslotRebuildEmerge(self): + """ + Test that we warn about a package in @world with no ebuild + where it is already installed and is blocking a subslot + rebuild. + """ + installed = { + "app-misc/i-do-not-exist-1": { + "EAPI": "8", + "RDEPEND": "dev-libs/libfoo:0/1=", + "DEPEND": "dev-libs/libfoo:0/1=", + }, + "app-misc/bar-1": { + "EAPI": "8", + "RDEPEND": "dev-libs/libfoo:0/1=", + "DEPEND": "dev-libs/libfoo:0/1=", + }, + "dev-libs/libfoo-1": { + "EAPI": "8", + "SLOT": "0/1", + }, + } + ebuilds = { + "app-misc/bar-1": { + "EAPI": "8", + "RDEPEND": "dev-libs/libfoo:=", + "DEPEND": "dev-libs/libfoo:=", + }, + "dev-libs/libfoo-2": { + "EAPI": "8", + "SLOT": "0/2", + }, + } + + playground = ResolverPlayground( + world=["app-misc/i-do-not-exist", "app-misc/bar"], + ebuilds=ebuilds, + installed=installed, + ) + + test_case = ResolverPlaygroundTestCase( + ["@world"], + mergelist=[], + options={ + "--update": True, + "--deep": True, + }, + success=True, + ) + + try: + # Just to make sure we don't freak out on the general case + # without worrying about the specific output first. + playground.run_TestCase(test_case) + self.assertEqual(test_case.test_success, True, test_case.fail_msg) + + # We need access to the depgraph object to check for missing_args + # so we run again manually. + depgraph = playground.run( + ["@world"], test_case.options, test_case.action + ).depgraph + + self.assertIsNotNone(depgraph._dynamic_config._missing_args) + self.assertTrue( + len(depgraph._dynamic_config._missing_args) > 0, + "Ebuild-less packages did not raise an error", + ) + finally: + playground.cleanup() + + def testAbsentWorldWarningEmerge(self): + """ + Test that we do not warn about a package in @world with an ebuild + available. + """ + + installed = { + "app-misc/i-do-exist-1": {}, + } + ebuilds = { + # Package has a newer ebuild available but not + # for the installed version. + "app-misc/i-do-exist-2": {} + } + + playground = ResolverPlayground( + world=["app-misc/i-do-exist"], + ebuilds=ebuilds, + installed=installed, + ) + + test_case = ResolverPlaygroundTestCase( + ["@world"], + mergelist=["app-misc/i-do-exist-2"], + options={ + "--update": True, + "--deep": True, + }, + success=True, + ) + + try: + # Just to make sure we don't freak out on the general case + # without worrying about the specific output first. + playground.run_TestCase(test_case) + self.assertEqual(test_case.test_success, True, test_case.fail_msg) + + # We need access to the depgraph object to check for missing_args + # so we run again manually. + depgraph = playground.run( + ["@world"], test_case.options, test_case.action + ).depgraph + + self.assertIsNotNone(depgraph._dynamic_config._missing_args) + self.assertTrue( + len(depgraph._dynamic_config._missing_args) == 0, + "Package with an ebuild was incorrectly flagged", + ) + finally: + playground.cleanup() + + def testAbsentNotInWorldWarningEmerge(self): + """ + Test that we do not warn about an installed package with no + ebuild available if the package is not in @world. + """ + + installed = { + "app-misc/i-do-not-exist-1": {}, + } + ebuilds = {} + + playground = ResolverPlayground( + world=[], + ebuilds=ebuilds, + installed=installed, + ) + + test_case = ResolverPlaygroundTestCase( + ["@world"], + mergelist=[], + options={ + "--update": True, + "--deep": True, + }, + success=True, + ) + + try: + # Just to make sure we don't freak out on the general case + # without worrying about the specific output first. + playground.run_TestCase(test_case) + self.assertEqual(test_case.test_success, True, test_case.fail_msg) + + # We need access to the depgraph object to check for missing_args + # so we run again manually. + depgraph = playground.run( + ["@world"], test_case.options, test_case.action + ).depgraph + + self.assertIsNotNone(depgraph._dynamic_config._missing_args) + self.assertTrue( + len(depgraph._dynamic_config._missing_args) == 0, + "Package without an ebuild but not in world was incorrectly flagged", + ) + finally: + playground.cleanup() + + def testAbsentNodeNotInWorldWarningEmerge(self): + """ + Test that we do not warn about an installed package with an ebuild available + if the package is not in @world but is depended on by something in @world. + """ + + installed = { + "app-misc/foo-1": {"RDEPEND": "dev-libs/bar"}, + "dev-libs/bar-1": {}, + } + ebuilds = { + "app-misc/foo-1": {"RDEPEND": "dev-libs/bar"}, + } + + playground = ResolverPlayground( + world=["app-misc/foo"], + ebuilds=ebuilds, + installed=installed, + ) + + test_case = ResolverPlaygroundTestCase( + ["@world"], + mergelist=[], + options={ + "--update": True, + "--deep": True, + }, + success=True, + ) + + try: + # Just to make sure we don't freak out on the general case + # without worrying about the specific output first. + playground.run_TestCase(test_case) + self.assertEqual(test_case.test_success, True, test_case.fail_msg) + + # We need access to the depgraph object to check for missing_args + # so we run again manually. + depgraph = playground.run( + ["@world"], test_case.options, test_case.action + ).depgraph + + self.assertIsNotNone(depgraph._dynamic_config._missing_args) + self.assertTrue( + len(depgraph._dynamic_config._missing_args) == 0, + "Package with an ebuild that was reachable from world was incorrectly flagged", + ) + finally: + playground.cleanup()
