jw-pkg packages install: Add -x/--exact option #159

Merged
Jan Lindemann merged 4 commits from jan/feature/20261028-jw-pkg-packages-install-add-x-exact-option into master 2026-10-28 21:02:52 +01:00 AGit
4 changed files with 659 additions and 2 deletions
Showing only changes of commit 36978c55e2 - Show all commits

lib.pm.*.install_packages(): Add functions

Currently, jw-pkg install is sometimes left to the high-level package
manager, which is allowed to install dependencies along, and that's
sometimes unwanted, but there's no backend in the low-level package manager
module lib.pm to allow specifying that. This commit adds it:
lib.pm.*.install_packages().

The functions install a list of local package files using the low-level
package managers rpm / dpkg, bypassing the high-level package managers yum
/ apt / zypper.

pm.rpm runs rpm -U and pm.dpkg runs dpkg -i. Every file is classified
against the installed state of its package, queried by name: a file
for a package that is not installed is installed fresh, a file with
another version installed is upgraded, and a file whose version is already
installed is reinstalled when force is set and skipped otherwise. On rpm,
plain -U refuses a package that is already installed, and --reinstall
aborts the transaction when a file of the incoming package differs from
the installed one, so the upgrades run before the reinstalls and the
same-version files need force.

With only_update, a file for a package that is not installed raises before
anything is installed.

The dpkg route is gated on an apt simulation, because dpkg -i leaves a
half-configured package when a dependency is missing: the simulation runs
first and the install is refused when it would install or remove anything
but the given files' own packages.

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v1.4.0
Signed-off-by: Jan Lindemann <jan@janware.com>
Jan Lindemann 2026-10-27 14:51:56 +01:00
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61

View file

@ -94,6 +94,111 @@ async def list_files(
file_list_str = await run_dpkg(['-L', *names], sudo = False, ec = ec)
return file_list_str.splitlines()
async def _file_name_version(
path: str,
ec: ExecContext | None = None,
) -> tuple[str, str]:
"""Return the (name, version) pair of a local deb file."""
out = await run_dpkg(['-I', path], sudo = False, throw = True, ec = ec)
name: str | None = None
version: str | None = None
for line in out.splitlines():
line = line.strip()
if line.startswith('Package: '):
name = line[len('Package: '):].strip()
elif line.startswith('Version: '):
version = line[len('Version: '):].strip()
if name is None or version is None:
raise Exception(f'cannot read the package name and version of {path}')
return name, version
async def _simulate_install(
files: list[str],
expected: set[str],
ec: ExecContext | None = None,
) -> None:
"""Gate a direct dpkg install on an apt simulation.
dpkg -i leaves a half-configured package when a dependency is
missing, instead of aborting the transaction cleanly, so the
install is simulated first and refused when the simulation
would install or remove anything but the expected packages.
"""
result = await run_sudo(
['/usr/bin/apt-get', '-s', '--no-install-recommends', 'install', *files],
ec = ec,
cmd_input = InputMode.NonInteractive,
throw = False,
)
out = result.stdout_str
if result.status != 0:
raise Exception(f'apt simulation of the direct install failed:\n{out}')
inst: list[str] = []
remv: list[str] = []
for line in out.splitlines():
parts = line.split()
if parts and parts[0] == 'Inst':
inst.append(parts[1])
elif parts and parts[0] == 'Remv':
remv.append(parts[1])
pulled = [pkg for pkg in inst if pkg not in expected]
if pulled or remv:
raise Exception(f'apt simulation would install {pulled} or remove {remv}')
async def install_packages(
paths: Iterable[str],
ec: ExecContext | None = None,
force: bool = False,
only_update: bool = False,
) -> None:
"""Install local deb files.
Every install is gated on an apt simulation, which refuses the
transaction when it would install or remove anything but the
given files' own packages; see _simulate_install().
Files are classified against the installed state of their
packages: files for packages that are not installed are
installed fresh, files with another version installed are
upgraded, and files whose version is already installed are
reinstalled with -i --force-reinstall when force is set and
skipped otherwise. The upgrades run before the reinstalls.
With only_update, a file for a package that is not installed
raises before anything is installed.
"""
paths = list(paths)
if not paths:
return
update: list[str] = []
reinstall: list[str] = []
not_installed: list[str] = []
names: dict[str, str] = {}
for path in paths:
name, version = await _file_name_version(path, ec = ec)
names[path] = name
pkgs = list(await query_packages([name], ec = ec, throw = False))
if not pkgs:
(not_installed if only_update else update).append(path)
continue
if version in {p.version for p in pkgs if p.version}:
reinstall.append(path)
continue
update.append(path)
if not_installed:
raise Exception(
'only_update: package not installed for ' +
'\n'.join(f' {p}' for p in not_installed)
)
selected = [*update, *reinstall] if force else update
if not selected:
return
await _simulate_install(selected, {names[path] for path in selected}, ec = ec)
if update:
await run_dpkg(['-i', *update], sudo = True, ec = ec)
if force and reinstall:
await run_dpkg(['-i', '--force-reinstall', *reinstall], sudo = True, ec = ec)
def _parse_verify_output(
pkg: str,
output: str,

View file

@ -80,6 +80,69 @@ async def list_files(
)
return stdout.splitlines()
async def _file_name_version(
path: str,
ec: ExecContext | None = None,
) -> tuple[str, str]:
"""Return the (name, version-release) pair of a local rpm file."""
out = await run_rpm(
['-qp', '--queryformat', '%{NAME}\t%{VERSION}-%{RELEASE}', path],
throw = True,
sudo = False,
mode = InputMode.NonInteractive,
ec = ec
)
name, version = out.split('\t', 1)
return name, version
async def install_packages(
paths: Iterable[str],
ec: ExecContext | None = None,
force: bool = False,
only_update: bool = False,
) -> None:
"""Install local rpm files.
Files are classified against the installed state of their
packages: files for packages that are not installed are
installed fresh, files with another version installed are
upgraded, and files whose version is already installed are
reinstalled with -U --reinstall when force is set and
skipped otherwise, because plain -U refuses a package that
is already installed. The upgrades run before the
reinstalls: --reinstall aborts the whole transaction when a
file of the incoming package differs from the installed
one, which is exactly what an upgrade changes.
With only_update, a file for a package that is not installed
raises before anything is installed.
"""
paths = list(paths)
if not paths:
return
update: list[str] = []
reinstall: list[str] = []
not_installed: list[str] = []
for path in paths:
name, version = await _file_name_version(path, ec = ec)
pkgs = list(await query_packages([name], ec = ec, throw = False))
if not pkgs:
(not_installed if only_update else update).append(path)
continue
if version in {p.version for p in pkgs if p.version}:
reinstall.append(path)
continue
update.append(path)
if not_installed:
raise Exception(
'only_update: package not installed for ' +
'\n'.join(f' {p}' for p in not_installed)
)
if update:
await run_rpm(['-U', *update], sudo = True, ec = ec)
if force and reinstall:
await run_rpm(['-U', '--reinstall', *reinstall], sudo = True, ec = ec)
def _parse_verify_output(
pkg: str,
output: str,

View file

@ -5,7 +5,7 @@ import asyncio
from typing import override
from jw.pkg.lib.ec.Local import Local
from jw.pkg.lib.pm.dpkg import list_files
from jw.pkg.lib.pm.dpkg import install_packages, list_files
from jw.pkg.lib.Result import Result
class CannedLocal(Local):
@ -79,3 +79,261 @@ assert cl2.cmds == [
cl3 = CannedTwoStage('', '')
assert asyncio.run(list_files([], ec = cl3)) == []
assert cl3.cmds == []
# -- install_packages --
class InstallLocal(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,
simulate_status: int = 0,
) -> None:
super().__init__('local')
self.files = files
self.installed = installed
self.simulate = simulate
self.simulate_status = simulate_status
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:
return Result(
stdout = self.simulate.encode(), status = self.simulate_status
)
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'):]
# -- Without force or only_update, all files go through a single
# plain -i, gated on the apt simulation
il = InstallLocal(
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-41'},
simulate = 'Inst jw-base [1.0.0-42]\n',
)
asyncio.run(install_packages(['/a/jw-base-run-1.0.0-42.deb'], ec = il))
sim = [cmd for cmd in il.cmds if '/usr/bin/apt-get' in cmd]
assert len(sim) == 1
args = sim[0][sim[0].index('/usr/bin/apt-get'):]
assert '-s' in args
assert '--no-install-recommends' in args
assert args[-1] == '/a/jw-base-run-1.0.0-42.deb'
installs = [dpkg_args(cmd) for cmd in il.cmds if '-i' in cmd]
assert installs == [['/usr/bin/dpkg', '-i', '/a/jw-base-run-1.0.0-42.deb']]
# -- Without force, a file whose version is already installed is
# skipped
il = InstallLocal(
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 = '',
)
asyncio.run(install_packages(['/a/jw-base-run-1.0.0-42.deb'], ec = il))
assert not [cmd for cmd in il.cmds if '-i' in cmd]
assert not [cmd for cmd in il.cmds if '/usr/bin/apt-get' in cmd]
# -- An empty selection runs no commands
il = InstallLocal(files = {}, installed = {}, simulate = '')
assert asyncio.run(install_packages([], ec = il)) is None
assert il.cmds == []
# -- force reinstalls a file whose version is already installed
il = InstallLocal(
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 = '',
)
asyncio.run(install_packages(['/a/jw-base-run-1.0.0-42.deb'], ec = il, force = True))
assert dpkg_args(il.cmds[-1]) == [
'/usr/bin/dpkg',
'-i',
'--force-reinstall',
'/a/jw-base-run-1.0.0-42.deb',
]
# -- force upgrades a file whose package is installed in another
# version with plain -i
il = InstallLocal(
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-41'},
simulate = 'Inst jw-base [1.0.0-42]\n',
)
asyncio.run(install_packages(['/a/jw-base-run-1.0.0-42.deb'], ec = il, force = True))
assert dpkg_args(il.cmds[-1]) == ['/usr/bin/dpkg', '-i', '/a/jw-base-run-1.0.0-42.deb']
# -- force installs a file whose package is not installed with
# plain -i
il = InstallLocal(
files = {'/a/jw-base-run-1.0.0-42.deb': 'Package: jw-base\nVersion: 1.0.0-42'},
installed = {},
simulate = 'Inst jw-base [1.0.0-42]\n',
)
asyncio.run(install_packages(['/a/jw-base-run-1.0.0-42.deb'], ec = il, force = True))
assert dpkg_args(il.cmds[-1]) == ['/usr/bin/dpkg', '-i', '/a/jw-base-run-1.0.0-42.deb']
# -- A file that cannot be queried raises before the simulation
# or the install
il = InstallLocal(
files = {},
installed = {},
simulate = '',
)
try:
asyncio.run(install_packages(['/a/broken.deb'], ec = il, force = True))
except Exception as e:
assert 'package name and version' in str(e)
else:
raise AssertionError('expected an exception')
assert not [cmd for cmd in il.cmds if '-i' in cmd]
assert not [cmd for cmd in il.cmds if '/usr/bin/apt-get' in cmd]
# -- only_update raises on files whose package is not installed,
# before simulating or installing anything
il = InstallLocal(
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 = '',
)
try:
asyncio.run(
install_packages(
['/a/jw-base-run-1.0.0-42.deb', '/a/jw-foo-run-1.0.0-42.deb'],
ec = il,
force = True,
only_update = True,
)
)
except Exception as e:
assert 'only_update' in str(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 il.cmds if '-i' in cmd]
assert not [cmd for cmd in il.cmds if '/usr/bin/apt-get' in cmd]
# -- only_update installs an update of an installed package
il = InstallLocal(
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-41'},
simulate = 'Inst jw-base [1.0.0-42]\n',
)
asyncio.run(
install_packages(
['/a/jw-base-run-1.0.0-42.deb'], ec = il, force = True, only_update = True
)
)
assert dpkg_args(il.cmds[-1]) == ['/usr/bin/dpkg', '-i', '/a/jw-base-run-1.0.0-42.deb']
# -- force runs two installs for a mixed selection, the upgrades
# first
il = InstallLocal(
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',
)
asyncio.run(
install_packages(
['/a/jw-base-run-1.0.0-42.deb', '/a/jw-base-run-1.0.0-41.deb'],
ec = il,
force = True,
)
)
installs = [dpkg_args(cmd) for cmd in il.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',
],
]
# -- The simulation refuses the install when it would pull a
# dependency
il = InstallLocal(
files = {'/a/jw-base-run-1.0.0-42.deb': 'Package: jw-base\nVersion: 1.0.0-42'},
installed = {},
simulate = 'Inst jw-base [1.0.0-42]\nInst jw-foo [2.0]\n',
)
try:
asyncio.run(
install_packages(['/a/jw-base-run-1.0.0-42.deb'], ec = il, force = True)
)
except Exception as e:
assert 'would install' in str(e)
assert 'jw-foo' in str(e)
else:
raise AssertionError('expected an exception')
assert not [cmd for cmd in il.cmds if '-i' in cmd]
# -- The simulation refuses the install when it would remove a
# package
il = InstallLocal(
files = {'/a/jw-base-run-1.0.0-42.deb': 'Package: jw-base\nVersion: 1.0.0-42'},
installed = {},
simulate = 'Inst jw-base [1.0.0-42]\nRemv jw-old [1.0]\n',
)
try:
asyncio.run(
install_packages(['/a/jw-base-run-1.0.0-42.deb'], ec = il, force = True)
)
except Exception as e:
assert 'remove' in str(e)
assert 'jw-old' in str(e)
else:
raise AssertionError('expected an exception')
assert not [cmd for cmd in il.cmds if '-i' in cmd]
# -- A failed simulation, for instance unmet dependencies, aborts
# with its output
il = InstallLocal(
files = {'/a/jw-base-run-1.0.0-42.deb': 'Package: jw-base\nVersion: 1.0.0-42'},
installed = {},
simulate = (
'The following packages have unmet dependencies:\n'
' jw-base : Depends: jw-foo but it is not going to be installed\n'
),
simulate_status = 100,
)
try:
asyncio.run(
install_packages(['/a/jw-base-run-1.0.0-42.deb'], ec = il, force = True)
)
except Exception as e:
assert 'unmet dependencies' in str(e)
else:
raise AssertionError('expected an exception')
assert not [cmd for cmd in il.cmds if '-i' in cmd]

View file

@ -6,7 +6,7 @@ from typing import override
from jw.pkg.lib.ec.Local import Local
from jw.pkg.lib.FlaggedFile import FlaggedFile
from jw.pkg.lib.pm.rpm import _parse_verify_output, list_files
from jw.pkg.lib.pm.rpm import _parse_verify_output, install_packages, list_files
from jw.pkg.lib.Result import Result
# -- _parse_verify_output --
@ -101,3 +101,234 @@ assert cl.cmds[-1] == ['/usr/bin/rpm', '-qal']
cl = CannedLocal('')
assert asyncio.run(list_files([], ec = cl)) == []
assert cl.cmds == []
# -- install_packages --
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'):]
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)
# -- Without force, a file whose version is already installed is
# skipped, because plain -U refuses it
dl = 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'},
)
asyncio.run(install_packages(['/a/jw-base-run-1.0.0-42.x86_64.rpm'], ec = dl))
assert not [cmd for cmd in dl.cmds if '-U' in cmd]
# -- Without force, fresh and update files go through a single
# plain -U
dl = DispatchLocal(
files = {
'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42',
'/b/jw-foo-run-1.0.0-42.x86_64.rpm': 'jw-foo\t1.0.0-42',
},
installed = {'jw-base': 'jw-base|||||1.0.0-41\n'},
)
asyncio.run(
install_packages(
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/b/jw-foo-run-1.0.0-42.x86_64.rpm'],
ec = dl,
)
)
installs = [cmd for cmd in dl.cmds if '-U' in cmd]
assert len(installs) == 1
assert installs[0][0] == '/usr/bin/sudo'
assert rpm_args(installs[0]) == [
'/usr/bin/rpm',
'-U',
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
'/b/jw-foo-run-1.0.0-42.x86_64.rpm',
]
# -- An empty selection installs nothing and runs no command
dl = DispatchLocal(files = {}, installed = {})
asyncio.run(install_packages([], ec = dl))
assert dl.cmds == []
# -- With force, a file whose version is already installed is reinstalled
dl = 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'},
)
asyncio.run(
install_packages(['/a/jw-base-run-1.0.0-42.x86_64.rpm'], ec = dl, force = True)
)
assert rpm_args(dl.cmds[-1]) == [
'/usr/bin/rpm',
'-U',
'--reinstall',
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
]
# -- With force, a file with another version installed is a plain upgrade
dl = 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-41\n'},
)
asyncio.run(
install_packages(['/a/jw-base-run-1.0.0-42.x86_64.rpm'], ec = dl, force = True)
)
assert rpm_args(dl.cmds[-1]) == [
'/usr/bin/rpm',
'-U',
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
]
# -- With force, a file for a package that is not installed is a plain
# install
dl = DispatchLocal(
files = {'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42'},
installed = {},
)
asyncio.run(
install_packages(['/a/jw-base-run-1.0.0-42.x86_64.rpm'], ec = dl, force = True)
)
assert rpm_args(dl.cmds[-1]) == [
'/usr/bin/rpm',
'-U',
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
]
# -- An unqueryable file raises before anything is installed
dl = DispatchLocal(files = {}, installed = {})
try:
asyncio.run(
install_packages(['/a/jw-base-run-1.0.0-42.x86_64.rpm'], ec = dl, force = True)
)
except Exception:
pass
else:
raise AssertionError('expected an exception')
assert not [cmd for cmd in dl.cmds if '-U' in cmd]
# -- With force, a mixed transaction runs two installs, the upgrades first
dl = 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'},
)
asyncio.run(
install_packages(
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/a/jw-base-run-1.0.0-41.x86_64.rpm'],
ec = dl,
force = True,
)
)
installs = [rpm_args(cmd) for cmd in dl.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',
],
]
# -- With only_update, a file whose package is not installed raises
# before anything is installed
dl = 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'},
)
try:
asyncio.run(
install_packages(
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/a/jw-foo-run-1.0.0-42.x86_64.rpm'],
ec = dl,
force = True,
only_update = True,
)
)
except Exception as e:
assert 'only_update' in str(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 dl.cmds if '-U' in cmd]
# -- With only_update, an update of an installed package is installed
dl = DispatchLocal(
files = {'/a/jw-base-run-1.0.0-43.x86_64.rpm': 'jw-base\t1.0.0-43'},
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
)
asyncio.run(
install_packages(
['/a/jw-base-run-1.0.0-43.x86_64.rpm'],
ec = dl,
force = True,
only_update = True,
)
)
installs = [rpm_args(cmd) for cmd in dl.cmds if '-U' in cmd]
assert installs == [
['/usr/bin/rpm', '-U', '/a/jw-base-run-1.0.0-43.x86_64.rpm'],
]
# -- With only_update and force off, a file whose version is
# already installed is skipped
dl = 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'},
)
asyncio.run(
install_packages(
['/a/jw-base-run-1.0.0-42.x86_64.rpm'], ec = dl, only_update = True
)
)
assert not [cmd for cmd in dl.cmds if '-U' in cmd]
# -- With only_update, all files absent raises without installing
dl = DispatchLocal(
files = {'/a/jw-foo-run-1.0.0-42.x86_64.rpm': 'jw-foo\t1.0.0-42'},
installed = {},
)
try:
asyncio.run(
install_packages(
['/a/jw-foo-run-1.0.0-42.x86_64.rpm'], ec = dl, only_update = True
)
)
except Exception as e:
assert 'only_update' in str(e)
else:
raise AssertionError('expected an exception')
assert not [cmd for cmd in dl.cmds if '-U' in cmd]