feat(info): show the install reason and drop the indentation
N? and `info PKG` now print three lines, none indented:
PKG: <description>
reason: explicitly installed | installed as a dependency | unknown | not installed
required by: <installed dependents, comma separated> | nothing
The reason comes from a new <be>_reason backend function: apt-mark
showauto / showmanual (dpkg decides for a package in neither list), the
Install Reason of pacman -Qi, dnf repoquery --installed %{reason} (dnf4
and dnf5 words) and the Installed line of zypper info (rpm plus libzypp's
AutoInstalled when zypper is locked). A row whose old version is "-" is
not installed and the backend is not asked. The "(installed)" qualifier
moves to --help, README and DESIGN.
The rpm E2E fixture and e2e_reset now also mark su-dummy-a and su-dummy-c
as user-installed on dnf, which records plain rpm installs as "unknown",
so 1? and 2? show both reasons on all 12 distros. The verified commands
are recorded in discovery/{apt,pacman,rpm}.md.
This commit is contained in:
1 parent
1354db6c39
commit
71d7f87aa3
21 files changed
+589
-48
No files matched your search
@@ -41,7 +41,7 @@ commands:
|
|||||||
trust PKG... add PKG and its installed dependency closure to the statefile
|
trust PKG... add PKG and its installed dependency closure to the statefile
|
||||||
untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG)
|
untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG)
|
||||||
refresh recompute dependency closures for every 'user' root
|
refresh recompute dependency closures for every 'user' root
|
||||||
info PKG one-line description + installed reverse dependencies (as N?)
|
info PKG description, install reason and installed dependents (as N?)
|
||||||
show PKG full native package information (as N??)
|
show PKG full native package information (as N??)
|
||||||
|
|
||||||
options:
|
options:
|
||||||
@@ -80,7 +80,8 @@ Refreshing package lists... done.
|
|||||||
<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever
|
<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever
|
||||||
> 2?
|
> 2?
|
||||||
su-dummy-b: dummy package su-dummy-b for skeptical-updater tests
|
su-dummy-b: dummy package su-dummy-b for skeptical-updater tests
|
||||||
required by (installed): su-dummy-a
|
reason: installed as a dependency
|
||||||
|
required by: su-dummy-a
|
||||||
> 1+
|
> 1+
|
||||||
trusted su-dummy-a (+1 dependency: su-dummy-b)
|
trusted su-dummy-a (+1 dependency: su-dummy-b)
|
||||||
> 3-
|
> 3-
|
||||||
@@ -126,7 +127,7 @@ listed prints an error, runs nothing, and the prompt comes back.
|
|||||||
| token | action |
|
| token | action |
|
||||||
|-------|--------|
|
|-------|--------|
|
||||||
| empty line | proceed: run the native upgrade with the current exclusions |
|
| empty line | proceed: run the native upgrade with the current exclusions |
|
||||||
| `N?` | one-line description and the installed packages that depend on it |
|
| `N?` | what is it: the one-line description, `reason:` how it got installed (`explicitly installed`, `installed as a dependency`, `unknown` when the package manager cannot tell, `not installed` for a new dependency) and `required by:` the packages that depend on it, or `nothing`. That list covers installed packages only |
|
||||||
| `N??` | full native info (`apt-cache show`, `pacman -Qi` + `-Si`, `dnf info`, `zypper info --requires`) |
|
| `N??` | full native info (`apt-cache show`, `pacman -Qi` + `-Si`, `dnf info`, `zypper info --requires`) |
|
||||||
| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with a `-` marker |
|
| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with a `-` marker |
|
||||||
| `Nx` | purge now, with its now-unneeded dependencies: the purge is simulated first and the prompt lists everything it removes (`Purge su-dummy-a now? This also removes: su-dummy-b [y/N]`); the answer is read from the same stdin; the rows of every removed package leave the list. `x` refuses when other installed packages depend on the target, like `pacman -Rns`; purge the dependents first (`not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first`). Rows with old version `-` (new dependencies, not installed) cannot be purged |
|
| `Nx` | purge now, with its now-unneeded dependencies: the purge is simulated first and the prompt lists everything it removes (`Purge su-dummy-a now? This also removes: su-dummy-b [y/N]`); the answer is read from the same stdin; the rows of every removed package leave the list. `x` refuses when other installed packages depend on the target, like `pacman -Rns`; purge the dependents first (`not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first`). Rows with old version `-` (new dependencies, not installed) cannot be purged |
|
||||||
@@ -220,7 +221,11 @@ upgrade` (Inst lines + the "kept back" block), `pacman -Qu` joined with `pacman
|
|||||||
`zypper --xmlout list-updates`, or on Tumbleweed (as root) the real plan
|
`zypper --xmlout list-updates`, or on Tumbleweed (as root) the real plan
|
||||||
`zypper --xmlout --no-refresh dist-upgrade --dry-run`. Reverse dependencies and dependency closures use
|
`zypper --xmlout --no-refresh dist-upgrade --dry-run`. Reverse dependencies and dependency closures use
|
||||||
`apt-cache rdepends` / the dpkg database, `pacman -Qi`, and plain `rpm` queries on
|
`apt-cache rdepends` / the dpkg database, `pacman -Qi`, and plain `rpm` queries on
|
||||||
dnf and zypper systems (see the discovery reports for why).
|
dnf and zypper systems (see the discovery reports for why). The install reason `N?` shows comes
|
||||||
|
from `apt-mark showauto` / `showmanual`, the `Install Reason` of `pacman -Qi`,
|
||||||
|
`dnf repoquery --installed --queryformat '%{reason}'` (packages installed with plain `rpm` are
|
||||||
|
`unknown` to dnf4, user-installed to dnf5) and the `Installed` line of `zypper info`
|
||||||
|
(`Yes (automatically)`; libzypp's `/var/lib/zypp/AutoInstalled` when zypper is locked).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
@@ -250,7 +255,8 @@ in front of symlinks to the few system tools it needs. The fakes replay the real
|
|||||||
outputs recorded in `discovery/*.md` (data in `tests/unit/data/`), keep their state
|
outputs recorded in `discovery/*.md` (data in `tests/unit/data/`), keep their state
|
||||||
(holds, purged packages) under the test's temp dir and log every invocation, so the
|
(holds, purged packages) under the test's temp dir and log every invocation, so the
|
||||||
tests assert the exact commands issued. Covered: CLI parsing and exit codes, backend
|
tests assert the exact commands issued. Covered: CLI parsing and exit codes, backend
|
||||||
detection and override, root handling and the sudo re-exec, every parser, the
|
detection and override, root handling and the sudo re-exec, every parser (install reasons
|
||||||
|
included: explicitly installed, dependency, unknown and not installed on each backend), the
|
||||||
statefile, the whole interactive grammar, the per-backend exclusion mechanics (apt
|
statefile, the whole interactive grammar, the per-backend exclusion mechanics (apt
|
||||||
hold journal incl. crash recovery and SIGTERM, pacman `--ignore`, dnf `--exclude`,
|
hold journal incl. crash recovery and SIGTERM, pacman `--ignore`, dnf `--exclude`,
|
||||||
zypper private lock file contents incl. drop-ins), the purge plans, the metadata freeze flags,
|
zypper private lock file contents incl. drop-ins), the purge plans, the metadata freeze flags,
|
||||||
@@ -273,9 +279,12 @@ plus `su-dummy-orphan`, an unrelated automatically-installed package nothing dep
|
|||||||
with the distro's own repositories disabled (apt `SU_FIXTURE_OFFLINE=1`, pacman
|
with the distro's own repositories disabled (apt `SU_FIXTURE_OFFLINE=1`, pacman
|
||||||
`FIXTURE_ISOLATE=1`, rpm `SU_FIXTURE_ISOLATE=1` after the build tools are installed),
|
`FIXTURE_ISOLATE=1`, rpm `SU_FIXTURE_ISOLATE=1` after the build tools are installed),
|
||||||
so the review list is exactly the three dummies and the result does not depend on
|
so the review list is exactly the three dummies and the result does not depend on
|
||||||
mirror state. Then `tests/e2e/*.bats` exercise the real package manager: banner,
|
mirror state. `su-dummy-b` is recorded as installed as a dependency on every backend (pacman
|
||||||
list, `N?`, `N??`, `N+` and the statefile, `trust`/`untrust`/`refresh`, excluding
|
`--asdeps`, `apt-mark auto`, `dnf mark remove`, zypper's `AutoInstalled`) and `su-dummy-a` as
|
||||||
`su-dummy-b` (with and without a pre-existing hold/lock/IgnorePkg/exclude on
|
explicitly installed (dnf needs `dnf mark install`: it records plain `rpm` installs as
|
||||||
|
`unknown`). Then `tests/e2e/*.bats` exercise the real package manager: banner, list, `N?` (the
|
||||||
|
reason and `required by:` of both), `N??`, `N+` and the statefile, `trust`/`untrust`/`refresh`,
|
||||||
|
excluding `su-dummy-b` (with and without a pre-existing hold/lock/IgnorePkg/exclude on
|
||||||
`su-dummy-c`, which must survive byte for byte), trusting re-including an excluded
|
`su-dummy-c`, which must survive byte for byte), trusting re-including an excluded
|
||||||
dependency, full upgrade, `Nx` (refused for su-dummy-b, which su-dummy-a needs; su-dummy-a
|
dependency, full upgrade, `Nx` (refused for su-dummy-b, which su-dummy-a needs; su-dummy-a
|
||||||
taking its dependency su-dummy-b along, shown in the prompt, and the unrelated orphan surviving),
|
taking its dependency su-dummy-b along, shown in the prompt, and the unrelated orphan surviving),
|
||||||
|
|||||||
@@ -605,3 +605,20 @@ Container start overhead is ~0.2-0.3 s; `docker pull` of the five images took 3-
|
|||||||
Everything else compared with `diff` over a 385-line transcript of ~60 commands (update, list, simulate, hold, show/policy/dpkg -s, depends/rdepends, real upgrade, dist-upgrade, purge, autoremove, error paths): identical wording and exit codes across the five images apart from the rows above and checksums/sizes. Specifically identical: `apt list` stderr warning, `Inst`/`Conf`/`Remv`/`Purg` formats, `kept back` block, `apt-mark` messages, `apt-cache depends/rdepends` formats, `E:`/`W:` texts and exit codes (0 / 100), lock message, conffile failure, Essential protection message.
|
Everything else compared with `diff` over a 385-line transcript of ~60 commands (update, list, simulate, hold, show/policy/dpkg -s, depends/rdepends, real upgrade, dist-upgrade, purge, autoremove, error paths): identical wording and exit codes across the five images apart from the rows above and checksums/sizes. Specifically identical: `apt list` stderr warning, `Inst`/`Conf`/`Remv`/`Purg` formats, `kept back` block, `apt-mark` messages, `apt-cache depends/rdepends` formats, `E:`/`W:` texts and exit codes (0 / 100), lock message, conffile failure, Essential protection message.
|
||||||
|
|
||||||
Pre-existing docker images were not touched (`images-before.tsv` compared: nothing missing or changed); only `debian:12`, `debian:13`, `ubuntu:22.04`, `ubuntu:24.04`, `ubuntu:latest` were pulled and only `docker run --rm` containers were used (one hung throwaway container of mine, caused by a test that re-ran the fixture over a locally modified conffile, was `docker kill`ed).
|
Pre-existing docker images were not touched (`images-before.tsv` compared: nothing missing or changed); only `debian:12`, `debian:13`, `ubuntu:22.04`, `ubuntu:24.04`, `ubuntu:latest` were pulled and only `docker run --rm` containers were used (one hung throwaway container of mine, caused by a test that re-ran the fixture over a locally modified conffile, was `docker kill`ed).
|
||||||
|
|
||||||
|
## 13. Item 13 - install reason (`N?` / `info`): `apt-mark showauto` / `showmanual`
|
||||||
|
|
||||||
|
Verified 2026-10-10 on debian:12 (apt 2.6.1), debian:13 (3.0.3), ubuntu:22.04 (2.4.14), ubuntu:24.04 (2.8.3) and ubuntu:latest (26.04.1, apt 3.2.0), fixture `SU_FIXTURE_OFFLINE=1 SU_FIXTURE_ORPHAN=1 SU_FIXTURE_B_AUTO=1`, `LC_ALL=C`. Identical on all five:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ apt-mark showmanual su-dummy-a -> su-dummy-a (rc 0) $ apt-mark showauto su-dummy-a -> (empty, rc 0)
|
||||||
|
$ apt-mark showmanual su-dummy-b -> (empty, rc 0) $ apt-mark showauto su-dummy-b -> su-dummy-b (rc 0)
|
||||||
|
$ apt-mark showauto su-dummy-orphan -> su-dummy-orphan $ apt-mark showmanual no-such-pkg -> (empty, rc 0)
|
||||||
|
$ apt-mark showmanual su-dummy-a su-dummy-b su-dummy-c -> su-dummy-a (only the named packages)
|
||||||
|
```
|
||||||
|
|
||||||
|
* Chosen: `apt-mark showauto PKG` -> dependency; else `apt-mark showmanual PKG` -> explicitly installed; in neither list -> `dpkg-query -W -f '${db:Status-Abbrev}' PKG`: installed (2nd character neither `n` nor `c`) counts as manual, else not installed. No image had an installed package in neither list (installed = manual + auto exactly: debian:12 92 = 2 + 90, ubuntu:24.04 96 = 93 + 3, ...), so that branch is a safety net.
|
||||||
|
* Read-only: both work as `nobody` (`setpriv --reuid=65534`) and while another process holds dpkg's `lock-frontend` and `lock` (fcntl, like dpkg; checked with `apt-mark hold`, which fails then with `dpkg frontend lock was locked by another process with pid N`, exit 100): 0 s wait, same answers.
|
||||||
|
* A removed package with its configuration files kept (`rc`) is in neither list: not installed. A package installed with `dpkg -i` is manual (also `su-dummy-c` reinstalled with `dpkg -i` after `dpkg -r`).
|
||||||
|
* apt-mark prints a native package without its architecture qualifier (`apt-mark showauto libc6:amd64` -> `libc6`) and nothing for an arch that is not installed (`libc6:i386` -> empty); a name that is not a package is treated as a pattern (`apt-mark showmanual 'su-dummy-?'` and `'su-dummy-.'` -> `su-dummy-a`), exact names win (`libapt-pkg6.0`, `libbz2-1.0`, `libdb5.3` answered for themselves). Hence the tool matches the output line by line against `PKG` (or `PKG` without `:arch`).
|
||||||
|
* Fixture: `su-dummy-a`/`-c` are manual (`dpkg -i`), `su-dummy-b` is auto with `SU_FIXTURE_B_AUTO=1` (`apt-mark auto su-dummy-b`); no change needed.
|
||||||
@@ -568,3 +568,20 @@ pman bugs (verified; relevant if the tool reuses `/etc/pacman.d/ignorepkg`):
|
|||||||
`pacman.conf` actually includes the file in `[options]` (silently ineffective, section 6).
|
`pacman.conf` actually includes the file in `[options]` (silently ineffective, section 6).
|
||||||
* The tool should read the effective list with `pacman-conf IgnorePkg` and write with anchored matches
|
* The tool should read the effective list with `pacman-conf IgnorePkg` and write with anchored matches
|
||||||
(`grep -qxF "IgnorePkg = $pkg"`, `sed -i "/^IgnorePkg = ${pkg}\$/d"` after escaping).
|
(`grep -qxF "IgnorePkg = $pkg"`, `sed -i "/^IgnorePkg = ${pkg}\$/d"` after escaping).
|
||||||
|
|
||||||
|
## 13. Install reason (`N?` / `info`): `Install Reason` of `pacman -Qi`
|
||||||
|
|
||||||
|
Verified 2026-10-10 on archlinux:latest (pacman 7.1.0), fixture `FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 FIXTURE_SYNC=1`, `COLUMNS=0 LC_ALL=C pacman -Qi -- PKG`:
|
||||||
|
|
||||||
|
```
|
||||||
|
su-dummy-a Install Reason : Explicitly installed rc 0
|
||||||
|
su-dummy-b Install Reason : Installed as a dependency for another package rc 0 (-D --asdeps)
|
||||||
|
su-dummy-c Install Reason : Explicitly installed rc 0
|
||||||
|
su-dummy-orphan Install Reason : Installed as a dependency for another package rc 0 (-U --asdeps)
|
||||||
|
no-such-pkg error: package 'no-such-pkg' was not found rc 1 (also for a package only in a sync db)
|
||||||
|
```
|
||||||
|
|
||||||
|
* `pacman -D --asexplicit su-dummy-b` / `--asdeps` flip the line at once. Works as `nobody` and with `db.lck` present (-Qi takes no lock). One line, no wrapping with `COLUMNS=0`.
|
||||||
|
* `pacman -Qi sh` answers for the provider (`bash`), like the description and `Required By` already do.
|
||||||
|
* Mapping: `Explicitly installed` -> explicit, `Installed as a dependency for another package` -> dependency, -Qi failing -> not installed, anything else (or no such line) -> unknown.
|
||||||
|
* Fixture: `su-dummy-b` is installed `--asdeps`, `a`/`c` explicitly; `e2e_reset` re-applies both after its `pacman -U`. No change needed.
|
||||||
@@ -704,3 +704,42 @@ Inside `proof.sh` (warm containers): forced refresh (`su_refresh`) 0.5-2.6 s (up
|
|||||||
* zypper exit codes 100-103, 106, 107 were not reproduced (see 7.2). `opensuse-slowroll`, MicroOS/Aeon (transactional), SLES were not tested.
|
* zypper exit codes 100-103, 106, 107 were not reproduced (see 7.2). `opensuse-slowroll`, MicroOS/Aeon (transactional), SLES were not tested.
|
||||||
* Multi-arch (x86_64 + i686) and installonly kernels were not exercised; the parser keys on `name.arch` and keeps the highest installed `evr` per key for the "old" column.
|
* Multi-arch (x86_64 + i686) and installonly kernels were not exercised; the parser keys on `name.arch` and keeps the highest installed `evr` per key for the "old" column.
|
||||||
* Docker images pulled during this work (all kept, none removed or re-tagged): `almalinux:9`, `almalinux:10`, `rockylinux:9`, `rockylinux/rockylinux:9`, `rockylinux/rockylinux:10`, `quay.io/rockylinux/rockylinux:9`, `quay.io/rockylinux/rockylinux:10`, `opensuse/tumbleweed:latest`, `opensuse/leap:latest`, `fedora:latest`.
|
* Docker images pulled during this work (all kept, none removed or re-tagged): `almalinux:9`, `almalinux:10`, `rockylinux:9`, `rockylinux/rockylinux:9`, `rockylinux/rockylinux:10`, `quay.io/rockylinux/rockylinux:9`, `quay.io/rockylinux/rockylinux:10`, `opensuse/tumbleweed:latest`, `opensuse/leap:latest`, `fedora:latest`.
|
||||||
|
|
||||||
|
## 14. Install reason (`N?` / `info`)
|
||||||
|
|
||||||
|
Verified 2026-10-10, fixture `SU_FIXTURE_ISOLATE=1 SU_FIXTURE_ORPHAN=1`, `LC_ALL=C`.
|
||||||
|
|
||||||
|
### 14.1 DNF: `dnf -q repoquery --installed --queryformat '%{reason}\n' PKG`
|
||||||
|
|
||||||
|
Same answers on almalinux:9 and rockylinux/rockylinux:9 (dnf 4.14.0), almalinux:10 and rockylinux/rockylinux:10 (dnf 4.20.0); fedora:latest (Fedora 44, dnf5 5.4.3.0) uses other words:
|
||||||
|
|
||||||
|
| state | dnf4 | dnf5 |
|
||||||
|
|---|---|---|
|
||||||
|
| installed with plain `rpm -Uvh` (fixture's a, b, c) | `unknown` | `External User` |
|
||||||
|
| `su-dummy-orphan` (pulled in by `dnf install su-dummy-orphan-parent`) | `dependency` | `Dependency` |
|
||||||
|
| `dnf install su-dummy-a` pulling in `su-dummy-b` | a `user`, b `dependency` | a `User`, b `Dependency` |
|
||||||
|
| not installed (in a repo or unknown) | no output, rc 0 | no output, rc 0 |
|
||||||
|
|
||||||
|
* `%{reason}` exists on every target, `--qf` and `--queryformat` both work. dnf4 ends the value with a newline, dnf5 does not (several installed versions would run together), so the format carries its own `\n` and the first non-empty line is used. Other values in the code: `weak-dependency`/`Weak Dependency` (dependency), `group`/`Group` (explicit: asked for with its group); dnf5 `External User` counts as user-installed (`repoquery --userinstalled` lists those packages) -> explicit; dnf4 `unknown` -> unknown.
|
||||||
|
* Fallback `dnf history userinstalled` not needed and not used: dnf5 has no such command (`Unknown argument "userinstalled"`, rc 2), and on dnf4 it lists the fixture's rpm-installed a, b, c (reason `unknown`) as "Packages installed by user", i.e. it would call b explicit.
|
||||||
|
* Marks: dnf4 `dnf mark remove PKG` -> `dependency`, `dnf mark install PKG` -> `user` (`mark user`/`mark dependency`: invalid choice, rc 2); dnf5 `dnf -y mark dependency|user PKG` (without `-y`: "Operation aborted", rc 1; `mark remove|install`: unknown argument, rc 2). A mark survives a later `dnf upgrade` and an `rpm -Uvh --oldpackage --replacepkgs` back to 1.0 (the e2e reset).
|
||||||
|
* Works as `nobody` (setpriv / python3 on almalinux:10), with or without `-C`, in 110-150 ms (dnf4) / 45 ms (dnf5), and does not touch the repositories even with the distro repos re-enabled and their metadata expired.
|
||||||
|
* Fixture consequence: on dnf the rpm-installed a and c would print `reason: unknown`; `SU_FIXTURE_B_AUTO=1` now also runs `dnf mark install su-dummy-a su-dummy-c` (dnf5: `-y mark user`), and `e2e_reset` re-marks a/c user and b dependency.
|
||||||
|
|
||||||
|
### 14.2 ZYPPER: `Installed` line of `zypper --non-interactive --no-refresh --quiet info PKG`
|
||||||
|
|
||||||
|
opensuse/tumbleweed (zypper 1.14.101, libzypp 17.38.16) and opensuse/leap 16.0 (same versions), identical, fixture with `SU_FIXTURE_B_AUTO=1`:
|
||||||
|
|
||||||
|
```
|
||||||
|
su-dummy-a Installed : Yes rc 0
|
||||||
|
su-dummy-b Installed : Yes (automatically) rc 0 (listed in /var/lib/zypp/AutoInstalled)
|
||||||
|
su-dummy-orphan Installed : Yes (automatically) rc 0 (pulled in by zypper install su-dummy-orphan-parent)
|
||||||
|
su-dummy-orphan-parent Installed : No rc 0 (Status : not installed)
|
||||||
|
bash Installed : Yes rc 0 (Repository : @System, repos isolated)
|
||||||
|
no-such-pkg package 'no-such-pkg' not found. rc 104
|
||||||
|
```
|
||||||
|
|
||||||
|
* The block also has `Installed Size : 15 B`: match `^Installed *:` only.
|
||||||
|
* Cross-check: the flag is exactly membership in `/var/lib/zypp/AutoInstalled` (bare package names, plus `pattern:...` lines and `#` comments; 0644 root, world-readable). Deleting `su-dummy-b` from it turns the line into `Yes` immediately; an `rpm -Uvh --replacepkgs` reinstall keeps it; `zypper install su-dummy-a` (rpm -e'd first) installs b with `Yes (automatically)` and adds it to the file; naming b on the command line (`zypper update su-dummy-a su-dummy-b`) drops the flag (b becomes user-installed).
|
||||||
|
* Works as `nobody`. As root, while another process owns the zypp lock (`/run/zypp.pid` with a live pid), `zypper info` exits 7 ("System management is locked by the application with pid N (sleep)") - as `nobody` it still answers. Fallback in that case (and for exit 104): `rpm -q PKG` fails -> not installed; else membership in AutoInstalled -> dependency / explicit; unreadable -> unknown.
|
||||||
|
* Fixture: a/c (plain rpm, not in AutoInstalled) are `Yes`, b is `Yes (automatically)` with `SU_FIXTURE_B_AUTO=1`; `e2e_reset` re-adds b to AutoInstalled. No change needed.
|
||||||
+11
-1
@@ -98,7 +98,7 @@ Command grammar (one line, whitespace-separated tokens, each `N` is the number i
|
|||||||
| token | action |
|
| token | action |
|
||||||
|--------|---------------------------------------------------------------------------|
|
|--------|---------------------------------------------------------------------------|
|
||||||
| empty line | proceed: run the native upgrade with the current exclusions |
|
| empty line | proceed: run the native upgrade with the current exclusions |
|
||||||
| `N?` | print the one-line description and the list of installed packages that depend on it |
|
| `N?` | print three lines, none indented: `PKG: <one-line description>`, `reason: <r>` with `<r>` = `explicitly installed`, `installed as a dependency`, `unknown` (the backend cannot tell) or `not installed` (also, without asking the backend, for a row whose old version is `-`), and `required by: <a, b, ...>` - the reverse dependencies, installed packages only - or `required by: nothing` |
|
||||||
| `N??` | print the full native info dump (`apt-cache show` / `pacman -Qi` / `dnf info` / `zypper info --requires`) |
|
| `N??` | print the full native info dump (`apt-cache show` / `pacman -Qi` / `dnf info` / `zypper info --requires`) |
|
||||||
| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with `-` marker |
|
| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with `-` marker |
|
||||||
| `Nx` | purge it now, following pacman's `-Rns` principle on every backend: if any installed package still depends on it, refuse and print who (the reverse dependencies `N?` shows) - never cascade; otherwise simulate the native purge of it together with its now-unneeded dependencies, show that removal set ("This also removes: ...") and ask `y/N`. As a safety check, a simulated set containing anything that is neither the target nor in its installed dependency closure is refused too. Only a purge that ran removes rows (of every package it removed). A row whose old version is `-` (a new dependency, not installed) cannot be purged |
|
| `Nx` | purge it now, following pacman's `-Rns` principle on every backend: if any installed package still depends on it, refuse and print who (the reverse dependencies `N?` shows) - never cascade; otherwise simulate the native purge of it together with its now-unneeded dependencies, show that removal set ("This also removes: ...") and ask `y/N`. As a safety check, a simulated set containing anything that is neither the target nor in its installed dependency closure is refused too. Only a purge that ran removes rows (of every package it removed). A row whose old version is `-` (a new dependency, not installed) cannot be purged |
|
||||||
@@ -202,6 +202,7 @@ Every backend is a set of bash functions with a common prefix. All output is
|
|||||||
<be>_describe PKG # stdout: one-line summary
|
<be>_describe PKG # stdout: one-line summary
|
||||||
<be>_full_info PKG # stdout: native full dump (candidate version if available)
|
<be>_full_info PKG # stdout: native full dump (candidate version if available)
|
||||||
<be>_rdepends PKG # stdout: installed packages requiring PKG, one per line
|
<be>_rdepends PKG # stdout: installed packages requiring PKG, one per line
|
||||||
|
<be>_reason PKG # stdout: explicit | dependency | unknown | not-installed (how PKG got installed)
|
||||||
<be>_closure PKG # stdout: installed recursive dependency closure of PKG (excluding PKG), one per line
|
<be>_closure PKG # stdout: installed recursive dependency closure of PKG (excluding PKG), one per line
|
||||||
<be>_purge_plan PKG # stdout: every package a purge of PKG and its now-unneeded dependencies removes
|
<be>_purge_plan PKG # stdout: every package a purge of PKG and its now-unneeded dependencies removes
|
||||||
# (simulation, no cascade); non-zero = refused
|
# (simulation, no cascade); non-zero = refused
|
||||||
@@ -213,6 +214,15 @@ Every backend is a set of bash functions with a common prefix. All output is
|
|||||||
Mutating calls go through `run_cmd` which honours `--dry-run` (prints `+ cmd` to
|
Mutating calls go through `run_cmd` which honours `--dry-run` (prints `+ cmd` to
|
||||||
stdout instead of executing).
|
stdout instead of executing).
|
||||||
|
|
||||||
|
`<be>_reason` sources (read-only, unprivileged, verified in `discovery/*.md`): apt
|
||||||
|
`apt-mark showauto PKG` (dependency) / `apt-mark showmanual PKG` (explicit), a package in neither
|
||||||
|
list is explicit when dpkg has it installed; pacman the `Install Reason` of `pacman -Qi`; dnf
|
||||||
|
`repoquery --installed --queryformat '%{reason}'` (`user`/`group`/dnf5 `External User` = explicit,
|
||||||
|
`dependency`/`weak-dependency` = dependency, dnf4 `unknown` - plain `rpm -i` - stays unknown, no
|
||||||
|
output = not installed); zypper the `Installed` line of `zypper --no-refresh info`
|
||||||
|
(`Yes (automatically)` / `Yes` / `No`), and when zypper gives no answer (its lock is busy) `rpm -q`
|
||||||
|
plus libzypp's `/var/lib/zypp/AutoInstalled`.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+122
-11
@@ -87,7 +87,7 @@ commands:
|
|||||||
trust PKG... add PKG and its installed dependency closure to the statefile
|
trust PKG... add PKG and its installed dependency closure to the statefile
|
||||||
untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG)
|
untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG)
|
||||||
refresh recompute dependency closures for every 'user' root
|
refresh recompute dependency closures for every 'user' root
|
||||||
info PKG one-line description + installed reverse dependencies (as N?)
|
info PKG description, install reason and installed dependents (as N?)
|
||||||
show PKG full native package information (as N??)
|
show PKG full native package information (as N??)
|
||||||
|
|
||||||
options:
|
options:
|
||||||
@@ -102,7 +102,9 @@ options:
|
|||||||
|
|
||||||
interactive commands (one line, whitespace-separated tokens, N = number in brackets):
|
interactive commands (one line, whitespace-separated tokens, N = number in brackets):
|
||||||
<empty line> proceed: run the native upgrade with the current exclusions
|
<empty line> proceed: run the native upgrade with the current exclusions
|
||||||
N? one-line description and installed packages that depend on it
|
N? what is it: the description, 'reason:' (explicitly installed, installed as
|
||||||
|
a dependency, unknown or not installed) and 'required by:', the packages
|
||||||
|
that depend on it - installed packages only
|
||||||
N?? full native info dump
|
N?? full native info dump
|
||||||
N- exclude from this run (toggle); shown with a '-' marker
|
N- exclude from this run (toggle); shown with a '-' marker
|
||||||
Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused
|
Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused
|
||||||
@@ -122,7 +124,7 @@ EOF
|
|||||||
session_help() {
|
session_help() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
<enter> upgrade now, skipping the rows marked '-'
|
<enter> upgrade now, skipping the rows marked '-'
|
||||||
N? what is it: one-line description + installed packages that need it
|
N? what is it: description, install reason + installed packages that need it
|
||||||
N?? full native info
|
N?? full native info
|
||||||
N- exclude N from this run (toggle)
|
N- exclude N from this run (toggle)
|
||||||
Nx purge N now: lists every package the purge removes, then asks y/N.
|
Nx purge N now: lists every package the purge removes, then asks y/N.
|
||||||
@@ -883,6 +885,41 @@ apt_rdepends() {
|
|||||||
apt-cache rdepends --installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances "$1" </dev/null 2>/dev/null |
|
apt-cache rdepends --installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances "$1" </dev/null 2>/dev/null |
|
||||||
awk '/^Reverse Depends:/{p=1;next} p{sub(/^[ |]+/,""); print}' | sort -u
|
awk '/^Reverse Depends:/{p=1;next} p{sub(/^[ |]+/,""); print}' | sort -u
|
||||||
}
|
}
|
||||||
|
# apt_reason PKG: apt's auto flag (discovery/apt.md item 13): listed by `apt-mark showauto` =
|
||||||
|
# dependency, by `showmanual` = explicit; in neither list but installed (dpkg) = explicit, else not
|
||||||
|
# installed. Both are read-only: no root, no dpkg lock (verified). apt-mark prints native packages
|
||||||
|
# without their architecture qualifier and treats a name that is no package as a pattern, hence
|
||||||
|
# the exact-line match.
|
||||||
|
apt_reason() {
|
||||||
|
local out st
|
||||||
|
out=$(apt-mark showauto "$1" </dev/null 2>/dev/null) || {
|
||||||
|
printf 'unknown\n'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if apt_mark_lists "$1" "$out"; then
|
||||||
|
printf 'dependency\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
out=$(apt-mark showmanual "$1" </dev/null 2>/dev/null) || {
|
||||||
|
printf 'unknown\n'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if apt_mark_lists "$1" "$out"; then
|
||||||
|
printf 'explicit\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# ${db:Status-Abbrev}: 2nd character = state; n(ot-installed) and c(onfig-files) = not installed
|
||||||
|
st=$(dpkg-query -W -f '${db:Status-Abbrev}' "$1" </dev/null 2>/dev/null) || st=
|
||||||
|
case ${st:1:1} in
|
||||||
|
'' | n | c) printf 'not-installed\n' ;;
|
||||||
|
*) printf 'explicit\n' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
apt_mark_lists() { # apt_mark_lists PKG OUTPUT: does OUTPUT (apt-mark show*) name PKG (name:arch too)?
|
||||||
|
local o=$'\n'$2$'\n'
|
||||||
|
[[ $o == *$'\n'"$1"$'\n'* ]] && return 0
|
||||||
|
[[ $1 == *:* && $o == *$'\n'"${1%:*}"$'\n'* ]]
|
||||||
|
}
|
||||||
apt_closure() {
|
apt_closure() {
|
||||||
dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\t${Pre-Depends}\t${Depends}\t${Provides}\n' </dev/null 2>/dev/null |
|
dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\t${Pre-Depends}\t${Depends}\t${Provides}\n' </dev/null 2>/dev/null |
|
||||||
awk -F'\t' -v roots="$1" "$AWK_APT_CLOSURE" | awk -v r="$1" '$0 != r' | sort -u
|
awk -F'\t' -v roots="$1" "$AWK_APT_CLOSURE" | awk -v r="$1" '$0 != r' | sort -u
|
||||||
@@ -1197,6 +1234,22 @@ pacman_rdepends() {
|
|||||||
read -r -a words <<<"$out" || true
|
read -r -a words <<<"$out" || true
|
||||||
for w in ${words[@]+"${words[@]}"}; do [[ $w == None ]] || printf '%s\n' "$w"; done | sort -u
|
for w in ${words[@]+"${words[@]}"}; do [[ $w == None ]] || printf '%s\n' "$w"; done | sort -u
|
||||||
}
|
}
|
||||||
|
# pacman_reason PKG: the "Install Reason" of `pacman -Qi` (discovery/pacman.md section 13); -Qi
|
||||||
|
# fails (exit 1, "package 'x' was not found") for a package that is not installed
|
||||||
|
pacman_reason() {
|
||||||
|
local out rc=0
|
||||||
|
out=$(pm_field -Qi 'Install Reason' "$1") || rc=$?
|
||||||
|
out=${out%%$'\n'*}
|
||||||
|
if ((rc)) && [[ -z $out ]]; then
|
||||||
|
printf 'not-installed\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
case ${out#*$'\t'} in
|
||||||
|
'Explicitly installed') printf 'explicit\n' ;;
|
||||||
|
'Installed as a dependency for another package') printf 'dependency\n' ;;
|
||||||
|
*) printf 'unknown\n' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# discovery/pacman/lib.sh pm_closure (forward mode): one pacman -Qi dump + awk BFS, roots excluded
|
# discovery/pacman/lib.sh pm_closure (forward mode): one pacman -Qi dump + awk BFS, roots excluded
|
||||||
pacman_closure() {
|
pacman_closure() {
|
||||||
@@ -1565,6 +1618,25 @@ dnf_describe() {
|
|||||||
dnf_full_info() { "$DNF" -q info "$1" </dev/null; }
|
dnf_full_info() { "$DNF" -q info "$1" </dev/null; }
|
||||||
dnf_rdepends() { rpm_rdeps "$1"; }
|
dnf_rdepends() { rpm_rdeps "$1"; }
|
||||||
dnf_closure() { rpm_closure "$1"; }
|
dnf_closure() { rpm_closure "$1"; }
|
||||||
|
# dnf_reason PKG: dnf's own record, `repoquery --installed --queryformat %{reason}` (discovery/rpm.md
|
||||||
|
# section 14): dnf4 user/dependency/weak-dependency/group/unknown (unknown = installed with plain rpm),
|
||||||
|
# dnf5 User/Dependency/Weak Dependency/Group/External User (= installed with plain rpm, which dnf5
|
||||||
|
# counts as user-installed). A group member was asked for with its group: explicit. No output = not
|
||||||
|
# installed. Works unprivileged and never touches the repositories (verified).
|
||||||
|
dnf_reason() {
|
||||||
|
local out l=
|
||||||
|
out=$("$DNF" -q repoquery --installed --queryformat $'%{reason}\n' "$1" </dev/null 2>/dev/null) || {
|
||||||
|
printf 'unknown\n'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
while IFS= read -r l; do [[ -z $l ]] || break; done <<<"$out"
|
||||||
|
case $l in
|
||||||
|
user | User | group | Group | 'External User') printf 'explicit\n' ;;
|
||||||
|
dependency | Dependency | weak-dependency | 'Weak Dependency') printf 'dependency\n' ;;
|
||||||
|
'') printf 'not-installed\n' ;;
|
||||||
|
*) printf 'unknown\n' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
# dnf_purge_plan PKG: the "Removing..." sections of `dnf --assumeno remove PKG` (dnf4 and dnf5
|
# dnf_purge_plan PKG: the "Removing..." sections of `dnf --assumeno remove PKG` (dnf4 and dnf5
|
||||||
# tables; --assumeno always exits 1, so success = a "Transaction Summary" or "Nothing to do")
|
# tables; --assumeno always exits 1, so success = a "Transaction Summary" or "Nothing to do")
|
||||||
dnf_purge_plan() {
|
dnf_purge_plan() {
|
||||||
@@ -1723,6 +1795,35 @@ zypper_describe() {
|
|||||||
zypper_full_info() { zypper --non-interactive --quiet info --requires "$1" </dev/null; }
|
zypper_full_info() { zypper --non-interactive --quiet info --requires "$1" </dev/null; }
|
||||||
zypper_rdepends() { rpm_rdeps "$1"; }
|
zypper_rdepends() { rpm_rdeps "$1"; }
|
||||||
zypper_closure() { rpm_closure "$1"; }
|
zypper_closure() { rpm_closure "$1"; }
|
||||||
|
# zypper_reason PKG: the "Installed" line of `zypper info` - "Yes (automatically)", "Yes" or "No"
|
||||||
|
# (discovery/rpm.md section 14). Without an answer from zypper (exit 7: another zypper holds the
|
||||||
|
# lock, root only; exit 104: unknown name) rpm and libzypp's own AutoInstalled list decide.
|
||||||
|
# SKEPTICAL_UPDATER_ZYPP_ROOT is a path prefix for the test-suite.
|
||||||
|
zypper_reason() {
|
||||||
|
local out rc=0 l auto=${SKEPTICAL_UPDATER_ZYPP_ROOT:-}/var/lib/zypp/AutoInstalled
|
||||||
|
out=$(zypper --non-interactive --no-refresh --quiet info "$1" </dev/null 2>/dev/null) || rc=$?
|
||||||
|
if ((rc == 0)); then
|
||||||
|
while IFS= read -r l; do
|
||||||
|
[[ $l =~ ^Installed[[:space:]]*:[[:space:]]*(.*)$ ]] || continue # not "Installed Size :"
|
||||||
|
case ${BASH_REMATCH[1]} in
|
||||||
|
'Yes (automatically)') printf 'dependency\n' ;;
|
||||||
|
Yes) printf 'explicit\n' ;;
|
||||||
|
No) printf 'not-installed\n' ;;
|
||||||
|
*) printf 'unknown\n' ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
done <<<"$out"
|
||||||
|
fi
|
||||||
|
if ! rpm -q "$1" </dev/null >/dev/null 2>&1; then
|
||||||
|
printf 'not-installed\n'
|
||||||
|
elif [[ ! -r $auto ]]; then
|
||||||
|
printf 'unknown\n'
|
||||||
|
elif grep -qxF -e "$1" -- "$auto"; then
|
||||||
|
printf 'dependency\n'
|
||||||
|
else
|
||||||
|
printf 'explicit\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
zypper_purge_plan() {
|
zypper_purge_plan() {
|
||||||
local xml rc=0 sec name rest
|
local xml rc=0 sec name rest
|
||||||
xml=$(zypper --non-interactive --no-refresh --xmlout remove --dry-run --clean-deps "$1" </dev/null 2>&1) || rc=$?
|
xml=$(zypper --non-interactive --no-refresh --xmlout remove --dry-run --clean-deps "$1" </dev/null 2>&1) || rc=$?
|
||||||
@@ -1945,22 +2046,32 @@ print_list() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
print_info() { # print_info PKG: description + installed reverse dependencies (N? / info)
|
# print_info PKG [OLD]: description, install reason and the installed packages that depend on PKG
|
||||||
local desc rd rc=0
|
# (N? / info). OLD "-" (a row of a new dependency) is not installed: the backend is not asked.
|
||||||
|
print_info() {
|
||||||
|
local desc rd reason rc=0
|
||||||
desc=$(be describe "$1") || rc=$?
|
desc=$(be describe "$1") || rc=$?
|
||||||
if ((rc)); then
|
if ((rc)); then
|
||||||
say "$1: (no description available: unknown package?)"
|
say "$1: (no description available: unknown package?)"
|
||||||
else
|
else
|
||||||
say "$C_BOLD$1$C_RESET: $desc"
|
say "$C_BOLD$1$C_RESET: $desc"
|
||||||
fi
|
fi
|
||||||
|
if [[ ${2:-} == - ]]; then
|
||||||
|
reason=not-installed
|
||||||
|
else
|
||||||
|
reason=$(be reason "$1" 2>/dev/null) || reason=unknown
|
||||||
|
fi
|
||||||
|
case $reason in
|
||||||
|
explicit) say 'reason: explicitly installed' ;;
|
||||||
|
dependency) say 'reason: installed as a dependency' ;;
|
||||||
|
not-installed) say 'reason: not installed' ;;
|
||||||
|
*) say 'reason: unknown' ;;
|
||||||
|
esac
|
||||||
rd=$(be rdepends "$1") || rd=''
|
rd=$(be rdepends "$1") || rd=''
|
||||||
rd=$(printf '%s\n' "$rd" | sed '/^$/d' | tr '\n' ' ')
|
rd=$(printf '%s\n' "$rd" | sed '/^$/d' | tr '\n' ' ')
|
||||||
rd=${rd% }
|
rd=${rd% }
|
||||||
if [[ -n $rd ]]; then
|
rd=${rd// /, }
|
||||||
say " required by (installed): ${rd// /, }"
|
say "required by: ${rd:-nothing}"
|
||||||
else
|
|
||||||
say ' required by (installed): nothing'
|
|
||||||
fi
|
|
||||||
return "$rc"
|
return "$rc"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2248,7 +2359,7 @@ session_line() {
|
|||||||
fi
|
fi
|
||||||
i=$REPLY
|
i=$REPLY
|
||||||
case $op in
|
case $op in
|
||||||
'?') print_info "${R_NAME[i]}" || true ;;
|
'?') print_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;;
|
||||||
'??') print_full_info "${R_NAME[i]}" || true ;;
|
'??') print_full_info "${R_NAME[i]}" || true ;;
|
||||||
-)
|
-)
|
||||||
if ((R_EXCL[i])); then
|
if ((R_EXCL[i])); then
|
||||||
|
|||||||
@@ -31,7 +31,10 @@
|
|||||||
# so it is a pre-existing unneeded package (dnf repoquery --unneeded / zypper packages --unneeded).
|
# so it is a pre-existing unneeded package (dnf repoquery --unneeded / zypper packages --unneeded).
|
||||||
#
|
#
|
||||||
# Optional (SU_FIXTURE_B_AUTO=1): su-dummy-b counts as installed as a dependency (dnf: `dnf mark
|
# Optional (SU_FIXTURE_B_AUTO=1): su-dummy-b counts as installed as a dependency (dnf: `dnf mark
|
||||||
# remove`, zypper: /var/lib/zypp/AutoInstalled), like the pacman fixture's --asdeps.
|
# remove`, zypper: /var/lib/zypp/AutoInstalled), like the pacman fixture's --asdeps; on dnf
|
||||||
|
# su-dummy-a and su-dummy-c also count as installed by the user (`dnf mark install`): dnf4 reports
|
||||||
|
# packages installed with plain rpm as reason "unknown" (dnf5: "External User"). zypper needs
|
||||||
|
# nothing for them: a package that is not in AutoInstalled is user-installed.
|
||||||
#
|
#
|
||||||
# Re-runnable: re-running resets everything to the "1.0 installed, 2.0 available" state.
|
# Re-runnable: re-running resets everything to the "1.0 installed, 2.0 available" state.
|
||||||
#
|
#
|
||||||
@@ -198,7 +201,11 @@ fi
|
|||||||
if [ "${SU_FIXTURE_B_AUTO:-0}" = 1 ]; then
|
if [ "${SU_FIXTURE_B_AUTO:-0}" = 1 ]; then
|
||||||
case $BACKEND in
|
case $BACKEND in
|
||||||
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
|
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
|
||||||
dnf|yum) "$BACKEND" -q mark remove su-dummy-b >/dev/null ;;
|
dnf|yum)
|
||||||
|
# dnf4 spelling first, then dnf5's (which asks for confirmation)
|
||||||
|
"$BACKEND" -q mark remove su-dummy-b >/dev/null 2>&1 || "$BACKEND" -q -y mark dependency su-dummy-b >/dev/null
|
||||||
|
"$BACKEND" -q mark install su-dummy-a su-dummy-c >/dev/null 2>&1 || "$BACKEND" -q -y mark user su-dummy-a su-dummy-c >/dev/null
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
log "su-dummy-b marked as installed as a dependency"
|
log "su-dummy-b marked as installed as a dependency"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -132,10 +132,14 @@ e2e_reset() {
|
|||||||
rpm -Uvh --oldpackage --replacepkgs /var/tmp/su-fixture/RPMS/*/su-dummy-[abc]-1.0-1.*.rpm >/dev/null 2>&1
|
rpm -Uvh --oldpackage --replacepkgs /var/tmp/su-fixture/RPMS/*/su-dummy-[abc]-1.0-1.*.rpm >/dev/null 2>&1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
# su-dummy-b is a dependency of su-dummy-a (installed as such), on every backend
|
# su-dummy-b is a dependency of su-dummy-a (installed as such), on every backend; su-dummy-a and
|
||||||
|
# su-dummy-c were installed by the user (dnf records plain rpm installs as reason unknown)
|
||||||
case $BE in
|
case $BE in
|
||||||
apt) apt-mark auto su-dummy-orphan su-dummy-b >/dev/null ;;
|
apt) apt-mark auto su-dummy-orphan su-dummy-b >/dev/null ;;
|
||||||
dnf) dnf -q mark remove su-dummy-b >/dev/null 2>&1 ;;
|
dnf)
|
||||||
|
dnf -q mark remove su-dummy-b >/dev/null 2>&1 || dnf -q -y mark dependency su-dummy-b >/dev/null 2>&1
|
||||||
|
dnf -q mark install su-dummy-a su-dummy-c >/dev/null 2>&1 || dnf -q -y mark user su-dummy-a su-dummy-c >/dev/null 2>&1
|
||||||
|
;;
|
||||||
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
|
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
|
||||||
esac
|
esac
|
||||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" && $(pkg_ver su-dummy-orphan) == 1.0 ]] || {
|
[[ $(versions) == "a=1.0 b=1.0 c=1.0" && $(pkg_ver su-dummy-orphan) == 1.0 ]] || {
|
||||||
|
|||||||
@@ -32,14 +32,27 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl
|
|||||||
refute_line --regexp '^.\[4\]'
|
refute_line --regexp '^.\[4\]'
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "N? prints the description; su-dummy-b is required by su-dummy-a" {
|
@test "N? prints the description, the install reason and who requires it" {
|
||||||
|
local -a lines_=()
|
||||||
|
local i a=-1 b=-1
|
||||||
run skeptical-updater --no-refresh <<<$'1?\n2?\nq'
|
run skeptical-updater --no-refresh <<<$'1?\n2?\nq'
|
||||||
assert_success
|
assert_success
|
||||||
assert_line --regexp '^su-dummy-a: .*([Dd]ummy|skeptical-updater).*$'
|
assert_line --regexp '^su-dummy-a: .*([Dd]ummy|skeptical-updater).*$'
|
||||||
assert_line --regexp '^su-dummy-b: .*([Dd]ummy|skeptical-updater).*$'
|
assert_line --regexp '^su-dummy-b: .*([Dd]ummy|skeptical-updater).*$'
|
||||||
# [1] su-dummy-a has no installed reverse dependency, [2] su-dummy-b is needed by su-dummy-a
|
# [1] su-dummy-a was installed explicitly and nothing needs it; [2] su-dummy-b was installed as
|
||||||
assert_line ' required by (installed): nothing'
|
# a dependency of su-dummy-a - the two lines right under each description, no indentation
|
||||||
assert_line ' required by (installed): su-dummy-a'
|
mapfile -t lines_ <<<"$output"
|
||||||
|
for i in "${!lines_[@]}"; do
|
||||||
|
case ${lines_[i]} in
|
||||||
|
'su-dummy-a: '*) a=$i ;;
|
||||||
|
'su-dummy-b: '*) b=$i ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ ${lines_[a + 1]} == 'reason: explicitly installed' && ${lines_[a + 2]} == 'required by: nothing' ]] ||
|
||||||
|
fail "1? (su-dummy-a): got '${lines_[a + 1]}' / '${lines_[a + 2]}'"
|
||||||
|
[[ ${lines_[b + 1]} == 'reason: installed as a dependency' && ${lines_[b + 2]} == 'required by: su-dummy-a' ]] ||
|
||||||
|
fail "2? (su-dummy-b): got '${lines_[b + 1]}' / '${lines_[b + 2]}'"
|
||||||
|
refute_output --partial 'required by (installed)'
|
||||||
assert_line 'Quit: nothing was upgraded.'
|
assert_line 'Quit: nothing was upgraded.'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +263,11 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl
|
|||||||
assert_line --partial "$BE"$'\t'"su-dummy-c"$'\t'"user"
|
assert_line --partial "$BE"$'\t'"su-dummy-c"$'\t'"user"
|
||||||
run asuser skeptical-updater info su-dummy-b
|
run asuser skeptical-updater info su-dummy-b
|
||||||
assert_success
|
assert_success
|
||||||
assert_line ' required by (installed): su-dummy-a'
|
assert_line 'reason: installed as a dependency'
|
||||||
|
assert_line 'required by: su-dummy-a'
|
||||||
|
run asuser skeptical-updater info su-dummy-a
|
||||||
|
assert_success
|
||||||
|
assert_line 'reason: explicitly installed'
|
||||||
run asuser skeptical-updater --dry-run --no-refresh <<<''
|
run asuser skeptical-updater --dry-run --no-refresh <<<''
|
||||||
assert_success
|
assert_success
|
||||||
assert_line --regexp "$(native_upgrade_regex)"
|
assert_line --regexp "$(native_upgrade_regex)"
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
gcc-12-base
|
||||||
|
libc6
|
||||||
|
libgcc-s1
|
||||||
|
mawk
|
||||||
|
su-dummy-b
|
||||||
|
su-dummy-orphan
|
||||||
@@ -51,6 +51,13 @@ declare -A PROVIDES=(
|
|||||||
[ncurses-libs]=$'ncurses-libs\nlibtinfo.so.6()(64bit)'
|
[ncurses-libs]=$'ncurses-libs\nlibtinfo.so.6()(64bit)'
|
||||||
[redhat-rpm-config]=redhat-rpm-config [rpm]=rpm
|
[redhat-rpm-config]=redhat-rpm-config [rpm]=rpm
|
||||||
)
|
)
|
||||||
|
# what dnf4 records as the install reason (`repoquery --installed --qf %{reason}`; the fake dnf
|
||||||
|
# speaks dnf5's words with FAKE_DNF_MAJOR=5): plain `rpm -i` leaves "unknown"
|
||||||
|
declare -A REASON=(
|
||||||
|
[su-dummy-a]=user [su-dummy-b]=dependency [su-dummy-c]=unknown [su-dummy-d]=user
|
||||||
|
[bash]=user [glibc]=dependency [glibc-common]=dependency [glibc-gconv-extra]=weak-dependency
|
||||||
|
[basesystem]=group [setup]=dependency [filesystem]=dependency
|
||||||
|
)
|
||||||
# FAKE_RPM_NO_D=1: su-dummy-d (which requires every dummy) is not installed
|
# FAKE_RPM_NO_D=1: su-dummy-d (which requires every dummy) is not installed
|
||||||
declare -A FILES=(
|
declare -A FILES=(
|
||||||
[su-dummy-a]=/usr/share/su-dummy/su-dummy-a [su-dummy-b]=/usr/share/su-dummy/su-dummy-b
|
[su-dummy-a]=/usr/share/su-dummy/su-dummy-a [su-dummy-b]=/usr/share/su-dummy/su-dummy-b
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
|
||||||
|
Information for package su-dummy-b:
|
||||||
|
-----------------------------------
|
||||||
|
Repository : su-dummy
|
||||||
|
Name : su-dummy-b
|
||||||
|
Version : 2.0-1
|
||||||
|
Arch : noarch
|
||||||
|
Vendor :
|
||||||
|
Installed Size : 15 B
|
||||||
|
Installed : Yes (automatically)
|
||||||
|
Status : out-of-date (version 1.0-1 installed)
|
||||||
|
Source package : su-dummy-b-2.0-1.src
|
||||||
|
Summary : skeptical-updater dummy su-dummy-b (v2.0)
|
||||||
|
Description :
|
||||||
|
Dummy package su-dummy-b version 2.0 for skeptical-updater tests.
|
||||||
|
|
||||||
|
Second paragraph of su-dummy-b 2.0 (blank line above on purpose).
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
|
||||||
|
Information for package su-dummy-orphan-parent:
|
||||||
|
-----------------------------------------------
|
||||||
|
Repository : su-dummy
|
||||||
|
Name : su-dummy-orphan-parent
|
||||||
|
Version : 1.0-1
|
||||||
|
Arch : noarch
|
||||||
|
Vendor :
|
||||||
|
Installed Size : 27 B
|
||||||
|
Installed : No
|
||||||
|
Status : not installed
|
||||||
|
Source package : su-dummy-orphan-parent-1.0-1.src
|
||||||
|
Summary : skeptical-updater dummy su-dummy-orphan-parent (v1.0)
|
||||||
|
Description :
|
||||||
|
Dummy package su-dummy-orphan-parent version 1.0 for skeptical-updater tests.
|
||||||
|
|
||||||
|
Second paragraph of su-dummy-orphan-parent 1.0 (blank line above on purpose).
|
||||||
|
|
||||||
@@ -7,6 +7,10 @@
|
|||||||
# FAKE_APT_MARK_UNHOLD_SIGNAL=SIG: send SIG to the applet before an unhold (a second Ctrl-C).
|
# FAKE_APT_MARK_UNHOLD_SIGNAL=SIG: send SIG to the applet before an unhold (a second Ctrl-C).
|
||||||
# FAKE_APT_MARK_UNHOLD_RC=N: unhold fails (nothing released). FAKE_APT_MARK_FAIL_MSG: the message a
|
# FAKE_APT_MARK_UNHOLD_RC=N: unhold fails (nothing released). FAKE_APT_MARK_FAIL_MSG: the message a
|
||||||
# failing hold prints. Every unhold logs whether $STATE_DIR/holds.lock is held by someone.
|
# failing hold prints. Every unhold logs whether $STATE_DIR/holds.lock is held by someone.
|
||||||
|
# showauto/showmanual: installed = the "?i" rows of data/apt/dpkg-status.tsv (not purged), auto = those
|
||||||
|
# listed in data/apt/auto.txt, manual = the others; FAKE_APT_MARK_NEITHER="pkg ..." are in neither
|
||||||
|
# list, FAKE_APT_MARK_SHOW_RC=N makes both fail. Like the real ones they print only the named
|
||||||
|
# packages (all of them without a name), native packages without their ":arch" qualifier.
|
||||||
# shellcheck source=../fakelib.bash
|
# shellcheck source=../fakelib.bash
|
||||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||||
fake_log "$@"
|
fake_log "$@"
|
||||||
@@ -27,8 +31,30 @@ case $verb in hold | unhold)
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
# show_marked auto|manual NAME...
|
||||||
|
show_marked() {
|
||||||
|
local want=$1 n st name mark
|
||||||
|
shift
|
||||||
|
local -A pick=()
|
||||||
|
for n in "$@"; do pick[${n%:all}]=1; done
|
||||||
|
if [[ -n ${FAKE_APT_MARK_SHOW_RC:-} ]]; then
|
||||||
|
echo "E: Unable to determine the package states" >&2
|
||||||
|
exit "$FAKE_APT_MARK_SHOW_RC"
|
||||||
|
fi
|
||||||
|
while IFS=$'\t' read -r st name _; do
|
||||||
|
[[ $st == ?i* ]] || continue
|
||||||
|
fake_purged "$name" && continue
|
||||||
|
[[ " ${FAKE_APT_MARK_NEITHER:-} " == *" $name "* ]] && continue
|
||||||
|
if grep -qxF -- "$name" "$FAKE_DATA/apt/auto.txt"; then mark=auto; else mark=manual; fi
|
||||||
|
[[ $mark == "$want" ]] || continue
|
||||||
|
if (($# == 0)) || [[ -n ${pick[$name]:-} || -n ${pick[$name:amd64]:-} ]]; then printf '%s\n' "$name"; fi
|
||||||
|
done <"$FAKE_DATA/apt/dpkg-status.tsv" | sort
|
||||||
|
}
|
||||||
|
|
||||||
case $verb in
|
case $verb in
|
||||||
showhold) sort "$h" ;;
|
showhold) sort "$h" ;;
|
||||||
|
showauto) show_marked auto "$@" ;;
|
||||||
|
showmanual) show_marked manual "$@" ;;
|
||||||
hold)
|
hold)
|
||||||
for p in "$@"; do
|
for p in "$@"; do
|
||||||
if grep -qxF -- "$p" "$h"; then echo "$p was already set on hold."; else
|
if grep -qxF -- "$p" "$h"; then echo "$p was already set on hold."; else
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||||
|
|
||||||
# fake dpkg-query (unit tests): the dpkg database dump used for dependency closures
|
# fake dpkg-query (unit tests): the dpkg database dump used for dependency closures, and
|
||||||
|
# `-W -f '${db:Status-Abbrev}' NAME` (the status of one package; purged = unknown to dpkg)
|
||||||
# shellcheck source=../fakelib.bash
|
# shellcheck source=../fakelib.bash
|
||||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||||
fake_log "$@"
|
fake_log "$@"
|
||||||
@@ -10,7 +11,30 @@ fake_log "$@"
|
|||||||
st=${SKEPTICAL_UPDATER_STATE_DIR:-}/trusted.tsv
|
st=${SKEPTICAL_UPDATER_STATE_DIR:-}/trusted.tsv
|
||||||
if [[ -n ${FAKE_STATE_APPEND:-} ]]; then printf '%s\n' "$FAKE_STATE_APPEND" >>"$st"; fi
|
if [[ -n ${FAKE_STATE_APPEND:-} ]]; then printf '%s\n' "$FAKE_STATE_APPEND" >>"$st"; fi
|
||||||
if [[ -n ${FAKE_STATE_SED:-} ]]; then sed -i "$FAKE_STATE_SED" "$st"; fi
|
if [[ -n ${FAKE_STATE_SED:-} ]]; then sed -i "$FAKE_STATE_SED" "$st"; fi
|
||||||
case " $* " in
|
args=" $* "
|
||||||
|
names=()
|
||||||
|
while (($#)); do
|
||||||
|
case $1 in
|
||||||
|
-f) shift ;;
|
||||||
|
-*) ;;
|
||||||
|
*) names+=("$1") ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
if ((${#names[@]})); then
|
||||||
|
rc=0
|
||||||
|
for n in "${names[@]}"; do
|
||||||
|
st=$(awk -F'\t' -v n="${n%:amd64}" '$2 == n { print $1; exit }' "$FAKE_DATA/apt/dpkg-status.tsv")
|
||||||
|
if [[ -z $st ]] || fake_purged "${n%:amd64}"; then
|
||||||
|
echo "dpkg-query: no packages found matching $n" >&2
|
||||||
|
rc=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
printf '%s' "$st"
|
||||||
|
done
|
||||||
|
exit "$rc"
|
||||||
|
fi
|
||||||
|
case $args in
|
||||||
*' -W '*) cat "$FAKE_DATA/apt/dpkg-status.tsv" ;;
|
*' -W '*) cat "$FAKE_DATA/apt/dpkg-status.tsv" ;;
|
||||||
*)
|
*)
|
||||||
echo "fake dpkg-query: unhandled: $*" >&2
|
echo "fake dpkg-query: unhandled: $*" >&2
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
# fake dnf (unit tests). FAKE_DNF_MAJOR=4|5 picks the output dialect; FAKE_DNF_CHECK_RC=0|1 simulates
|
# fake dnf (unit tests). FAKE_DNF_MAJOR=4|5 picks the output dialect; FAKE_DNF_CHECK_RC=0|1 simulates
|
||||||
# "no updates" / an error; FAKE_DNF_CHECK=obsoletes replays a check-update with only an
|
# "no updates" / an error; FAKE_DNF_CHECK=obsoletes replays a check-update with only an
|
||||||
# "Obsoleting Packages" section; FAKE_DNF_REMOVE_REFUSE=1 makes `--assumeno remove` fail;
|
# "Obsoleting Packages" section; FAKE_DNF_REMOVE_REFUSE=1 makes `--assumeno remove` fail;
|
||||||
# removal tables: see the --assumeno remove branch.
|
# removal tables: see the --assumeno remove branch. `repoquery --installed --qf %{reason}`: REASON of
|
||||||
|
# data/rpm/db.bash (dnf5's words with FAKE_DNF_MAJOR=5), FAKE_DNF_REASON="pkg=raw value" overrides
|
||||||
|
# it, FAKE_DNF_REPOQUERY_RC=N makes repoquery fail.
|
||||||
# shellcheck source=../fakelib.bash
|
# shellcheck source=../fakelib.bash
|
||||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||||
# shellcheck source=../../data/rpm/db.bash
|
# shellcheck source=../../data/rpm/db.bash
|
||||||
@@ -41,7 +43,28 @@ case " $* " in
|
|||||||
;;
|
;;
|
||||||
*' repoquery '*)
|
*' repoquery '*)
|
||||||
p=${pos[-1]}
|
p=${pos[-1]}
|
||||||
|
rc=$(fake_rc FAKE_DNF_REPOQUERY_RC 0)
|
||||||
|
if ((rc)); then
|
||||||
|
echo "Error: rpmdb open failed" >&2
|
||||||
|
exit "$rc"
|
||||||
|
fi
|
||||||
case " $* " in
|
case " $* " in
|
||||||
|
*' --installed '*'%{reason}'*)
|
||||||
|
installed "$p" || exit 0
|
||||||
|
r=${REASON[$p]:-unknown}
|
||||||
|
if ((major == 5)); then
|
||||||
|
case $r in
|
||||||
|
user) r=User ;;
|
||||||
|
dependency) r=Dependency ;;
|
||||||
|
weak-dependency) r='Weak Dependency' ;;
|
||||||
|
group) r=Group ;;
|
||||||
|
unknown) r='External User' ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
[[ ${FAKE_DNF_REASON:-} != "$p="* ]] || r=${FAKE_DNF_REASON#*=}
|
||||||
|
printf '%s\n' "$r"
|
||||||
|
((major == 5)) || echo
|
||||||
|
;;
|
||||||
*' --upgrades '*) case $p in su-dummy-[abc]) printf 'skeptical-updater dummy %s (v2.0)\n' "$p"; ((major == 5)) || echo ;; esac ;;
|
*' --upgrades '*) case $p in su-dummy-[abc]) printf 'skeptical-updater dummy %s (v2.0)\n' "$p"; ((major == 5)) || echo ;; esac ;;
|
||||||
*) case $p in su-dummy-new) printf 'skeptical-updater dummy su-dummy-new (v1.0)\n' ;; esac ;;
|
*) case $p in su-dummy-new) printf 'skeptical-updater dummy su-dummy-new (v1.0)\n' ;; esac ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
# FAKE_PACMAN_STDERR=TEXT: -Sl and -Qu also print TEXT on stderr (an unreadable database).
|
# FAKE_PACMAN_STDERR=TEXT: -Sl and -Qu also print TEXT on stderr (an unreadable database).
|
||||||
# -Rs --print: FAKE_PACMAN_RS_REFUSE=1 makes pacman refuse ("breaks dependency");
|
# -Rs --print: FAKE_PACMAN_RS_REFUSE=1 makes pacman refuse ("breaks dependency");
|
||||||
# FAKE_PACMAN_RS_EXTRA="pkg:other" adds other to the plan of pkg.
|
# FAKE_PACMAN_RS_EXTRA="pkg:other" adds other to the plan of pkg.
|
||||||
|
# FAKE_PACMAN_REASON="pkg=text" replaces the "Install Reason" of pkg in -Qi (empty text: no such line).
|
||||||
# FAKE_PACMAN_QU=fixture|real|none selects the -Qu sample,
|
# FAKE_PACMAN_QU=fixture|real|none selects the -Qu sample,
|
||||||
# FAKE_PACMAN_SL=empty makes -Sl print nothing, FAKE_PACMAN_SUP_EXTRA="a b" adds packages that
|
# FAKE_PACMAN_SL=empty makes -Sl print nothing, FAKE_PACMAN_SUP_EXTRA="a b" adds packages that
|
||||||
# only -Sup (the real transaction) shows. Every -Q*/-S[il] call records its COLUMNS value, every
|
# only -Sup (the real transaction) shows. Every -Q*/-S[il] call records its COLUMNS value, every
|
||||||
@@ -107,7 +108,19 @@ case $op in
|
|||||||
[[ -z ${FAKE_PACMAN_STDERR:-} ]] || echo "$FAKE_PACMAN_STDERR" >&2
|
[[ -z ${FAKE_PACMAN_STDERR:-} ]] || echo "$FAKE_PACMAN_STDERR" >&2
|
||||||
[[ ${FAKE_PACMAN_SL:-} == empty ]] || cat "$D/Sl.txt"
|
[[ ${FAKE_PACMAN_SL:-} == empty ]] || cat "$D/Sl.txt"
|
||||||
;;
|
;;
|
||||||
-Qi) blocks "$D/Qi.txt" "${targets[@]}" ;;
|
-Qi)
|
||||||
|
if [[ -z ${FAKE_PACMAN_REASON:-} ]]; then
|
||||||
|
blocks "$D/Qi.txt" "${targets[@]}"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
rc=0
|
||||||
|
out=$(blocks "$D/Qi.txt" "${targets[@]}") || rc=$?
|
||||||
|
awk -v n="${FAKE_PACMAN_REASON%%=*}" -v r="${FAKE_PACMAN_REASON#*=}" '
|
||||||
|
/^Name +: / { cur = $0; sub(/^Name +: /, "", cur) }
|
||||||
|
/^Install Reason +: / && cur == n { if (r != "") print "Install Reason : " r; next }
|
||||||
|
{ print }' <<<"$out"
|
||||||
|
exit "$rc"
|
||||||
|
;;
|
||||||
-Si)
|
-Si)
|
||||||
t=()
|
t=()
|
||||||
for a in "${targets[@]}"; do t+=("${a#*/}"); done
|
for a in "${targets[@]}"; do t+=("${a#*/}"); done
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
# FAKE_ZYPPER_DUP=fixture|empty|removes the `dist-upgrade --dry-run` plan (data/zypper/dup-*.xml).
|
# FAKE_ZYPPER_DUP=fixture|empty|removes the `dist-upgrade --dry-run` plan (data/zypper/dup-*.xml).
|
||||||
# addlock writes real zypp lock entries into the locksfile.path of $ZYPP_CONF (default
|
# addlock writes real zypp lock entries into the locksfile.path of $ZYPP_CONF (default
|
||||||
# /etc/zypp/locks is never touched: the fake refuses); update/dist-upgrade log that file.
|
# /etc/zypp/locks is never touched: the fake refuses); update/dist-upgrade log that file.
|
||||||
|
# info replays data/zypper/info-NAME.txt (exit 104 without one); FAKE_ZYPPER_INFO_RC=7 makes it fail
|
||||||
|
# like a zypp lock held by another process.
|
||||||
# shellcheck source=../fakelib.bash
|
# shellcheck source=../fakelib.bash
|
||||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||||
fake_log "${ZYPP_CONF:+ZYPP_CONF=$ZYPP_CONF }$*"
|
fake_log "${ZYPP_CONF:+ZYPP_CONF=$ZYPP_CONF }$*"
|
||||||
@@ -43,6 +45,11 @@ refresh)
|
|||||||
list-updates) cat "$D/lu-${FAKE_ZYPPER_LU:-fixture}.xml" ;;
|
list-updates) cat "$D/lu-${FAKE_ZYPPER_LU:-fixture}.xml" ;;
|
||||||
info)
|
info)
|
||||||
p=${args[-1]}
|
p=${args[-1]}
|
||||||
|
if [[ -n ${FAKE_ZYPPER_INFO_RC:-} ]]; then
|
||||||
|
echo "System management is locked by the application with pid 4242 (zypper)."
|
||||||
|
echo "Close this application before trying again."
|
||||||
|
exit "$FAKE_ZYPPER_INFO_RC"
|
||||||
|
fi
|
||||||
if [[ -f $D/info-$p.txt ]]; then cat "$D/info-$p.txt"; else
|
if [[ -f $D/info-$p.txt ]]; then cat "$D/info-$p.txt"; else
|
||||||
echo "package '$p' not found."
|
echo "package '$p' not found."
|
||||||
exit 104
|
exit 104
|
||||||
|
|||||||
@@ -64,11 +64,30 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc
|
|||||||
assert_line 'excluded from this run: su-dummy-b'
|
assert_line 'excluded from this run: su-dummy-b'
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "N? prints the description and the installed packages that depend on it" {
|
@test "N? prints the description, the install reason and the installed packages that depend on it" {
|
||||||
run skup --no-refresh <<<$'2?\nq'
|
run skup --no-refresh <<<$'1?\n2?\nq'
|
||||||
assert_success
|
assert_success
|
||||||
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
|
assert_output --partial "$(printf '%s\n' '> 1?' \
|
||||||
assert_line ' required by (installed): su-dummy-a'
|
'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests' \
|
||||||
|
'reason: explicitly installed' \
|
||||||
|
'required by: nothing' \
|
||||||
|
'> 2?' \
|
||||||
|
'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests' \
|
||||||
|
'reason: installed as a dependency' \
|
||||||
|
'required by: su-dummy-a')"
|
||||||
|
refute_output --partial 'required by (installed)'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "N? on a new dependency (old version -) says 'not installed' without asking the package manager" {
|
||||||
|
FAKE_APT_SIM=real run skup --no-refresh <<<$'9?\nq'
|
||||||
|
assert_success
|
||||||
|
assert_line --regexp '^ \[9\] su-dummy-newdep +- +-> +1\.0 +su-fixture:su-repo$'
|
||||||
|
assert_output --partial "$(printf '%s\n' '> 9?' \
|
||||||
|
'su-dummy-newdep: (no description available: unknown package?)' \
|
||||||
|
'reason: not installed' \
|
||||||
|
'required by: nothing')"
|
||||||
|
log_lacks '^apt-mark show'
|
||||||
|
log_lacks '^dpkg-query -W -f \$\{db:Status-Abbrev\} '
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "N?? prints the native full info" {
|
@test "N?? prints the native full info" {
|
||||||
|
|||||||
+156
-10
@@ -50,13 +50,43 @@ setup() { common_setup; }
|
|||||||
@test "apt: description = candidate Description; reverse deps deduplicated, '|' stripped" {
|
@test "apt: description = candidate Description; reverse deps deduplicated, '|' stripped" {
|
||||||
run skup info su-dummy-b
|
run skup info su-dummy-b
|
||||||
assert_success
|
assert_success
|
||||||
assert_output "$(printf '%s\n' 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests' ' required by (installed): su-dummy-a')"
|
assert_output "$(printf '%s\n' 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests' 'reason: installed as a dependency' 'required by: su-dummy-a')"
|
||||||
log_has 'apt-cache show --no-all-versions su-dummy-b'
|
log_has 'apt-cache show --no-all-versions su-dummy-b'
|
||||||
log_has 'apt-cache rdepends --installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances su-dummy-b'
|
log_has 'apt-cache rdepends --installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances su-dummy-b'
|
||||||
run skup info gpgv
|
run skup info gpgv
|
||||||
assert_success
|
assert_success
|
||||||
assert_line 'gpgv: GNU privacy guard - signature verification tool'
|
assert_line 'gpgv: GNU privacy guard - signature verification tool'
|
||||||
assert_line ' required by (installed): apt'
|
assert_line 'required by: apt'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "apt: install reason from apt-mark showauto / showmanual, dpkg for a package in neither list" {
|
||||||
|
run skup info su-dummy-a
|
||||||
|
assert_success
|
||||||
|
assert_output "$(printf '%s\n' 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests' 'reason: explicitly installed' 'required by: nothing')"
|
||||||
|
log_has 'apt-mark showauto su-dummy-a'
|
||||||
|
log_has 'apt-mark showmanual su-dummy-a'
|
||||||
|
log_lacks '^dpkg-query -W -f \$\{db:Status-Abbrev\} '
|
||||||
|
run skup info su-dummy-b
|
||||||
|
assert_line 'reason: installed as a dependency'
|
||||||
|
# apt-mark prints a native package without the ":arch" it was asked for
|
||||||
|
run skup info libc6:amd64
|
||||||
|
assert_line 'reason: installed as a dependency'
|
||||||
|
# installed but in neither list: manual
|
||||||
|
FAKE_APT_MARK_NEITHER=su-dummy-c run skup info su-dummy-c
|
||||||
|
assert_success
|
||||||
|
assert_line 'reason: explicitly installed'
|
||||||
|
# shellcheck disable=SC2016 # dpkg-query's own ${...} format, literally
|
||||||
|
log_has 'dpkg-query -W -f ${db:Status-Abbrev} su-dummy-c'
|
||||||
|
# removed with its configuration files kept (rc), or unknown to dpkg: not installed
|
||||||
|
run skup info su-dummy-f
|
||||||
|
assert_line 'reason: not installed'
|
||||||
|
run skup info gpgv
|
||||||
|
assert_success
|
||||||
|
assert_line 'reason: not installed'
|
||||||
|
# apt-mark cannot tell
|
||||||
|
FAKE_APT_MARK_SHOW_RC=100 run skup info su-dummy-a
|
||||||
|
assert_success
|
||||||
|
assert_line 'reason: unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "apt: the description also comes from Description-<lang> fields (Translation files)" {
|
@test "apt: the description also comes from Description-<lang> fields (Translation files)" {
|
||||||
@@ -141,7 +171,7 @@ setup() { common_setup; }
|
|||||||
run skup info su-dummy-a
|
run skup info su-dummy-a
|
||||||
assert_success
|
assert_success
|
||||||
assert_line 'su-dummy-a: Skeptical-updater dummy package A'
|
assert_line 'su-dummy-a: Skeptical-updater dummy package A'
|
||||||
assert_line ' required by (installed): nothing'
|
assert_line 'required by: nothing'
|
||||||
run skup info su-dummy-new
|
run skup info su-dummy-new
|
||||||
assert_success
|
assert_success
|
||||||
assert_line 'su-dummy-new: Skeptical-updater dummy package NEW'
|
assert_line 'su-dummy-new: Skeptical-updater dummy package NEW'
|
||||||
@@ -152,9 +182,32 @@ setup() { common_setup; }
|
|||||||
run skup info glibc
|
run skup info glibc
|
||||||
assert_success
|
assert_success
|
||||||
assert_line 'glibc: GNU C Library'
|
assert_line 'glibc: GNU C Library'
|
||||||
assert_line ' required by (installed): bash, gcc-libs, ncurses, readline'
|
assert_line 'required by: bash, gcc-libs, ncurses, readline'
|
||||||
run skup info su-dummy-b
|
run skup info su-dummy-b
|
||||||
assert_line ' required by (installed): su-dummy-a'
|
assert_line 'required by: su-dummy-a'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "pacman: install reason from the 'Install Reason' of -Qi; -Qi failing = not installed" {
|
||||||
|
use_fakes pacman
|
||||||
|
run skup info su-dummy-a
|
||||||
|
assert_success
|
||||||
|
assert_output "$(printf '%s\n' 'su-dummy-a: Skeptical-updater dummy package A' 'reason: explicitly installed' 'required by: nothing')"
|
||||||
|
run skup info su-dummy-b
|
||||||
|
assert_success
|
||||||
|
assert_output "$(printf '%s\n' 'su-dummy-b: Skeptical-updater dummy package B (dependency of A)' 'reason: installed as a dependency' 'required by: su-dummy-a')"
|
||||||
|
run skup info glibc
|
||||||
|
assert_line 'reason: installed as a dependency'
|
||||||
|
# in a sync database only
|
||||||
|
run skup info su-dummy-new
|
||||||
|
assert_success
|
||||||
|
assert_line 'reason: not installed'
|
||||||
|
# a value this tool does not know, or no such field
|
||||||
|
FAKE_PACMAN_REASON='su-dummy-c=Installed for a reason pacman 9 invented' run skup info su-dummy-c
|
||||||
|
assert_success
|
||||||
|
assert_line 'reason: unknown'
|
||||||
|
FAKE_PACMAN_REASON='su-dummy-c=' run skup info su-dummy-c
|
||||||
|
assert_line 'reason: unknown'
|
||||||
|
log_has 'pacman -Qi -- su-dummy-c'
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "pacman: the closure resolves provides (sonames, sh) and version constraints" {
|
@test "pacman: the closure resolves provides (sonames, sh) and version constraints" {
|
||||||
@@ -249,7 +302,7 @@ setup() { common_setup; }
|
|||||||
run skup info su-dummy-d
|
run skup info su-dummy-d
|
||||||
assert_success
|
assert_success
|
||||||
assert_line 'su-dummy-d: skeptical-updater dummy su-dummy-d (v1.0)'
|
assert_line 'su-dummy-d: skeptical-updater dummy su-dummy-d (v1.0)'
|
||||||
assert_line ' required by (installed): nothing'
|
assert_line 'required by: nothing'
|
||||||
FAKE_DNF_MAJOR=5 run skup info su-dummy-a
|
FAKE_DNF_MAJOR=5 run skup info su-dummy-a
|
||||||
assert_line 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)'
|
assert_line 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)'
|
||||||
}
|
}
|
||||||
@@ -257,11 +310,55 @@ setup() { common_setup; }
|
|||||||
@test "rpm reverse deps: provides + owned files joined with every requirement (rich deps too)" {
|
@test "rpm reverse deps: provides + owned files joined with every requirement (rich deps too)" {
|
||||||
use_fakes dnf rpm
|
use_fakes dnf rpm
|
||||||
run skup info su-dummy-b
|
run skup info su-dummy-b
|
||||||
assert_line ' required by (installed): su-dummy-a, su-dummy-d'
|
assert_line 'required by: su-dummy-a, su-dummy-d'
|
||||||
run skup info su-dummy-c
|
run skup info su-dummy-c
|
||||||
assert_line ' required by (installed): su-dummy-d'
|
assert_line 'required by: su-dummy-d'
|
||||||
run skup info bash
|
run skup info bash
|
||||||
assert_line ' required by (installed): filesystem, glibc-common, redhat-rpm-config, rpm, su-dummy-d'
|
assert_line 'required by: filesystem, glibc-common, redhat-rpm-config, rpm, su-dummy-d'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "dnf4: install reason from repoquery --installed %{reason}; plain rpm -i = unknown" {
|
||||||
|
use_fakes dnf rpm
|
||||||
|
run skup info su-dummy-a
|
||||||
|
assert_success
|
||||||
|
assert_output "$(printf '%s\n' 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)' 'reason: explicitly installed' 'required by: su-dummy-d')"
|
||||||
|
log_has 'dnf -q repoquery --installed --queryformat %{reason}'
|
||||||
|
run skup info su-dummy-b
|
||||||
|
assert_line 'reason: installed as a dependency'
|
||||||
|
run skup info su-dummy-c
|
||||||
|
assert_line 'reason: unknown'
|
||||||
|
# weak-dependency counts as a dependency, a group member as asked for
|
||||||
|
run skup info glibc-gconv-extra
|
||||||
|
assert_line 'reason: installed as a dependency'
|
||||||
|
run skup info basesystem
|
||||||
|
assert_line 'reason: explicitly installed'
|
||||||
|
# only in a repository: repoquery --installed prints nothing
|
||||||
|
run skup info su-dummy-new
|
||||||
|
assert_success
|
||||||
|
assert_line 'su-dummy-new: skeptical-updater dummy su-dummy-new (v1.0)'
|
||||||
|
assert_line 'reason: not installed'
|
||||||
|
# repoquery failing: unknown
|
||||||
|
FAKE_DNF_REPOQUERY_RC=1 run skup info su-dummy-a
|
||||||
|
assert_success
|
||||||
|
assert_line 'reason: unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "dnf5: install reason in dnf5's words (External User = plain rpm -i, counted as user-installed)" {
|
||||||
|
use_fakes dnf rpm
|
||||||
|
export FAKE_DNF_MAJOR=5
|
||||||
|
run skup info su-dummy-a
|
||||||
|
assert_success
|
||||||
|
assert_line 'reason: explicitly installed'
|
||||||
|
run skup info su-dummy-b
|
||||||
|
assert_line 'reason: installed as a dependency'
|
||||||
|
run skup info su-dummy-c
|
||||||
|
assert_line 'reason: explicitly installed'
|
||||||
|
run skup info glibc-gconv-extra
|
||||||
|
assert_line 'reason: installed as a dependency'
|
||||||
|
FAKE_DNF_REASON='su-dummy-c=None' run skup info su-dummy-c
|
||||||
|
assert_line 'reason: unknown'
|
||||||
|
run skup info su-dummy-new
|
||||||
|
assert_line 'reason: not installed'
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "rpm closure: file deps, rich deps without their condition, rpmlib() dropped" {
|
@test "rpm closure: file deps, rich deps without their condition, rpmlib() dropped" {
|
||||||
@@ -393,9 +490,58 @@ setup() { common_setup; }
|
|||||||
@test "zypper: reverse deps and closure use the same pure-rpm code" {
|
@test "zypper: reverse deps and closure use the same pure-rpm code" {
|
||||||
use_fakes zypper rpm
|
use_fakes zypper rpm
|
||||||
run skup info su-dummy-c
|
run skup info su-dummy-c
|
||||||
assert_line ' required by (installed): su-dummy-d'
|
assert_line 'required by: su-dummy-d'
|
||||||
run skup trust su-dummy-a
|
run skup trust su-dummy-a
|
||||||
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||||
run cut -f1 "$STATE_FILE"
|
run cut -f1 "$STATE_FILE"
|
||||||
assert_line zypper
|
assert_line zypper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "zypper: install reason from the Installed line of zypper info (not 'Installed Size')" {
|
||||||
|
use_fakes zypper rpm
|
||||||
|
run skup info su-dummy-a
|
||||||
|
assert_success
|
||||||
|
assert_output "$(printf '%s\n' 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)' 'reason: explicitly installed' 'required by: su-dummy-d')"
|
||||||
|
log_has 'zypper --non-interactive --no-refresh --quiet info su-dummy-a'
|
||||||
|
run skup info su-dummy-b
|
||||||
|
assert_success
|
||||||
|
assert_output "$(printf '%s\n' 'su-dummy-b: skeptical-updater dummy su-dummy-b (v2.0)' 'reason: installed as a dependency' 'required by: su-dummy-a, su-dummy-d')"
|
||||||
|
# only in a repository
|
||||||
|
run skup info su-dummy-orphan-parent
|
||||||
|
assert_success
|
||||||
|
assert_line 'su-dummy-orphan-parent: skeptical-updater dummy su-dummy-orphan-parent (v1.0)'
|
||||||
|
assert_line 'reason: not installed'
|
||||||
|
# unknown to zypper (exit 104) and to rpm
|
||||||
|
run skup info nothing-like-this
|
||||||
|
assert_failure 1
|
||||||
|
assert_line 'reason: not installed'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "zypper: without an answer from zypper (lock held: exit 7) rpm + libzypp's AutoInstalled decide" {
|
||||||
|
use_fakes zypper rpm
|
||||||
|
export SKEPTICAL_UPDATER_ZYPP_ROOT=$BATS_TEST_TMPDIR/zroot FAKE_ZYPPER_INFO_RC=7
|
||||||
|
# no readable AutoInstalled: the tool cannot tell
|
||||||
|
run skup info su-dummy-b
|
||||||
|
assert_success
|
||||||
|
assert_line 'su-dummy-b: skeptical-updater dummy su-dummy-b (v1.0)'
|
||||||
|
assert_line 'reason: unknown'
|
||||||
|
mkdir -p "$SKEPTICAL_UPDATER_ZYPP_ROOT/var/lib/zypp"
|
||||||
|
printf '# AutoInstalled generated Sat Oct 10 11:14:20 2026\n#\nsu-dummy-b\npattern:minimal_base\n#\n' >"$SKEPTICAL_UPDATER_ZYPP_ROOT/var/lib/zypp/AutoInstalled"
|
||||||
|
run skup info su-dummy-b
|
||||||
|
assert_line 'reason: installed as a dependency'
|
||||||
|
run skup info su-dummy-a
|
||||||
|
assert_line 'reason: explicitly installed'
|
||||||
|
run skup info su-dummy-orphan-parent
|
||||||
|
assert_line 'reason: not installed'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "zypper on Tumbleweed: N? on a new dependency (old -) says 'not installed' without asking" {
|
||||||
|
use_fakes zypper rpm
|
||||||
|
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
||||||
|
FAKE_ZYPPER_DUP=removes run skup --no-refresh <<<$'2?\nq'
|
||||||
|
assert_success
|
||||||
|
assert_line ' [2] su-dummy-newdep - -> 1.0-1 su-dummy'
|
||||||
|
assert_line 'reason: not installed'
|
||||||
|
assert_line 'required by: nothing'
|
||||||
|
log_lacks 'zypper --non-interactive --no-refresh --quiet info su-dummy-newdep'
|
||||||
|
}
|
||||||
@@ -39,7 +39,8 @@ setup() {
|
|||||||
assert_line "apt${T}su-dummy-a${T}user${T}2026-10-09T09:40:12Z"
|
assert_line "apt${T}su-dummy-a${T}user${T}2026-10-09T09:40:12Z"
|
||||||
run skup info su-dummy-b
|
run skup info su-dummy-b
|
||||||
assert_success
|
assert_success
|
||||||
assert_line ' required by (installed): su-dummy-a'
|
assert_line 'reason: installed as a dependency'
|
||||||
|
assert_line 'required by: su-dummy-a'
|
||||||
run skup show su-dummy-a
|
run skup show su-dummy-a
|
||||||
assert_success
|
assert_success
|
||||||
assert_line 'Package: su-dummy-a'
|
assert_line 'Package: su-dummy-a'
|
||||||
|
|||||||
Reference in new issue
Block a user