first commit
This commit is contained in:
commit
1354db6c39
90 files changed
+13123
No files matched your search
@@ -0,0 +1,384 @@
|
||||
# skeptical-updater
|
||||
|
||||
A distro-agnostic, single-file bash applet for a *manual, conscious* update cycle.
|
||||
It wraps the native package manager (apt, dnf/yum, pacman/pman, zypper), lists
|
||||
upgradable packages, lets the user interrogate each one, and only then runs the
|
||||
native upgrade. Packages the user has explicitly acknowledged are remembered in a
|
||||
statefile and never questioned again. It is one file with no runtime dependencies
|
||||
beyond bash >= 4.3 and the distro's package manager, and it never reimplements
|
||||
package management: every action shells out to the native tool.
|
||||
|
||||
The specification is [docs/DESIGN.md](docs/DESIGN.md); the per-family command
|
||||
research it is built on lives in [discovery/](discovery/).
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
sudo make install # /usr/local/bin/skeptical-updater
|
||||
sudo make install PREFIX=/usr # /usr/bin/skeptical-updater
|
||||
make install DESTDIR=/tmp/pkg PREFIX=/usr # staged install, e.g. for packaging
|
||||
sudo make uninstall
|
||||
```
|
||||
|
||||
Or just copy the `skeptical-updater` file somewhere in `PATH` and make it executable.
|
||||
The statefile directory (`/var/lib/skeptical-updater/`) is created by the applet
|
||||
on first use, not by `make install`; `make uninstall` leaves it in place.
|
||||
|
||||
Runtime needs: bash >= 4.3, the package manager, coreutils, sed and grep (plus awk,
|
||||
which the apt and pacman backends use and which those distros always ship). `sudo`
|
||||
is optional: when the tool needs root and you are not root, it re-runs itself
|
||||
through `sudo` with the same arguments, or fails with a clear message.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
skeptical-updater [options] [command]
|
||||
|
||||
commands:
|
||||
(none) interactive review + upgrade
|
||||
list print the review list and exit (no prompt)
|
||||
trusted print 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)
|
||||
refresh recompute dependency closures for every 'user' root
|
||||
info PKG one-line description + installed reverse dependencies (as N?)
|
||||
show PKG full native package information (as N??)
|
||||
|
||||
options:
|
||||
--backend=NAME force apt|dnf|pacman|zypper (default: detect pacman > apt > dnf/yum > zypper)
|
||||
--all also list trusted packages (marked with '=')
|
||||
--no-refresh skip the metadata refresh step
|
||||
--dry-run print native commands instead of running mutating ones
|
||||
--state-dir=DIR override /var/lib/skeptical-updater (env: SKEPTICAL_UPDATER_STATE_DIR)
|
||||
--no-color disable ANSI colours (also honours NO_COLOR and non-tty stdout)
|
||||
-h, --help this help
|
||||
-V, --version print the version
|
||||
```
|
||||
|
||||
Exit status: `0` ok, `1` usage or backend error, `2` aborted (end of input),
|
||||
otherwise the exit status of the package manager's upgrade step.
|
||||
|
||||
What needs root: the metadata refresh, the upgrade, a purge, and writing a state
|
||||
directory you cannot write. `--dry-run`, `list --no-refresh`, `trusted`, `info` and
|
||||
`show` work unprivileged (as long as the state directory is readable).
|
||||
|
||||
### Example session
|
||||
|
||||
A Debian 12 box with three pending upgrades; `su-dummy-c` is on hold
|
||||
(`apt-mark hold`) and `su-dummy-a` depends on `su-dummy-b`:
|
||||
|
||||
```
|
||||
$ sudo skeptical-updater
|
||||
Backend: apt (Debian GNU/Linux 12 (bookworm))
|
||||
Refreshing package lists... done.
|
||||
3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:
|
||||
|
||||
[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo
|
||||
[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo
|
||||
[3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held]
|
||||
|
||||
<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever
|
||||
> 2?
|
||||
su-dummy-b: dummy package su-dummy-b for skeptical-updater tests
|
||||
required by (installed): su-dummy-a
|
||||
> 1+
|
||||
trusted su-dummy-a (+1 dependency: su-dummy-b)
|
||||
> 3-
|
||||
excluded from this run: su-dummy-c
|
||||
> l
|
||||
3 upgradable, 2 trusted (hidden, use --all to show), 1 to review:
|
||||
|
||||
-[3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held]
|
||||
>
|
||||
Excluded from this run: su-dummy-c
|
||||
+ apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade
|
||||
...
|
||||
The following packages have been kept back:
|
||||
su-dummy-c
|
||||
The following packages will be upgraded:
|
||||
su-dummy-a su-dummy-b
|
||||
...
|
||||
Upgrade finished.
|
||||
|
||||
$ skeptical-updater trusted
|
||||
# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)
|
||||
apt su-dummy-a user 2026-10-09T19:24:13Z
|
||||
apt su-dummy-b dep:su-dummy-a 2026-10-09T19:24:13Z
|
||||
```
|
||||
|
||||
Next time `su-dummy-a` and `su-dummy-b` are upgraded silently; only new packages
|
||||
are questioned. The prompt reads stdin, so the same session can be scripted:
|
||||
`printf '3-\n\n' | sudo skeptical-updater` (an empty line is what triggers the
|
||||
upgrade; end of input without one aborts with exit status 2).
|
||||
|
||||
`[held]` marks packages the package manager will not upgrade on its own: apt
|
||||
holds and "kept back" packages, pacman `IgnorePkg` (`[ignored]` in `pacman -Qu`).
|
||||
dnf `exclude=`/versionlock and zypper locks hide packages from the native listing,
|
||||
so they do not appear at all.
|
||||
|
||||
### Command grammar
|
||||
|
||||
One line, whitespace-separated tokens; `N` is the number in brackets. Numbers are
|
||||
stable for the whole session (rows that leave the list leave a gap). Every token
|
||||
of a line is validated first: a line with an unknown token or a number that is not
|
||||
listed prints an error, runs nothing, and the prompt comes back.
|
||||
|
||||
| token | action |
|
||||
|-------|--------|
|
||||
| empty line | proceed: run the native upgrade with the current exclusions |
|
||||
| `N?` | one-line description and the installed packages that depend on it |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `N+` | trust forever: write it and its installed dependency closure to the statefile; the row leaves the list. Rows this hides are re-included if they were excluded (a trusted package is upgraded) |
|
||||
| `*+` / `*-` | `+` / `-` for every listed package (`*-` is an all-or-nothing toggle: when all are excluded it re-includes them) |
|
||||
| `l` | redraw the list |
|
||||
| `q` | quit without upgrading (exit 0) |
|
||||
| `h`, `?` | help |
|
||||
| end of input | abort without upgrading (exit 2) |
|
||||
|
||||
Several tokens may share a line (`1? 3- 4+`); they run left to right. With `--all`
|
||||
trusted packages are listed too, marked with `=`. When purges leave nothing to upgrade the
|
||||
session ends by itself (exit 0).
|
||||
|
||||
Under the rows, `Note:` lines report pending work that is not a row: new dependencies or
|
||||
replacements only the pacman transaction (`-Sup`) shows, packages dnf installs to obsolete
|
||||
others, packages a Tumbleweed `dist-upgrade` removes. With no row but such work, the list
|
||||
says "0 to review, but ... reports pending changes" and `<enter>` still runs the upgrade.
|
||||
|
||||
### Dry run
|
||||
|
||||
`--dry-run` prints every mutating command as `+ <command>` instead of running it:
|
||||
the upgrade, purges, apt holds/unholds, zypper lock files, the installation of the reviewed
|
||||
pacman databases and statefile writes. The
|
||||
metadata refresh still runs when you are root (it is printed when you are not), and
|
||||
read-only queries run normally, so the list is real:
|
||||
|
||||
```
|
||||
$ skeptical-updater --dry-run <<<$'2-\n'
|
||||
...
|
||||
Excluded from this run: su-dummy-b
|
||||
+ apt-mark hold su-dummy-b
|
||||
+ apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade
|
||||
+ apt-mark unhold su-dummy-b
|
||||
Dry run: nothing was changed.
|
||||
```
|
||||
|
||||
## Statefile
|
||||
|
||||
`/var/lib/skeptical-updater/trusted.tsv` (directory override: `--state-dir=DIR` or
|
||||
`SKEPTICAL_UPDATER_STATE_DIR`). The directory is created `0755` on the first write.
|
||||
|
||||
```
|
||||
# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)
|
||||
apt nodejs user 2026-10-09T09:40:12Z
|
||||
apt libnode115 dep:nodejs 2026-10-09T09:40:12Z
|
||||
```
|
||||
|
||||
- Tab-separated, one record per line, sorted and unique on (backend, package);
|
||||
lines starting with `#` are comments and are preserved (kept at the top).
|
||||
Malformed lines are kept verbatim with a warning.
|
||||
- `reason` is `user` for a package you confirmed (`N+` / `trust`), or
|
||||
`dep:<root>` for a package that was part of the installed dependency closure of
|
||||
`<root>` when you trusted it. A `user` row is never downgraded to `dep:`.
|
||||
- A package is trusted when a row with the current backend and its name exists;
|
||||
the reason does not matter. Rows of other backends are kept untouched.
|
||||
- Writes are atomic: `trusted.tsv.tmp.<pid>` in the same directory, renamed over
|
||||
the old file. They run under `trusted.lock` (`flock`, or an atomic `mkdir` lock with stale
|
||||
detection where `flock` is missing): the file is re-read and this session's changes are
|
||||
replayed onto it, so a `trust` in another terminal during a session is kept.
|
||||
- A state directory you cannot enter, or a statefile you cannot read, is an error (never
|
||||
silently "nothing trusted").
|
||||
- `untrust PKG` removes the rows for `PKG` and every `dep:PKG` row; `refresh`
|
||||
recomputes the closure of every `user` root (adds new dependencies, drops rows
|
||||
that are no longer needed, keeps the dates of the others).
|
||||
|
||||
The other files are recovery aids that only exist while an upgrade runs (or after one was
|
||||
killed with `kill -9` or a power loss): `holds.added`, the apt hold journal (used under
|
||||
`holds.lock`), and `zypper-private`, the path of the private zypper configuration. At the start
|
||||
of every root run, before anything is listed, a left-over journal is replayed (the holds it
|
||||
recorded are released, a message says which) and a left-over private configuration is removed.
|
||||
|
||||
## How exclusions work
|
||||
|
||||
`N-` exclusions are per run and never persisted: you are asked again next time.
|
||||
Permanent holds belong to the package manager (`apt-mark hold`, `IgnorePkg`,
|
||||
`zypper addlock`, dnf `exclude=`), and skeptical-updater never changes them.
|
||||
|
||||
| backend | refresh | upgrade | per-run exclusion | purge |
|
||||
|---------|---------|---------|-------------------|-------|
|
||||
| apt | `apt-get update -qq -o APT::Update::Error-Mode=any` | `apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade` | temporary `apt-mark hold` of the excluded packages that are *not already held*, journalled in `holds.added` before holding; afterwards exactly those are released (also on failure, Ctrl-C, TERM, HUP, QUIT, a second signal during the restore, or a closed stdout; a journal left by `kill -9` is replayed at the start of the next root run). Holds you placed are never touched. `apt-mark` ignores `DPkg::Lock::Timeout`, so it is retried for up to 60 s while dpkg's lock is busy. | plan: `apt-get -s purge --auto-remove PKG` minus what `apt-get -s autoremove` already listed (`--auto-remove` is system-wide), then `apt-get -y purge` of exactly that set: PKG and the dependencies this purge orphans; no `autoremove` |
|
||||
| pacman / pman | `pacman -Sy --dbpath <throwaway copy>` (with `local` linked to the real local database, seeded with the system's `*.db` and `*.db.sig`): listing, `q`, end of input and `--dry-run` never touch `/var/lib/pacman/sync`; errors pacman prints about it are shown | check `pacman -Sup --noconfirm --ignore ... --dbpath <throwaway>` (on failure: nothing changed, its exit status), install the reviewed `*.db`/`*.db.sig` into `/var/lib/pacman/sync` under `db.lck`, all-or-nothing, never over a newer system database (exit 2), then `pacman -Su --noconfirm` - exactly the reviewed metadata, no second sync; with `--no-refresh` plain `pacman -Su` | `--ignore a,b` | plan: `pacman -Rs --print`; then `pacman -Rns --noconfirm -- ...` |
|
||||
| dnf / yum | `dnf -q --refresh --setopt=skip_if_unavailable=0 makecache` | `dnf -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 upgrade` (the reviewed metadata; the repo glob is needed because a repo's own `metadata_expire` overrides the main one) | `--exclude=a,b` (adds to `exclude=` from the configuration) | plan: `dnf --assumeno remove PKG`; then `dnf -y remove ...` |
|
||||
| zypper | `zypper --non-interactive --quiet refresh` | `zypper --non-interactive --no-refresh update` (Leap/SLE) or `dist-upgrade` (Tumbleweed/Slowroll, by `ID=` in os-release) | locks in a *private* `ZYPP_CONF`: the effective configuration (main zypp.conf plus the `zypp.conf.d` drop-ins of `/usr/etc` and `/etc`) with `locksfile.path` pointing at a copy of the effective lock file (also when a drop-in sets it), plus `addlock` of the exclusions; deleted afterwards: the real lock file is never written | plan: `zypper --xmlout remove --dry-run --clean-deps PKG`; then `zypper --non-interactive --no-refresh remove --clean-deps ...` |
|
||||
|
||||
Every package-manager call runs with `LC_ALL=C` (and `DEBIAN_FRONTEND=noninteractive`;
|
||||
`COLUMNS=0` for pacman), with stdin from `/dev/null` so that it can never eat the
|
||||
commands you pipe in. The upgradable list comes from `apt-get -s --with-new-pkgs
|
||||
upgrade` (Inst lines + the "kept back" block), `pacman -Qu` joined with `pacman -Sl`,
|
||||
`dnf -q check-update` (+ `rpm -qa` for the installed versions) and
|
||||
`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
|
||||
`apt-cache rdepends` / the dpkg database, `pacman -Qi`, and plain `rpm` queries on
|
||||
dnf and zypper systems (see the discovery reports for why).
|
||||
|
||||
## Testing
|
||||
|
||||
Everything runs in Docker; the host only needs GNU make, bash, Docker and (once,
|
||||
for `make vendor`) curl. Run `make help` for the full list of targets.
|
||||
|
||||
| target | what it does |
|
||||
|--------|--------------|
|
||||
| `make vendor` | download the pinned bats-core, bats-support and bats-assert into `tests/vendor/` (once; no network afterwards) |
|
||||
| `make lint` | shellcheck the applet, the test scripts, fixtures and fakes (`koalaman/shellcheck` image; `discovery/` is reference material and is not linted) |
|
||||
| `make test-unit` | `tests/unit/*.bats` in a `debian:12` container, against fake package managers |
|
||||
| `make test-e2e` | `tests/e2e/` inside every distro listed in `tests/docker/images.txt`, against the real package managers; logs go to `tests/docker/logs/<label>.log` |
|
||||
| `make test` | snapshot, `test-unit`, `test-e2e`, then **always** `docker-cleanup`; exits with the tests' status |
|
||||
| `make test-no-cleanup` | same as `test` but keeps the pulled images, for iterating |
|
||||
| `make docker-cleanup-dry-run` | show what the cleanup would do |
|
||||
| `make clean` | remove test logs and `.bats-tmp/` |
|
||||
|
||||
Variables: `make test-e2e E2E_ONLY=debian-12` (comma-separated list allowed)
|
||||
runs a subset of distros; `E2E_FAIL_FAST=1` stops at the first failing distro.
|
||||
|
||||
### Unit tests (`tests/unit/`)
|
||||
|
||||
No root, no network, no real package manager. The applet runs with a hermetic
|
||||
`PATH`: the fakes in `tests/unit/fakes/<backend>/` (`apt-get`, `apt-cache`,
|
||||
`dpkg-query`, `apt-mark`, `pacman`, `pman`, `dnf`, `yum`, `rpm`, `zypper`, `sudo`)
|
||||
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
|
||||
(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
|
||||
detection and override, root handling and the sudo re-exec, every parser, the
|
||||
statefile, the whole interactive grammar, the per-backend exclusion mechanics (apt
|
||||
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,
|
||||
`--dry-run`, and the robustness fixes (hold restore with a closed stdout and with a second
|
||||
signal during the exit handler, the holds lock kept while a restore failed, journal and private
|
||||
zypper configuration replayed only when no other run holds their lock, the pacman pre-check /
|
||||
database install / `-Su` sequence and stale throwaway databases, statefile locking and merge,
|
||||
the `mkdir` lock fallback (EPERM owner alive, no busy loop), an inaccessible state directory, the
|
||||
sudo re-exec of the file itself and its loop guard).
|
||||
About 30 seconds.
|
||||
|
||||
### End-to-end tests (`tests/e2e/`)
|
||||
|
||||
`tests/docker/run-e2e.sh <label> <image>` runs `tests/e2e/entrypoint.sh` as root in a
|
||||
throwaway container (repository mounted read-only). The entrypoint installs the
|
||||
applet, builds a fixture with `tests/e2e/fixtures/{apt,pacman,rpm}.sh` (copies of
|
||||
the proven `discovery/` fixtures): `su-dummy-a` (depends on `su-dummy-b`),
|
||||
`su-dummy-b` and `su-dummy-c` installed at 1.0, 2.0 offered by a local repository,
|
||||
plus `su-dummy-orphan`, an unrelated automatically-installed package nothing depends on,
|
||||
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),
|
||||
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,
|
||||
list, `N?`, `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
|
||||
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),
|
||||
purging everything, `q`, end of input, `--dry-run`, the read-only commands as an unprivileged
|
||||
user, plus per backend: pacman never touching the real sync database outside the upgrade and
|
||||
installing exactly the reviewed database at `<enter>` (same hash, a package published after the
|
||||
review is not installed), apt
|
||||
holds waiting for a busy dpkg lock, zypper honouring a lock file set in a drop-in and applying
|
||||
the reviewed metadata although a newer package appeared after the review.
|
||||
|
||||
The matrix (`tests/docker/images.txt`):
|
||||
|
||||
| label | image | backend |
|
||||
|-------|-------|---------|
|
||||
| debian-12, debian-13 | `debian:12`, `debian:13` | apt |
|
||||
| ubuntu-22.04, ubuntu-24.04, ubuntu-latest | `ubuntu:22.04`, `ubuntu:24.04`, `ubuntu:latest` | apt |
|
||||
| archlinux | `archlinux:latest` | pacman |
|
||||
| almalinux-9, almalinux-10 | `almalinux:9`, `almalinux:10` | dnf (dnf4 on both) |
|
||||
| rockylinux-9, rockylinux-10 | `rockylinux/rockylinux:9`, `rockylinux/rockylinux:10` | dnf (dnf4) |
|
||||
| opensuse-tumbleweed, opensuse-leap | `opensuse/tumbleweed:latest`, `opensuse/leap:latest` | zypper (`dist-upgrade` / `update`) |
|
||||
|
||||
The Rocky images come from the `rockylinux/` namespace: the Docker Hub library
|
||||
`rockylinux:9` is a stale Rocky 9.3 build and `rockylinux:10` does not exist.
|
||||
The rpm fixtures install `rpm-build` and `createrepo_c` from the distro mirrors, so
|
||||
the rpm-family runs need network for that step (everything after it is offline).
|
||||
|
||||
### Docker image hygiene
|
||||
|
||||
The E2E matrix pulls a dozen distro images. To leave the host as it was:
|
||||
|
||||
- `tests/docker/snapshot.sh` (`make docker-snapshot`) records the image set in
|
||||
`tests/docker/images-before.tsv` and `tests/docker/image-ids-before.txt`. It
|
||||
**refuses to overwrite an existing snapshot**, so a second `make test` cannot
|
||||
sneak the images pulled by the first run into the baseline.
|
||||
- `tests/docker/cleanup.sh` (`make docker-cleanup`) restores that set: it points
|
||||
pre-existing tags that a newer pull took over (e.g. `archlinux:latest`) back at
|
||||
their original image, untags tags that are new, and deletes image ids that are
|
||||
new. It never stops or removes containers; images still used by a container are
|
||||
left in place and reported. It is idempotent and exits non-zero if a
|
||||
pre-existing tag could not be restored. `--dry-run` prints the plan.
|
||||
|
||||
`make test` always runs the cleanup at the end; use `make test-no-cleanup` (or the
|
||||
individual `test-unit` / `test-e2e` targets) while iterating and `make docker-cleanup`
|
||||
when done. To re-baseline on purpose (for example after pulling images you want to
|
||||
keep), run `make docker-cleanup` first if a test run happened since the last
|
||||
snapshot, then:
|
||||
|
||||
```sh
|
||||
tests/docker/snapshot.sh --force
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
From the discovery reports (`discovery/*.md`), all reproduced there:
|
||||
|
||||
- **pacman**: excluding a package that another upgrade needs at a newer version
|
||||
(e.g. `a` 2.0 depends on `b>=2.0`, `b` excluded) makes `pacman -Su --noconfirm`
|
||||
abort the *whole* transaction (exit 1, nothing upgraded): `--noconfirm` answers
|
||||
"skip the unresolvable package?" with no. Exclude the dependants too.
|
||||
- **dnf4** (EL9/EL10): the same situation is an error (exit 1) unless `--nobest`
|
||||
is used; dnf5 silently skips the dependant (exit 0). The tool prints a hint, it
|
||||
does not retry with `--nobest`.
|
||||
- **zypper on Tumbleweed**: `dist-upgrade` with a vendor change (or another
|
||||
problem the solver cannot decide alone) is cancelled by `--non-interactive` with
|
||||
exit 4; run `zypper dist-upgrade` by hand to pick a solution.
|
||||
- **apt**: holding an excluded package also keeps back everything that needs a
|
||||
newer version of it (shown in apt's own "kept back" output).
|
||||
- **pacman**: the upgrade applies exactly the reviewed metadata: the reviewed transaction is
|
||||
checked first (`pacman -Sup` against the throwaway database; if it fails, e.g. the ignored-
|
||||
dependency trap above, nothing on the system changes and the tool exits with pacman's status),
|
||||
then the reviewed databases are installed into `/var/lib/pacman/sync` (under pacman's own
|
||||
`db.lck`, all-or-nothing) and `pacman -Su` runs. If a system database changed since the review
|
||||
and is newer (you ran `pacman -Sy` meanwhile), nothing is changed and the tool exits with 2:
|
||||
run it again. If that `pacman -Su` still fails or is interrupted (a file conflict, Ctrl-C, a
|
||||
full disk), the system's sync database is newer than its installed packages - the tool warns;
|
||||
finish with `pacman -Su` or run it again before any `pacman -S`. `--no-refresh` applies the existing databases with `-Su`.
|
||||
Throwaway databases left by `kill -9` are removed at the start of the next root run.
|
||||
- **dnf**: `--setopt=*.metadata_expire=-1` freezes cached metadata (verified on dnf4 and dnf5
|
||||
against an HTTP repository), but libdnf reads local `file://` repositories live, so those
|
||||
can change between review and upgrade. dnf5 is only exercised by the unit tests (with
|
||||
recorded dnf5 output); the EL images of the E2E matrix all run dnf4.
|
||||
- **zypper**: the private configuration assumes libzypp's layering (main file from `/etc`,
|
||||
else `/usr/etc`; drop-ins of both `zypp.conf.d` directories, `/etc` winning on equal names,
|
||||
in name order; nothing else when `ZYPP_CONF` is set). Verified on Tumbleweed and Leap 16 for
|
||||
an `/etc` drop-in. On Leap the review list is `list-updates` (new dependencies are not rows);
|
||||
on Tumbleweed the plan needs root, so unprivileged runs show `list-updates` with a note.
|
||||
- Purges follow pacman's `-Rns` principle on every backend: a package that other installed
|
||||
packages depend on is not purged (they are listed; purge them first), and only dependencies
|
||||
that were installed automatically and that nothing else needs go along with the target -
|
||||
dependencies installed by hand stay. The reverse-dependency query is the one `N?` uses (apt:
|
||||
`apt-cache rdepends --installed`, which also counts a dependent whose candidate, not installed,
|
||||
version needs the package; rpm: provides + files); a dependent it misses is still caught by the
|
||||
check that the simulated removal holds nothing outside the target's dependency closure.
|
||||
- **Closures are computed at trust time.** A dependency that appears later is new
|
||||
to the system and is questioned once; run `skeptical-updater refresh` to re-trust
|
||||
the current closures of all your trusted packages wholesale. A package shared by
|
||||
several trusted roots is recorded under the first one only, so `untrust` of that
|
||||
root drops it until the next `refresh`.
|
||||
- The upgradable lists show what the native listing shows: new dependencies are rows on apt
|
||||
and on Tumbleweed (`-` as the old version); on pacman and dnf new dependencies,
|
||||
replacements and obsoletes only appear as a note, and removals only on Tumbleweed (note);
|
||||
everything else the transaction does is visible in the package manager's own output.
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0-or-later, see [LICENSE](LICENSE).
|
||||
Reference in new issue
Block a user