fix: update local packages during update cmd (#128)

* fix: update local packages based on pkgver/rel

* fixes

* fix: default no rebuild on vercmp fail
This commit is contained in:
2 * r + 2 * t
2026-06-19 18:17:02 +10:00
committed by GitHub
parent cfc62f683e
commit 2e9a387951
2 changed files with 114 additions and 27 deletions
+85 -27
View File
@@ -24,6 +24,46 @@ def _try_run(cmd: list[str], error_msg: str, **kwargs) -> None:
raise PackageError(error_msg) from e
def _read_srcinfo(directory: Path) -> dict[str, list[str]]:
"""Run `makepkg --printsrcinfo` in `directory`, grouping each key to its list of values."""
try:
srcinfo = subprocess.check_output(["makepkg", "--printsrcinfo"], cwd=directory, text=True)
except (subprocess.CalledProcessError, FileNotFoundError) as e:
raise PackageError(f"failed to read package metadata in {directory}") from e
fields: dict[str, list[str]] = {}
for line in srcinfo.splitlines():
key, sep, value = line.partition("=")
if not sep:
continue
fields.setdefault(key.strip(), []).append(value.strip())
return fields
def _srcinfo_version(fields: dict[str, list[str]]) -> str | None:
"""Build the `[epoch:]pkgver-pkgrel` version string from parsed .SRCINFO fields, or None if absent."""
pkgver = next(iter(fields.get("pkgver", [])), None)
pkgrel = next(iter(fields.get("pkgrel", [])), None)
if pkgver is None or pkgrel is None:
return None
version = f"{pkgver}-{pkgrel}"
epoch = next(iter(fields.get("epoch", [])), None)
return f"{epoch}:{version}" if epoch else version
def _vercmp(a: str, b: str) -> int:
"""Use pacman's `vercmp` to compare to package versions."""
try:
return int(subprocess.check_output(["vercmp", a, b], text=True).strip())
except (subprocess.CalledProcessError, FileNotFoundError, ValueError) as e:
warn(f"vercmp failed, assuming equal: {e}")
return 0 # Don't rebuild when unable to check version
def _install_aur_helper(helper: str, noconfirm: bool = False) -> None:
pacman_cmd = ["sudo", "pacman", "-S", "--needed", "git", "base-devel"]
if noconfirm:
@@ -90,7 +130,15 @@ class PackageInstaller(ABC):
"""Build and install the PKGBUILD in `directory`, returning the installed package names."""
@abstractmethod
def is_installed(self, package: str) -> bool: ...
def installed_version(self, package: str) -> str | None:
"""Return the installed version of `package`, or None if it is not installed."""
def is_installed(self, package: str) -> bool:
return self.installed_version(package) is not None
@abstractmethod
def needs_rebuild(self, directory: Path, packages: list[str]) -> bool:
"""Whether the PKGBUILD in `directory` would build a version differing from the installed `packages`."""
@abstractmethod
def system_update(self) -> None: ...
@@ -111,7 +159,10 @@ class NoopInstaller(PackageInstaller):
info(f"Skipping local package build (not on Arch): {directory}")
return []
def is_installed(self, package: str) -> bool:
def installed_version(self, package: str) -> str | None:
return None
def needs_rebuild(self, directory: Path, packages: list[str]) -> bool:
return False
def system_update(self) -> None:
@@ -145,23 +196,9 @@ class ArchInstaller(PackageInstaller):
_try_run([self.helper, "-Rns", *self.flags, *packages], f"failed to remove packages: {', '.join(packages)}")
def build_install(self, directory: Path) -> list[str]:
try:
srcinfo = subprocess.check_output(["makepkg", "--printsrcinfo"], cwd=directory, text=True)
except (subprocess.CalledProcessError, FileNotFoundError) as e:
raise PackageError(f"failed to read package metadata in {directory}") from e
names = []
depends = []
for line in srcinfo.splitlines():
key, sep, value = line.partition("=")
if not sep:
continue
key = key.strip()
if key == "pkgname":
names.append(value.strip())
elif key == "depends":
depends.append(value.strip())
fields = _read_srcinfo(directory)
names = fields.get("pkgname", [])
depends = fields.get("depends", [])
self.install(depends, explicit=False)
@@ -172,16 +209,37 @@ class ArchInstaller(PackageInstaller):
["makepkg", "-fsi", *self.flags], f"failed to build local package in {directory}", cwd=directory, env=env
)
# Clean build artifacts
for artifact in directory.glob("*.pkg.tar*"):
try:
artifact.unlink()
except OSError as e:
warn(f"failed to remove build artifact {artifact}: {e}")
return names
def is_installed(self, package: str) -> bool:
return (
subprocess.run(
["pacman", "-Q", package],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
def installed_version(self, package: str) -> str | None:
result = subprocess.run(
["pacman", "-Q", package],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
if result.returncode != 0:
return None
# `pacman -Q` prints "<name> <version>"
parts = result.stdout.split()
return parts[1] if len(parts) >= 2 else None
def needs_rebuild(self, directory: Path, packages: list[str]) -> bool:
built = _srcinfo_version(_read_srcinfo(directory))
if built is None:
return False # Can't determine the source version, leave as is
# Rebuild when installed version < repo version
# Don't rebuild packages that have been removed
return any(
(installed := self.installed_version(pkg)) is not None and _vercmp(built, installed) > 0 for pkg in packages
)
def system_update(self) -> None: