cmds.pkg.CmdVerify: Add command #142

Merged
Jan Lindemann merged 3 commits from jan/feature/20261016-cmds-pkg-cmdverify-add-command into master 2026-10-16 09:53:58 +02:00 AGit
5 changed files with 173 additions and 1 deletions
Showing only changes of commit 8106d4a7b0 - Show all commits

cmds.pkg.CmdVerify: Add command
All checks were successful
CI / Packaging - Kali Linux (pull_request) Successful in 4m24s
CI / Packaging - OpenSUSE Tumbleweed (pull_request) Successful in 4m43s
CI / Packaging test (pull_request) Successful in 0s
CI / Packaging - Kali Linux (push) Successful in 4m48s
CI / Packaging - OpenSUSE Tumbleweed (push) Successful in 4m38s
CI / Packaging test (push) Successful in 0s

Add the "packages verify" command: it runs Distro.verify() with the app's
package filter and prints one line per flagged file.

Signed-off-by: Jan Lindemann <jan@janware.com>
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v1.2.0
Jan Lindemann 2026-10-06 18:12:38 +02:00
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61

View file

@ -0,0 +1,29 @@
from __future__ import annotations
from typing import TYPE_CHECKING, override
from .Cmd import Cmd, Parent
if TYPE_CHECKING:
from argparse import ArgumentParser, Namespace
class CmdVerify(Cmd): # export
def __init__(self, parent: Parent) -> None:
super().__init__(
parent,
'verify',
help = (
'Verify the integrity of installed packages: report files'
' modified or deleted outside of the package manager'
)
)
@override
def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser)
@override
async def _run(self, args: Namespace) -> None:
for file in await self.distro.verify():
print(file)

View file

@ -66,6 +66,8 @@ Available subcommands of packages:
reboot-required Check whether the machine needs rebooting reboot-required Check whether the machine needs rebooting
refresh Refresh the distribution's notion of available packages refresh Refresh the distribution's notion of available packages
select Select packages by filter select Select packages by filter
verify Verify the integrity of installed packages: report files
modified or deleted outside of the package manager
============= Running: jw-pkg.py -t ../../../.. --log-level info packages delete --help ============= Running: jw-pkg.py -t ../../../.. --log-level info packages delete --help
usage: jw-pkg.py packages delete [-h] [names ...] usage: jw-pkg.py packages delete [-h] [names ...]
@ -140,6 +142,14 @@ Select packages by filter
positional arguments: positional arguments:
filter Package filter string filter Package filter string
options:
-h, --help show this help message and exit
============= Running: jw-pkg.py -t ../../../.. --log-level info packages verify --help
usage: jw-pkg.py packages verify [-h]
Verify the integrity of installed packages: report files modified or deleted
outside of the package manager
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
============= Running: jw-pkg.py -t ../../../.. --log-level info platform --help ============= Running: jw-pkg.py -t ../../../.. --log-level info platform --help

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,81 @@
from __future__ import annotations
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.Result import Result
class CannedLocal(Local):
def __init__(self, stdout: str) -> None:
super().__init__('local')
self.stdout = stdout
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)
return Result(stdout = self.stdout.encode(), status = 0)
# -- One listing over several packages
cl = CannedLocal('/usr/bin/b\n/etc/b/b.conf.jw-tmpl\n')
assert asyncio.run(list_files(['a', 'b'], ec = cl)) == \
['/usr/bin/b', '/etc/b/b.conf.jw-tmpl']
assert cl.cmds[-1] == [
'/usr/bin/dpkg',
'-L',
'a',
'b',
]
# -- The unfiltered selection is the standard interface, two
# queries: select the installed packages, then list their files
# in one command; the list output carries a package-name header
# and a blank line per package, both dropped
class CannedTwoStage(Local):
def __init__(self, query: str, listing: str) -> None:
super().__init__('local')
self.query = query
self.listing = listing
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)
out = self.query if '-W' in cmd else self.listing
return Result(stdout = out.encode(), status = 0)
cl2 = CannedTwoStage('a\nb\n', 'a:\n/usr/bin/a\n\nb:\n/etc/b/b.conf.jw-tmpl\n')
assert asyncio.run(list_files(None, ec = cl2)) == \
['/usr/bin/a', '/etc/b/b.conf.jw-tmpl']
assert cl2.cmds == [
['/usr/bin/dpkg-query', '-W', '-f=${?installed:binary:Package}\n'],
['/usr/bin/dpkg-query', '-L', 'a', 'b'],
]
# -- An empty selection lists nothing and runs no query
cl3 = CannedTwoStage('', '')
assert asyncio.run(list_files([], ec = cl3)) == []
assert cl3.cmds == []

View file

@ -1,7 +1,13 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from typing import override
from jw.pkg.lib.ec.Local import Local
from jw.pkg.lib.FlaggedFile import FlaggedFile from jw.pkg.lib.FlaggedFile import FlaggedFile
from jw.pkg.lib.pm.rpm import _parse_verify_output from jw.pkg.lib.pm.rpm import _parse_verify_output, list_files
from jw.pkg.lib.Result import Result
# -- _parse_verify_output -- # -- _parse_verify_output --
@ -56,3 +62,42 @@ f = FlaggedFile('pkg', '/etc/foo', 'SM5....T.')
assert repr(f) == 'pkg /etc/foo SM5....T.' assert repr(f) == 'pkg /etc/foo SM5....T.'
print('All rpm pm tests passed') print('All rpm pm tests passed')
# -- list_files --
class CannedLocal(Local):
def __init__(self, stdout: str) -> None:
super().__init__('local')
self.stdout = stdout
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)
return Result(stdout = self.stdout.encode(), status = 0)
# -- One query over several packages, the output split into lines
cl = CannedLocal('/usr/bin/a\n/etc/a/a.conf.jw-tmpl\n')
assert asyncio.run(list_files(['a', 'b'], ec = cl)) == \
['/usr/bin/a', '/etc/a/a.conf.jw-tmpl']
assert cl.cmds[-1] == ['/usr/bin/rpm', '-ql', 'a', 'b']
# -- The unfiltered selection is one query without package names
cl = CannedLocal('/usr/bin/a\n/etc/a/a.conf.jw-tmpl\n')
assert asyncio.run(list_files(None, ec = cl)) == ['/usr/bin/a', '/etc/a/a.conf.jw-tmpl']
assert cl.cmds[-1] == ['/usr/bin/rpm', '-qal']
# -- An empty selection lists nothing and runs no query
cl = CannedLocal('')
assert asyncio.run(list_files([], ec = cl)) == []
assert cl.cmds == []