Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 72 additions & 80 deletions lib/_emerge/depgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -1935,16 +1935,9 @@ def _process_slot_conflicts(self):
# conflicts (or by blind luck).
raise self._unknown_internal_error()

# Both _process_slot_conflict and _slot_operator_trigger_reinstalls
# can call _slot_operator_update_probe, which requires that
# self._dynamic_config._blocked_pkgs has been initialized by a
# call to the _validate_blockers method.
for conflict in self._dynamic_config._package_tracker.slot_conflicts():
self._process_slot_conflict(conflict)

if self._dynamic_config._allow_backtracking:
self._slot_operator_trigger_reinstalls()

def _process_slot_conflict(self, conflict):
"""
Process slot conflict data to identify specific atoms which
Expand Down Expand Up @@ -2900,50 +2893,50 @@ def _replace_installed_atom(self, inst_pkg):

return None

def _slot_operator_trigger_reinstalls(self):
def _slot_operator_trigger_backtracking(self, dep):
"""
Search for packages with slot-operator deps on older slots, and schedule
rebuilds if they can link to a newer slot that's in the graph.
Trigger backtracking for slot operator issues if needed.
Return True if this triggers backtracking, and False otherwise.
"""
if not self._dynamic_config._allow_backtracking:
return False

atom = dep.atom

if not (atom.soname or atom.slot_operator_built):
new_child_slot = self._slot_change_probe(dep)
if new_child_slot is not None:
self._slot_change_backtrack(dep, new_child_slot)
return True

if not (dep.parent and isinstance(dep.parent, Package) and dep.parent.built):
return False

rebuild_if_new_slot = (
self._dynamic_config.myparams.get("rebuild_if_new_slot", "y") == "y"
)

for slot_key, slot_info in self._dynamic_config._slot_operator_deps.items():
for dep in slot_info:
atom = dep.atom

if not (atom.soname or atom.slot_operator_built):
new_child_slot = self._slot_change_probe(dep)
if new_child_slot is not None:
self._slot_change_backtrack(dep, new_child_slot)
continue

if not (
dep.parent and isinstance(dep.parent, Package) and dep.parent.built
):
continue
# If the parent is not installed, check if it needs to be
# rebuilt against an installed instance, since otherwise
# it could trigger downgrade of an installed instance as
# in bug #652938.
want_update_probe = dep.want_update or not dep.parent.installed

# Check for slot update first, since we don't want to
# trigger reinstall of the child package when a newer
# slot will be used instead.
if rebuild_if_new_slot and want_update_probe:
new_dep = self._slot_operator_update_probe(dep, new_child_slot=True)
if new_dep is not None:
self._slot_operator_update_backtrack(dep, new_child_slot=new_dep.child)
return True

# If the parent is not installed, check if it needs to be
# rebuilt against an installed instance, since otherwise
# it could trigger downgrade of an installed instance as
# in bug #652938.
want_update_probe = dep.want_update or not dep.parent.installed

# Check for slot update first, since we don't want to
# trigger reinstall of the child package when a newer
# slot will be used instead.
if rebuild_if_new_slot and want_update_probe:
new_dep = self._slot_operator_update_probe(dep, new_child_slot=True)
if new_dep is not None:
self._slot_operator_update_backtrack(
dep, new_child_slot=new_dep.child
)
if want_update_probe:
if self._slot_operator_update_probe(dep):
self._slot_operator_update_backtrack(dep)
return True

if want_update_probe:
if self._slot_operator_update_probe(dep):
self._slot_operator_update_backtrack(dep)
return False

def _reinstall_for_flags(
self, pkg, forced_flags, orig_use, orig_iuse, cur_use, cur_iuse
Expand Down Expand Up @@ -3434,44 +3427,6 @@ def _add_pkg(self, pkg, dep):
raise
del e

# NOTE: REQUIRED_USE checks are delayed until after
# package selection, since we want to prompt the user
# for USE adjustment rather than have REQUIRED_USE
# affect package selection and || dep choices.
if (
not pkg.built
and pkg._metadata.get("REQUIRED_USE")
and eapi_has_required_use(pkg.eapi)
):
required_use_is_sat = check_required_use(
pkg._metadata["REQUIRED_USE"],
self._pkg_use_enabled(pkg),
pkg.iuse.is_valid_flag,
eapi=pkg.eapi,
)
if not required_use_is_sat:
if dep.atom is not None and dep.parent is not None:
self._add_parent_atom(pkg, (dep.parent, dep.atom))

if arg_atoms:
for parent_atom in arg_atoms:
parent, atom = parent_atom
self._add_parent_atom(pkg, parent_atom)

atom = dep.atom
if atom is None:
atom = Atom("=" + pkg.cpv)
self._dynamic_config._unsatisfied_deps_for_display.append(
((pkg.root, atom), {"myparent": dep.parent, "show_req_use": pkg})
)
self._dynamic_config._required_use_unsatisfied = True
self._dynamic_config._skip_restart = True
# Add pkg to digraph in order to enable autounmask messages
# for this package, which is useful when autounmask USE
# changes have violated REQUIRED_USE.
self._dynamic_config.digraph.add(pkg, dep.parent, priority=priority)
return 0

if not pkg.onlydeps:
existing_node, existing_node_matches = self._check_slot_conflict(
pkg, dep.atom
Expand Down Expand Up @@ -3630,6 +3585,43 @@ def _add_pkg(self, pkg, dep):
and (dep.atom.soname or dep.atom.slot_operator == "=")
):
self._add_slot_operator_dep(dep)
if self._slot_operator_trigger_backtracking(dep):
# Drop slot operator deps that trigger backtracking, since
# they may be irrelevant and therefore we don't want to
# enforce the REQUIRED_USE check that comes below (bug 964705).
# Since backtracking has been triggered, the _need_restart flag
# is set and this depgraph is only useful for collecting
# backtracking parameters at this point, so it is acceptable to
# drop dependencies as needed. It would not be acceptable to
# abort depgraph creation here, since that would not scale well
# for large numbers of slot operator rebuilds.
return 1

# NOTE: REQUIRED_USE checks are delayed until after
# package selection, since we want to prompt the user
# for USE adjustment rather than have REQUIRED_USE
# affect package selection and || dep choices.
if (
not pkg.built
and pkg._metadata.get("REQUIRED_USE")
and eapi_has_required_use(pkg.eapi)
):
required_use_is_sat = check_required_use(
pkg._metadata["REQUIRED_USE"],
self._pkg_use_enabled(pkg),
pkg.iuse.is_valid_flag,
eapi=pkg.eapi,
)
if not required_use_is_sat:
atom = dep.atom
if atom is None:
atom = Atom("=" + pkg.cpv)
self._dynamic_config._unsatisfied_deps_for_display.append(
((pkg.root, atom), {"myparent": dep.parent, "show_req_use": pkg})
)
self._dynamic_config._required_use_unsatisfied = True
self._dynamic_config._skip_restart = True
return 0

recurse = deep is True or not self._too_deep(self._depth_increment(depth, n=1))
dep_stack = self._dynamic_config._dep_stack
Expand Down
1 change: 1 addition & 0 deletions lib/portage/tests/resolver/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ py.install_sources(
'test_backtracking.py',
'test_bdeps.py',
'test_binary_pkg_ebuild_visibility.py',
'test_binpackage_downgrades_slot_dep.py',
'test_blocker.py',
'test_broken_deps.py',
'test_bootstrap_deps.py',
Expand Down
112 changes: 112 additions & 0 deletions lib/portage/tests/resolver/test_binpackage_downgrades_slot_dep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 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 BinpackageDowngradesSlotDepTestCase(TestCase):
def testBinpackageDowngradesSlotDep(self):
python_use = "python_targets_python3_12 +python_targets_python3_13"
python_usedep = "python_targets_python3_12(-)?,python_targets_python3_13(-)?"
common_metadata = {
"EAPI": "8",
"REQUIRED_USE": """
python? (
|| (
python_targets_python3_12
python_targets_python3_13
)
)
""",
}

ebuilds = {
"dev-libs/libxml2-2.13.9": {
"IUSE": f"+python {python_use}",
"SLOT": "2",
**common_metadata,
},
"dev-libs/libxml2-2.14.6": {
"IUSE": f"+python {python_use}",
"SLOT": "2/16",
**common_metadata,
},
"dev-libs/libxslt-1.1.43-r1": {
"IUSE": f"python {python_use}",
"RDEPEND": f"""
>=dev-libs/libxml2-2.13:2=
python? (
>=dev-libs/libxml2-2.13:2=[python,{python_usedep}]
)
""",
**common_metadata,
},
}

binpkgs = {
"dev-libs/libxslt-1.1.43-r1": {
"IUSE": f"python {python_use}",
"RDEPEND": f"""
>=dev-libs/libxml2-2.13:2/2=
python? (
>=dev-libs/libxml2-2.13:2/2=[python,{python_usedep}]
)
""",
**common_metadata,
},
}

installed = {
"dev-libs/libxml2-2.14.6": {
"IUSE": f"+python {python_use}",
"USE": f"python python_targets_python3_13",
"SLOT": "2/16",
**common_metadata,
},
"dev-libs/libxslt-1.1.43-r1": {
"IUSE": f"python {python_use}",
"USE": "python_targets_python3_13",
"RDEPEND": f"""
>=dev-libs/libxml2-2.13:2/16=
python? (
>=dev-libs/libxml2-2.13:2/16=[python,{python_usedep}]
)
""",
**common_metadata,
},
}

world = []
user_config = {"package.use": ["*/* -python_targets_python3_13"]}

playground = ResolverPlayground(
ebuilds=ebuilds,
installed=installed,
binpkgs=binpkgs,
world=world,
user_config=user_config,
debug=False,
)

settings = playground.settings
profile_path = settings.profile_path

test_cases = (
ResolverPlaygroundTestCase(
["dev-libs/libxslt"],
success=True,
options={"--usepkg": True},
mergelist=["dev-libs/libxslt-1.1.43-r1"],
),
)

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()
Loading