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>
This commit is contained in:
parent
afbdc2e4b0
commit
36978c55e2
2 changed files with 659 additions and 2 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue