diff --git a/plugins/decman-pacman/src/decman/plugins/aur/commands.py b/plugins/decman-pacman/src/decman/plugins/aur/commands.py index ca33720..9bb5c8f 100644 --- a/plugins/decman-pacman/src/decman/plugins/aur/commands.py +++ b/plugins/decman-pacman/src/decman/plugins/aur/commands.py @@ -1,5 +1,4 @@ import decman.plugins.pacman as pacman -import pyalpm import decman.config as config import decman.core.command as command @@ -142,15 +141,7 @@ class AurPacmanInterface(pacman.PacmanInterface): """ Returns a set of orphaned foreign packages. """ - out: set[str] = set() - for pkg in self._handle.get_localdb().pkgcache: - if pkg.reason != pyalpm.PKG_REASON_DEPEND: - continue - if pkg.compute_requiredby(): - continue - if not self._is_native(pkg.name): - out.add(pkg.name) - return out + return self._get_orphans(pacman.PacmanInterface._is_foreign) def is_installable(self, pkg: str) -> bool: """ diff --git a/plugins/decman-pacman/src/decman/plugins/pacman.py b/plugins/decman-pacman/src/decman/plugins/pacman.py index 619c299..0ec77bd 100644 --- a/plugins/decman-pacman/src/decman/plugins/pacman.py +++ b/plugins/decman-pacman/src/decman/plugins/pacman.py @@ -1,5 +1,6 @@ import re import shutil +from typing import Callable import pyalpm @@ -208,6 +209,7 @@ class PacmanInterface: self._handle = self._create_pyalpm_handle() self._name_index = self._create_name_index() self._provides_index = self._create_provides_index() + self._requiredby_index = self._create_requiredby_index() def _create_pyalpm_handle(self): root = "/" @@ -226,8 +228,8 @@ class PacmanInterface: return h - def _create_name_index(self) -> set[str]: - return {pkg.name for db in self._handle.get_syncdbs() for pkg in db.pkgcache} + def _create_name_index(self) -> dict[str, pyalpm.Package]: + return {pkg.name: pkg for db in self._handle.get_syncdbs() for pkg in db.pkgcache} def _create_provides_index(self) -> dict[str, set[str]]: out: dict[str, set[str]] = {} @@ -235,11 +237,18 @@ class PacmanInterface: for pkg in db.pkgcache: for p in pkg.provides: out.setdefault(strip_dependency(p), set()).add(pkg.name) + out.setdefault(p, set()).add(pkg.name) return out + def _create_requiredby_index(self) -> dict[str, set[str]]: + return {p.name: set(p.compute_requiredby()) for p in self._handle.get_localdb().pkgcache} + def _is_native(self, package: str) -> bool: return package in self._name_index + def _is_foreign(self, package: str) -> bool: + return not self._is_native(package) + def get_native_explicit(self) -> set[str]: """ Returns a set of explicitly installed native packages. @@ -252,19 +261,28 @@ class PacmanInterface: return packages + def _get_orphans(self, filter_fn: Callable[["PacmanInterface", str], bool]) -> set[str]: + orphans: set[str] = { + p.name + for p in self._handle.get_localdb().pkgcache + if p.reason == pyalpm.PKG_REASON_DEPEND and filter_fn(self, p.name) + } + + # Prune orphans until there are only packages that are requiredby other orphans + changed = True + while changed: + changed = False + for name in tuple(orphans): + if self._requiredby_index.get(name, set()) - orphans: + orphans.remove(name) + changed = True + return orphans + def get_native_orphans(self) -> set[str]: """ Returns a set of orphaned native packages. """ - out: set[str] = set() - for pkg in self._handle.get_localdb().pkgcache: - if pkg.reason != pyalpm.PKG_REASON_DEPEND: - continue - if pkg.compute_requiredby(): - continue - if self._is_native(pkg.name): - out.add(pkg.name) - return out + return self._get_orphans(PacmanInterface._is_native) def get_foreign_explicit(self) -> set[str]: """ diff --git a/plugins/decman-pacman/tests/test_deep_orphan_removal.py b/plugins/decman-pacman/tests/test_deep_orphan_removal.py new file mode 100644 index 0000000..85a9eac --- /dev/null +++ b/plugins/decman-pacman/tests/test_deep_orphan_removal.py @@ -0,0 +1,88 @@ +import pyalpm +import pytest +from decman.plugins.aur import AurPacmanInterface +from decman.plugins.pacman import PacmanInterface + + +class FakePackage: + def __init__(self, name: str, is_explicit: bool, required_by: list[str]): + self.name = name + self.reason = pyalpm.PKG_REASON_EXPLICIT if is_explicit else pyalpm.PKG_REASON_DEPEND + self.required_by = required_by + self.provides = [name] + + def compute_requiredby(self): + return self.required_by + + +class FakeDB: + def __init__(self, pkgcache: list[FakePackage]): + self.pkgcache = pkgcache + + +class FakePyalpmHandle: + def __init__(self): + pass + + def get_syncdbs(self): + return [ + FakeDB( + [ + FakePackage("a", True, []), + FakePackage("b", False, ["a"]), + FakePackage("c", False, ["b"]), + FakePackage("d", False, []), + FakePackage("e", False, ["f"]), + FakePackage("f", False, ["g"]), + FakePackage("g", False, []), + ] + ) + ] + + def get_localdb(self): + return FakeDB( + self.get_syncdbs()[0].pkgcache + + [ + FakePackage("h", True, []), + FakePackage("i", False, ["h"]), + FakePackage("j", False, []), + FakePackage("k", False, ["l"]), + FakePackage("l", False, []), + ] + ) + + +def fake_create_pyalpm_handle(self): + return FakePyalpmHandle() + + +def test_get_native_orphans_pacman( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(PacmanInterface, "_create_pyalpm_handle", fake_create_pyalpm_handle) + + interface = PacmanInterface( + None, # type: ignore + False, + set(), + 2048, + "/var/lib/pacman/", + ) + + assert interface.get_native_orphans() == {"d", "e", "f", "g"} + + +def test_get_foreign_orphans_aur( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(AurPacmanInterface, "_create_pyalpm_handle", fake_create_pyalpm_handle) + + interface = AurPacmanInterface( + None, # type: ignore + False, + set(), + 2048, + "/var/lib/pacman/", + ) + + assert interface.get_foreign_orphans() == {"j", "k", "l"}