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
8 changed files with 522 additions and 14 deletions

View file

@ -286,4 +286,23 @@ sde = SuseDistro(
assert asyncio.run(sde.pkg_files()) == []
assert '--queryformat' in cl.cmds[-1]
# -- _install_local_files --
# -- With dependency resolution, the files go to the package manager;
# DummyDistro._install() is a no-op
asyncio.run(
d._install_local_files(['/a/jw-base-run-1.0.0-42.rpm'], False, install_deps = True)
)
# -- Without, the base implementation raises: there is no direct route
try:
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.rpm'], False, install_deps = False
)
)
assert False, 'Should have raised'
except NotImplementedError:
pass
print('All Distro tests passed')

View file

@ -0,0 +1,4 @@
TOPDIR = ../../../../../../../
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/dirs.mk

View file

@ -0,0 +1,7 @@
TOPDIR = ../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-run.mk
all:
test: run

View file

@ -0,0 +1,127 @@
from __future__ import annotations
import asyncio
from typing import override
from jw.pkg.lib.distros.debian.Distro import Distro as DebianDistro
from jw.pkg.lib.ec.Local import Local
from jw.pkg.lib.Result import Result
class DispatchLocal(Local):
"""A canned execution context that answers dpkg queries and
the apt simulation"""
def __init__(
self,
files: dict[str, str],
installed: dict[str, str],
simulate: str,
) -> None:
super().__init__('local')
self.files = files
self.installed = installed
self.simulate = simulate
self.cmds: list[list[str]] = []
@override
async def _run(
self,
cmd: list[str],
wd: str | None,
verbose: bool,
cmd_input: bytes | None,
mod_env: dict[str, str] | None,
interactive: bool,
log_prefix: str,
) -> Result:
self.cmds.append(cmd)
if '-I' in cmd:
return Result(stdout = self.files.get(cmd[-1], '').encode(), status = 0)
if cmd[0].endswith('dpkg-query'):
name = cmd[-1]
return Result(
stdout = self.installed.get(name, '').encode(),
status = 0 if name in self.installed else 1,
)
if '/usr/bin/apt-get' in cmd and '-s' in cmd:
return Result(stdout = self.simulate.encode(), status = 0)
return Result(stdout = b'', status = 0)
def dpkg_args(cmd: list[str]) -> list[str]:
"""The dpkg command without the sudo prefix and its options"""
return cmd[cmd.index('/usr/bin/dpkg'):]
# -- A mixed transaction runs two installs, the upgrades first: the
# distro installs local files with force, which keeps the reinstall
# semantics for already-installed versions
cl = DispatchLocal(
files = {
'/a/jw-base-run-1.0.0-42.deb': 'Package: jw-base\nVersion: 1.0.0-42',
'/a/jw-base-run-1.0.0-41.deb': 'Package: jw-base\nVersion: 1.0.0-41',
},
installed = {'jw-base': 'jw-base|||||1.0.0-42'},
simulate = 'Inst jw-base [1.0.0-41]\n',
)
d = DebianDistro(cl, id = 'debian')
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.deb', '/a/jw-base-run-1.0.0-41.deb'],
False,
install_deps = False,
)
)
installs = [dpkg_args(cmd) for cmd in cl.cmds if '-i' in cmd]
assert installs == [
['/usr/bin/dpkg', '-i', '/a/jw-base-run-1.0.0-41.deb'],
[
'/usr/bin/dpkg',
'-i',
'--force-reinstall',
'/a/jw-base-run-1.0.0-42.deb',
],
]
# -- only_update raises on files whose package is not installed
cl = DispatchLocal(
files = {
'/a/jw-base-run-1.0.0-42.deb': 'Package: jw-base\nVersion: 1.0.0-42',
'/a/jw-foo-run-1.0.0-42.deb': 'Package: jw-foo\nVersion: 1.0.0-42',
},
installed = {'jw-base': 'jw-base|||||1.0.0-42'},
simulate = '',
)
d = DebianDistro(cl, id = 'debian')
try:
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.deb', '/a/jw-foo-run-1.0.0-42.deb'],
True,
install_deps = False,
)
)
except Exception as e:
assert '/a/jw-foo-run-1.0.0-42.deb' in str(e)
else:
raise AssertionError('expected an exception')
assert not [cmd for cmd in cl.cmds if '-i' in cmd]
# -- deps routes local files through the package manager
cl = DispatchLocal(
files = {'/a/jw-base-run-1.0.0-42.deb': 'Package: jw-base\nVersion: 1.0.0-42'},
installed = {'jw-base': 'jw-base|||||1.0.0-42'},
simulate = '',
)
d = DebianDistro(cl, id = 'debian')
asyncio.run(
d._install_local_files(['/a/jw-base-run-1.0.0-42.deb'], False, install_deps = True)
)
pm_cmds = [cmd for cmd in cl.cmds if '/usr/bin/apt-get' in cmd]
assert len(pm_cmds) == 1
args = pm_cmds[0][pm_cmds[0].index('/usr/bin/apt-get'):]
assert 'install' in args
assert '--no-install-recommends' in args
assert args[-1] == '/a/jw-base-run-1.0.0-42.deb'
assert not [cmd for cmd in cl.cmds if '-i' in cmd]
print('All debian distro tests passed')

View file

@ -0,0 +1,7 @@
TOPDIR = ../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-run.mk
all:
test: run

View file

@ -0,0 +1,134 @@
from __future__ import annotations
import asyncio
from typing import override
from jw.pkg.lib.distros.redhat.Distro import Distro as RedhatDistro
from jw.pkg.lib.ec.Local import Local
from jw.pkg.lib.Result import Result
class DispatchLocal(Local):
"""A canned execution context that answers rpm queries"""
def __init__(self, files: dict[str, str], installed: dict[str, str]) -> None:
super().__init__('local')
self.files = files
self.installed = installed
self.cmds: list[list[str]] = []
@override
async def _run(
self,
cmd: list[str],
wd: str | None,
verbose: bool,
cmd_input: bytes | None,
mod_env: dict[str, str] | None,
interactive: bool,
log_prefix: str,
) -> Result:
self.cmds.append(cmd)
if '-qp' in cmd:
return Result(stdout = self.files.get(cmd[-1], '').encode(), status = 0)
if '-q' in cmd:
name = cmd[-1]
return Result(
stdout = self.installed.get(name, '').encode(),
status = 0 if name in self.installed else 1,
)
return Result(stdout = b'', status = 0)
def rpm_args(cmd: list[str]) -> list[str]:
"""The rpm command without the sudo prefix and its options"""
return cmd[cmd.index('/usr/bin/rpm'):]
# -- A mixed transaction runs two installs, the upgrades first: the
# distro installs local files with force, which keeps the reinstall
# semantics for already-installed versions
cl = DispatchLocal(
files = {
'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42',
'/a/jw-base-run-1.0.0-41.x86_64.rpm': 'jw-base\t1.0.0-41',
},
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
)
d = RedhatDistro(cl, id = 'redhat')
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/a/jw-base-run-1.0.0-41.x86_64.rpm'],
False,
install_deps = False,
)
)
installs = [rpm_args(cmd) for cmd in cl.cmds if '-U' in cmd]
assert installs == [
['/usr/bin/rpm', '-U', '/a/jw-base-run-1.0.0-41.x86_64.rpm'],
[
'/usr/bin/rpm',
'-U',
'--reinstall',
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
],
]
# -- A single file whose version is already installed is reinstalled
cl = DispatchLocal(
files = {'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42'},
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
)
d = RedhatDistro(cl, id = 'redhat')
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.x86_64.rpm'], False, install_deps = False
)
)
assert rpm_args(cl.cmds[-1]) == [
'/usr/bin/rpm',
'-U',
'--reinstall',
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
]
# -- only_update raises on files whose package is not installed
cl = DispatchLocal(
files = {
'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42',
'/a/jw-foo-run-1.0.0-42.x86_64.rpm': 'jw-foo\t1.0.0-42',
},
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
)
d = RedhatDistro(cl, id = 'redhat')
try:
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/a/jw-foo-run-1.0.0-42.x86_64.rpm'],
True,
install_deps = False,
)
)
except Exception as e:
assert '/a/jw-foo-run-1.0.0-42.x86_64.rpm' in str(e)
else:
raise AssertionError('expected an exception')
assert not [cmd for cmd in cl.cmds if '-U' in cmd]
# -- deps routes local files through the package manager
cl = DispatchLocal(
files = {'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42'},
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
)
d = RedhatDistro(cl, id = 'redhat')
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.x86_64.rpm'], False, install_deps = True
)
)
pm_cmds = [cmd for cmd in cl.cmds if '/usr/bin/yum' in cmd]
assert len(pm_cmds) == 1
args = pm_cmds[0][pm_cmds[0].index('/usr/bin/yum'):]
assert 'install' in args
assert args[-1] == '/a/jw-base-run-1.0.0-42.x86_64.rpm'
assert not [cmd for cmd in cl.cmds if '-U' in cmd]
print('All redhat distro tests passed')

View file

@ -0,0 +1,7 @@
TOPDIR = ../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-run.mk
all:
test: run

View file

@ -0,0 +1,134 @@
from __future__ import annotations
import asyncio
from typing import override
from jw.pkg.lib.distros.suse.Distro import Distro as SuseDistro
from jw.pkg.lib.ec.Local import Local
from jw.pkg.lib.Result import Result
class DispatchLocal(Local):
"""A canned execution context that answers rpm queries"""
def __init__(self, files: dict[str, str], installed: dict[str, str]) -> None:
super().__init__('local')
self.files = files
self.installed = installed
self.cmds: list[list[str]] = []
@override
async def _run(
self,
cmd: list[str],
wd: str | None,
verbose: bool,
cmd_input: bytes | None,
mod_env: dict[str, str] | None,
interactive: bool,
log_prefix: str,
) -> Result:
self.cmds.append(cmd)
if '-qp' in cmd:
return Result(stdout = self.files.get(cmd[-1], '').encode(), status = 0)
if '-q' in cmd:
name = cmd[-1]
return Result(
stdout = self.installed.get(name, '').encode(),
status = 0 if name in self.installed else 1,
)
return Result(stdout = b'', status = 0)
def rpm_args(cmd: list[str]) -> list[str]:
"""The rpm command without the sudo prefix and its options"""
return cmd[cmd.index('/usr/bin/rpm'):]
# -- A mixed transaction runs two installs, the upgrades first: the
# distro installs local files with force, which keeps the reinstall
# semantics for already-installed versions
cl = DispatchLocal(
files = {
'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42',
'/a/jw-base-run-1.0.0-41.x86_64.rpm': 'jw-base\t1.0.0-41',
},
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
)
d = SuseDistro(cl, id = 'suse')
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/a/jw-base-run-1.0.0-41.x86_64.rpm'],
False,
install_deps = False,
)
)
installs = [rpm_args(cmd) for cmd in cl.cmds if '-U' in cmd]
assert installs == [
['/usr/bin/rpm', '-U', '/a/jw-base-run-1.0.0-41.x86_64.rpm'],
[
'/usr/bin/rpm',
'-U',
'--reinstall',
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
],
]
# -- A single file whose version is already installed is reinstalled
cl = DispatchLocal(
files = {'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42'},
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
)
d = SuseDistro(cl, id = 'suse')
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.x86_64.rpm'], False, install_deps = False
)
)
assert rpm_args(cl.cmds[-1]) == [
'/usr/bin/rpm',
'-U',
'--reinstall',
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
]
# -- only_update raises on files whose package is not installed
cl = DispatchLocal(
files = {
'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42',
'/a/jw-foo-run-1.0.0-42.x86_64.rpm': 'jw-foo\t1.0.0-42',
},
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
)
d = SuseDistro(cl, id = 'suse')
try:
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/a/jw-foo-run-1.0.0-42.x86_64.rpm'],
True,
install_deps = False,
)
)
except Exception as e:
assert '/a/jw-foo-run-1.0.0-42.x86_64.rpm' in str(e)
else:
raise AssertionError('expected an exception')
assert not [cmd for cmd in cl.cmds if '-U' in cmd]
# -- deps routes local files through the package manager
cl = DispatchLocal(
files = {'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42'},
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
)
d = SuseDistro(cl, id = 'suse')
asyncio.run(
d._install_local_files(
['/a/jw-base-run-1.0.0-42.x86_64.rpm'], False, install_deps = True
)
)
pm_cmds = [cmd for cmd in cl.cmds if '/usr/bin/zypper' in cmd]
assert len(pm_cmds) == 1
args = pm_cmds[0][pm_cmds[0].index('/usr/bin/zypper'):]
assert 'install' in args
assert args[-1] == '/a/jw-base-run-1.0.0-42.x86_64.rpm'
assert not [cmd for cmd in cl.cmds if '-U' in cmd]
print('All suse distro tests passed')