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]
|
self, installer: PackageInstaller, source: DotsSource, current: dict[str, list[str]], desired: list[str]
|
||||||
) -> dict[str, list[str]]:
|
) -> dict[str, list[str]]:
|
||||||
to_build = [p for p in desired if p not in current]
|
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]
|
to_remove = [p for p in current if p not in desired]
|
||||||
installed = dict(current)
|
installed = dict(current)
|
||||||
|
|
||||||
@@ -200,6 +201,11 @@ class Command:
|
|||||||
log(f"Building new local packages: {', '.join(to_build)}")
|
log(f"Building new local packages: {', '.join(to_build)}")
|
||||||
installed.update(build_local_packages(installer, source, 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:
|
if to_remove:
|
||||||
print()
|
print()
|
||||||
info(f"Local packages no longer required: {', '.join(to_remove)}")
|
info(f"Local packages no longer required: {', '.join(to_remove)}")
|
||||||
@@ -211,6 +217,29 @@ class Command:
|
|||||||
|
|
||||||
return installed
|
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:
|
def summarize(self, changeset: Changeset, new_files: list[Path], revived_files: list[Path]) -> None:
|
||||||
print()
|
print()
|
||||||
conflicts = len(new_files) + len(revived_files)
|
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
|
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:
|
def _install_aur_helper(helper: str, noconfirm: bool = False) -> None:
|
||||||
pacman_cmd = ["sudo", "pacman", "-S", "--needed", "git", "base-devel"]
|
pacman_cmd = ["sudo", "pacman", "-S", "--needed", "git", "base-devel"]
|
||||||
if noconfirm:
|
if noconfirm:
|
||||||
@@ -90,7 +130,15 @@ class PackageInstaller(ABC):
|
|||||||
"""Build and install the PKGBUILD in `directory`, returning the installed package names."""
|
"""Build and install the PKGBUILD in `directory`, returning the installed package names."""
|
||||||
|
|
||||||
@abstractmethod
|
@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
|
@abstractmethod
|
||||||
def system_update(self) -> None: ...
|
def system_update(self) -> None: ...
|
||||||
@@ -111,7 +159,10 @@ class NoopInstaller(PackageInstaller):
|
|||||||
info(f"Skipping local package build (not on Arch): {directory}")
|
info(f"Skipping local package build (not on Arch): {directory}")
|
||||||
return []
|
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
|
return False
|
||||||
|
|
||||||
def system_update(self) -> None:
|
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)}")
|
_try_run([self.helper, "-Rns", *self.flags, *packages], f"failed to remove packages: {', '.join(packages)}")
|
||||||
|
|
||||||
def build_install(self, directory: Path) -> list[str]:
|
def build_install(self, directory: Path) -> list[str]:
|
||||||
try:
|
fields = _read_srcinfo(directory)
|
||||||
srcinfo = subprocess.check_output(["makepkg", "--printsrcinfo"], cwd=directory, text=True)
|
names = fields.get("pkgname", [])
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
depends = fields.get("depends", [])
|
||||||
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())
|
|
||||||
|
|
||||||
self.install(depends, explicit=False)
|
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
|
["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
|
return names
|
||||||
|
|
||||||
def is_installed(self, package: str) -> bool:
|
def installed_version(self, package: str) -> str | None:
|
||||||
return (
|
result = subprocess.run(
|
||||||
subprocess.run(
|
["pacman", "-Q", package],
|
||||||
["pacman", "-Q", package],
|
stdout=subprocess.PIPE,
|
||||||
stdout=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
text=True,
|
||||||
).returncode
|
)
|
||||||
== 0
|
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:
|
def system_update(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user