mirror of
https://github.com/caelestia-dots/cli.git
synced 2026-06-19 07:20:01 -05:00
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:
@@ -192,6 +192,7 @@ class Command:
|
||||
self, installer: PackageInstaller, source: DotsSource, current: dict[str, list[str]], desired: list[str]
|
||||
) -> dict[str, list[str]]:
|
||||
to_build = [p for p in desired if p not in current]
|
||||
to_rebuild = self.outdated_local_packages(installer, source, current, desired)
|
||||
to_remove = [p for p in current if p not in desired]
|
||||
installed = dict(current)
|
||||
|
||||
@@ -200,6 +201,11 @@ class Command:
|
||||
log(f"Building new local packages: {', '.join(to_build)}")
|
||||
installed.update(build_local_packages(installer, source, to_build))
|
||||
|
||||
if to_rebuild:
|
||||
print()
|
||||
log(f"Rebuilding updated local packages: {', '.join(to_rebuild)}")
|
||||
installed.update(build_local_packages(installer, source, to_rebuild))
|
||||
|
||||
if to_remove:
|
||||
print()
|
||||
info(f"Local packages no longer required: {', '.join(to_remove)}")
|
||||
@@ -211,6 +217,29 @@ class Command:
|
||||
|
||||
return installed
|
||||
|
||||
def outdated_local_packages(
|
||||
self, installer: PackageInstaller, source: DotsSource, current: dict[str, list[str]], desired: list[str]
|
||||
) -> list[str]:
|
||||
"""Repo paths whose installed packages are older than what the repo would build (skipped when off Arch)."""
|
||||
|
||||
outdated = []
|
||||
for path in desired:
|
||||
if path not in current:
|
||||
continue
|
||||
|
||||
directory = source.working_path(path)
|
||||
if not directory.is_dir():
|
||||
continue
|
||||
|
||||
try:
|
||||
if installer.needs_rebuild(directory, current[path]):
|
||||
outdated.append(path)
|
||||
except PackageError as e:
|
||||
# Failed to read PKGBUILD, leave it as-is
|
||||
warn(f"could not check {path} for updates, leaving as-is: {e}")
|
||||
|
||||
return outdated
|
||||
|
||||
def summarize(self, changeset: Changeset, new_files: list[Path], revived_files: list[Path]) -> None:
|
||||
print()
|
||||
conflicts = len(new_files) + len(revived_files)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user