The removal-count guard was skipped whenever --yes was set:
if [[ "${ASSUME_YES}" != "yes" && "${remv_count}" -gt ... ]]
and prune refuses to run *without* --yes. So inside prune -- the only
command that removes anything -- the first condition was always false
and the threshold could never fire. It fired only in plan, which changes
nothing. The guard was live exactly where it did not matter and dead
where it did.
The check is now unconditional, with --max-removals N as the explicit
override, so raising the limit is a separate decision from not wanting
to be prompted. The critical-package check was already unconditional and
is unchanged.
Verified with --yes set and the default limit of 75: an ordinary purge
passes, 90 removals aborts, and a critical package aborts.
Also applies the same startup fix as the converter -- mkdir and the tee
redirection ran at file scope, before argument parsing, so --help and a
mistyped flag failed with a raw mkdir error rather than printing usage.
ubuntu-desktop-prune.sh has had this same guardrail all along, and parses
the simulation more carefully than the version I wrote for the converter.
Two differences were real bugs:
apt-get -s emits the architecture qualifier in multiarch situations, so a
critical removal appears as "Remv sudo:amd64". The exact match against
CRITICAL_PACKAGES compared that against "sudo" and did not match -- the
gate would have waved through the single case it exists to stop. The
suffix is now stripped, as the prune script already did.
apt also prints "Purg <pkg>" for purges, not only "Remv". Those were not
matched at all, so a purged package counted toward neither the critical
check nor the removal threshold.
Verified against simulated output: "Remv sudo:amd64" and
"Purg systemd:amd64" now both abort, and a mixed non-critical set still
passes.
Both `mkdir -p "$LOG_DIR"` and `exec > >(tee -a "$LOG_FILE")` ran at file
scope, before a single argument was parsed. On any machine where
/var/log is not writable by the caller -- which is every machine, since
this needs sudo -- `--help` and a mistyped flag both failed with a raw
mkdir or tee error rather than printing usage or naming the bad flag.
Both now happen in ensure_log_dir(), called from main() once a real
command has been dispatched, so usage and argument errors work for
anyone while everything from the command onward is still logged. exec
applies to the shell rather than the function, so moving it changes
nothing about what gets captured -- verified by running a command with
the log directory redirected and confirming the output landed in it.
A non-root command now also fails with "Cannot create /var/log/... --
re-run with sudo" instead of leaking mkdir's own message.
With the simulation gate implemented, the safety model section is now
true, and says where the gate runs (inside convert, immediately before
the install) and what it writes. Plan mode is labelled advisory, because
that is what it is: it logs a simulation for a human to read and decides
nothing.
Removed four options the README documented that were never implemented:
--no-auto-fix, --overwrite-keyring, --recreate-keyring and
--no-purge-flavor. Documented --preserve-snap, which was implemented and
missing. Every flag now matches the parser in both directions.
Two troubleshooting sections described behaviour that does not exist.
Keyring recovery pointed at the two missing keyring flags; the real
mechanism is a check against the expected Mint key ID with --yes to
overwrite, which is worth stating since it is what makes the plain-HTTP
fallback safe. Flavor-package purging was never written, so the login
loop it referred to is now described as something to fix by hand.
Also removes --no-install-recommends from the usage text, which I added
in the previous commit believing it was a script flag. It is an apt
option inside apt_opts_common; the parser rejects it. That is the same
class of error this commit exists to fix, found by checking the flag
lists against the parser in both directions rather than trusting either.
The README's safety model claimed convert "runs an APT simulation and
aborts if APT wants to remove critical packages" or if "too many
removals are detected (default threshold 40)". Neither existed. There
was no --max-removals flag, no critical-package list, and convert ran
straight from `apt-get update` to `apt-get -y install` with nothing in
between. plan mode does simulate, but it only tees the output to a file
and checks the exit code -- it never reads the removals, and convert
never calls it.
That is the gap that matters here: mixing Mint and Ubuntu repositories
is exactly when APT resolves a conflict by proposing to remove a large
part of the system, and -y means nothing stops it.
simulate_and_gate now runs the same install as a simulation against the
live APT configuration -- sources and pinning are already written by
that point, so it reflects what the real install would do -- and parses
the Remv lines:
- any package in CRITICAL_PACKAGES aborts unconditionally. There is no
threshold at which removing sudo, systemd, libc6 or the kernel meta
package is acceptable.
- more than --max-removals (default 40) aborts, listing them.
- anything below that is listed as a warning and allowed.
- a simulation APT cannot resolve aborts rather than proceeding.
The package list moved into mint_stack_packages(), read by both the gate
and the installer, so the gate cannot end up vouching for a different
set of packages than the one that gets installed.
Also fixes usage(), which told users to run `convert --i-accept-theISK`.