lib.distros.Distro.install_*(): Add install_deps

The install methods of Distro inconsistently resolve dependencies or don't
depending on the descendant's implementation. Pin that more unambigously by
adding a new parameter install_deps, defaulting to True, to the following
methods of lib.Distro:

  _install_local_files()
  _install_urls()
  _install_urls_and_names()
  install()

The implementations in lib.distros.* make use of the new
lib.pm.*.install_packages() method introduced by the previous commit.

Add a unit test directory per Distro descendant.

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v1.4.0
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-10-27 14:54:28 +01:00
commit c5ad946d6e
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
4 changed files with 522 additions and 14 deletions

View file

@ -401,17 +401,27 @@ class Distro(abc.ABC):
async def _install(self, names: Iterable[str], only_update: bool) -> None:
pass
# Default implementation assumes package manager can handle local files.
# Not true for all distros. Override if Distro knows better.
# The default route passes local files to the package manager, which
# resolves their dependencies. The direct route (install_deps = False)
# is not available here; override in descendants that offer it.
async def _install_local_files(
self, paths: Iterable[str], only_update: bool
self, paths: Iterable[str], only_update: bool, install_deps: bool
) -> None:
assert not (only_update and install_deps)
if not install_deps:
raise NotImplementedError(
f'Installing local files without dependency resolution '
f'is not supported on {self.os}'
)
await self._install(paths, only_update = only_update)
# Download first and then install. Override if Distro knows better.
async def _install_urls(self, urls: Iterable[str], only_update: bool) -> None:
async def _install_urls(
self, urls: Iterable[str], only_update: bool, install_deps: bool
) -> None:
from .util import copy
assert not (only_update and install_deps)
tmp: str | None = None
try:
tmp = await self.__exec_context.mktemp(
@ -424,7 +434,9 @@ class Distro(abc.ABC):
raise paths
if isinstance(paths, str):
paths = [paths]
await self._install_local_files(paths, only_update = only_update)
await self._install_local_files(
paths, only_update = only_update, install_deps = install_deps
)
finally:
if tmp is not None:
await self.__exec_context.erase(tmp)
@ -434,8 +446,10 @@ class Distro(abc.ABC):
# - Pass names to package manager
# Override if Distro knows better.
async def _install_urls_and_names(
self, packages: Iterable[str], only_update: bool
self, packages: Iterable[str], only_update: bool, install_deps: bool
) -> None:
assert not (only_update and install_deps)
urls: list[str] = []
names: list[str] = []
for package in packages:
@ -447,15 +461,25 @@ class Distro(abc.ABC):
continue
names.append(package)
if urls:
await self._install_urls(urls, only_update = only_update)
await self._install_urls(
urls, only_update = only_update, install_deps = install_deps
)
if names:
await self._install(names, only_update = only_update)
async def install(self, names: Collection[str], only_update: bool = False) -> None:
async def install(
self,
names: Collection[str],
only_update: bool,
install_deps: bool = True,
) -> None:
assert not (only_update and install_deps)
if not names:
log(WARNING, 'No packages specified for installation')
return
await self._install_urls_and_names(names, only_update = only_update)
await self._install_urls_and_names(
names, only_update = only_update, install_deps = install_deps
)
# -- delete

View file

@ -6,7 +6,13 @@ from typing import TYPE_CHECKING, Any, override
from ...Distro import Distro as Base
from ...log import NOTICE, log
from ...pm.dpkg import list_files, query_packages, run_dpkg, verify_packages
from ...pm.dpkg import (
install_packages,
list_files,
query_packages,
run_dpkg,
verify_packages,
)
if TYPE_CHECKING:
from typing import Iterable
@ -81,6 +87,17 @@ class Distro(Base):
args.extend(names)
await self.apt_get(args)
@override
async def _install_local_files(
self, paths: Iterable[str], only_update: bool, install_deps: bool
) -> None:
if install_deps:
await self._install(paths, only_update = False)
return
await install_packages(
paths, ec = self.ctx, force = True, only_update = only_update
)
@override
async def _delete(self, names: Iterable[str]) -> None:
await self.dpkg(['-P', *names], sudo = True)

View file

@ -3,7 +3,13 @@ from __future__ import annotations
from typing import TYPE_CHECKING, override
from ...Distro import Distro as Base
from ...pm.rpm import list_files, query_packages, run_rpm, verify_packages
from ...pm.rpm import (
install_packages,
list_files,
query_packages,
run_rpm,
verify_packages,
)
if TYPE_CHECKING:
from typing import Any, Collection, Iterable
@ -66,6 +72,17 @@ class Distro(Base):
args.extend(names)
await self.yum(args)
@override
async def _install_local_files(
self, paths: Iterable[str], only_update: bool, install_deps: bool
) -> None:
if install_deps:
await self._install(paths, only_update = False)
return
await install_packages(
paths, ec = self.ctx, force = True, only_update = only_update
)
@override
async def _delete(self, names: Iterable[str]) -> None:
await self.rpm(['-e', *names], sudo = True)

View file

@ -3,7 +3,13 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, override
from ...Distro import Distro as Base
from ...pm.rpm import list_files, query_packages, run_rpm, verify_packages
from ...pm.rpm import (
install_packages,
list_files,
query_packages,
run_rpm,
verify_packages,
)
if TYPE_CHECKING:
from collections.abc import Collection
@ -87,9 +93,14 @@ class Distro(Base):
@override
async def _install_local_files(
self, paths: Iterable[str], only_update: bool
self, paths: Iterable[str], only_update: bool, install_deps: bool
) -> None:
await self.rpm(['-U', '--reinstall', *paths])
if install_deps:
await self._install(paths, only_update = False)
return
await install_packages(
paths, ec = self.ctx, force = True, only_update = only_update
)
@override
async def _delete(self, names: Iterable[str]) -> None: