Improve orphan package discovery

This commit is contained in:
Kivi Kaitaniemi
2025-12-27 23:44:35 +02:00
parent 6078968ac1
commit 7e0b49be2f
3 changed files with 118 additions and 21 deletions
@@ -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:
"""
@@ -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]:
"""
@@ -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"}