The repository audit made any Terra that is not Panama's own pinned form a trust-root failure, and status 78 then stopped every stage before it ran. A machine that installed Terra the way Terra documents it -- terra-release's own repo file, a metalink, the key at its stock path -- was classified hostile and had no way back, because install_terra_repository refused to touch a machine terra-release had already reached. A gate with no door. The trust root is the signing key, and that key is byte-for-byte the fingerprint this repository reviewed and pinned, with every signature check already on. So verify the fingerprint and adopt the configuration into the pinned form instead of refusing it. Adoption needs no network and no DNF, it runs before any other transaction in the stage, and it is repeatable, which it has to be: terra-release owns that file and restores it on update. Adoption stays narrow. The pinned fingerprint must match both the reviewed key and the key the machine actually verifies against, the gpgkey must be a local file under the system trust directory, and the endpoint must be one Terra itself serves -- so the reviewed baseurl or the reviewed metalink host, now pinned as TERRA_METALINK_BASEURL. An unknown key, a redirected baseurl, a second enabled Terra, or a disabled signature check is still a hard refusal. A refusal also stops less than it did. It suppresses the stages that open DNF and the migrations, which may run a transaction of their own, and the run still exits 78. It no longer stops link-dotfiles, link-skills or link-user, which read no repository and install no package. Exiting before them is what left this laptop with a stale ~/.claude/skills and no shipped skill reachable. Also stub ensure_flathub_remote in the extras contract, which has been failing since that call was added to install_extra_category without one. Claude-Session: https://claude.ai/code/session_01PeTrG9dGY89UWuhGm4Pr1s
289 lines
12 KiB
Bash
Executable File
289 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The optional application categories: what the interview offers, and what
|
|
# choosing one actually installs.
|
|
#
|
|
# The rules:
|
|
#
|
|
# 1. Every category the interview offers installs something. The menu is read
|
|
# from the directory rather than written down, so an empty or missing file
|
|
# is a checkbox that does nothing -- the same defect this repository has
|
|
# refused to ship twice now.
|
|
# 2. A `flatpak:` line reaches flatpak with the prefix removed, and never
|
|
# reaches dnf. Getting this backwards installs nothing and says it did.
|
|
# 3. Nothing is installed without being asked for. An extras loop that ran on
|
|
# its own defaults would put Steam on a work laptop.
|
|
# 4. Every name is a real package. The whole point of admitting applications
|
|
# one at a time from dnf or Flathub is that they stay installable; a typo
|
|
# here is a category that fails on somebody else's machine, not this one.
|
|
# Checked against the actual repositories, and skipped when offline.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
installer="$repo_dir/setup/scripts/install-packages"
|
|
interview="$repo_dir/setup/scripts/interview"
|
|
extras_dir="$repo_dir/setup/packages/extras"
|
|
catalog="$repo_dir/setup/lib/extras-catalog"
|
|
|
|
# The same parser both front doors use. A contract that re-implemented the
|
|
# format would eventually be testing its own idea of it rather than the one that
|
|
# runs -- which is exactly how the package lists came to be annotated with
|
|
# comments that every contract stripped and dnf did not.
|
|
# shellcheck source=../../setup/lib/extras-catalog
|
|
source "$catalog"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
[[ -d "$extras_dir" ]] || { printf 'extras contract: no %s\n' "$extras_dir" >&2; exit 1; }
|
|
|
|
# ── 1. Every category is a real offer ────────────────────────────────────────
|
|
|
|
shopt -s nullglob
|
|
categories=("$extras_dir"/*)
|
|
(( ${#categories[@]} > 0 )) || note 'the extras directory is empty, so the checklist offers nothing'
|
|
|
|
for category in "${categories[@]}"; do
|
|
name="$(basename "$category")"
|
|
[[ -f "$category" ]] || { note "$name is not a file"; continue; }
|
|
entries="$(catalog_entries "$category" | grep -c . || true)"
|
|
(( entries > 0 )) || note "the $name category installs nothing, so choosing it does nothing"
|
|
done
|
|
|
|
# The menu has to come from the directory. A hardcoded list is one that goes
|
|
# stale the first time a category is added.
|
|
grep -q 'extras_dir' "$interview" \
|
|
|| note 'the interview does not read the categories from the extras directory'
|
|
grep -q 'gum choose --no-limit' "$interview" \
|
|
|| note 'the interview does not offer the categories as a multiple-choice checklist'
|
|
|
|
# ── 2 & 3. What a chosen category actually installs ──────────────────────────
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
filter="$(sed -n '/^packages_in()/,/^}/p' "$installer")"
|
|
loop="$(sed -n '/^install_extra_category()/,/^}/p' "$installer")"
|
|
# install_extra_category verifies the Flathub remote before installing a
|
|
# flatpak, and records a soft failure when it cannot. Both live outside the
|
|
# extracted function and have contracts of their own, so they stand in here as
|
|
# trusted -- what is under test is which targets reach which installer.
|
|
deps='ensure_flathub_remote() { :; }
|
|
softly_failed=()'
|
|
[[ -n "$filter" && -n "$loop" ]] || {
|
|
printf 'extras contract: install-packages no longer defines packages_in and install_extra_category\n' >&2
|
|
exit 1
|
|
}
|
|
|
|
stub_dir="$work/bin"
|
|
mkdir -p "$stub_dir"
|
|
calls="$work/calls"
|
|
for command in dnf flatpak; do
|
|
cat >"$stub_dir/$command" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf '%s %s\n' "$command" "\$*" >>"$calls"
|
|
STUB
|
|
chmod +x "$stub_dir/$command"
|
|
done
|
|
cat >"$stub_dir/sudo" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
exec "$@"
|
|
STUB
|
|
chmod +x "$stub_dir/sudo"
|
|
|
|
fixture="$work/mixed"
|
|
cat >"$fixture" <<'LIST'
|
|
# Both managers in one category, which is the case this format exists for.
|
|
from-dnf
|
|
flatpak:org.example.FromFlathub
|
|
LIST
|
|
|
|
(
|
|
PATH="$stub_dir:$PATH"
|
|
log() { :; }
|
|
source "$catalog"
|
|
eval "$filter"
|
|
eval "$loop"
|
|
eval "$deps"
|
|
install_extra_category "$fixture"
|
|
)
|
|
|
|
recorded="$(cat "$calls" 2>/dev/null)"
|
|
|
|
grep -q 'dnf install -y from-dnf' <<<"$recorded" \
|
|
|| note 'a bare line in a category is not installed with dnf'
|
|
grep -q 'flatpak install -y flathub org.example.FromFlathub' <<<"$recorded" \
|
|
|| note 'a flatpak: line does not reach flatpak with the prefix removed'
|
|
if grep 'dnf install' <<<"$recorded" | grep -q 'flatpak:'; then
|
|
note 'a flatpak: line is passed to dnf, which cannot install it'
|
|
fi
|
|
if grep 'flatpak install' <<<"$recorded" | grep -q 'from-dnf'; then
|
|
note 'a dnf package is passed to flatpak'
|
|
fi
|
|
|
|
# A flatpak-only category -- which most of the real ones are -- must survive
|
|
# the installer's own strict options. Filtering the dnf half with `grep -v`
|
|
# once left an exit 1 for zero matches, and set -e killed the stage before its
|
|
# flatpak half ran. Run under those options, not the contract's laxer ones;
|
|
# the status is captured rather than `||`-guarded because a condition context
|
|
# would switch errexit off inside the subshell and hide the very failure this
|
|
# pins.
|
|
: >"$calls"
|
|
flatpak_only="$work/flatpak-only"
|
|
printf 'flatpak:org.example.OnlyFlatpak\n' >"$flatpak_only"
|
|
(
|
|
set -euo pipefail
|
|
PATH="$stub_dir:$PATH"
|
|
log() { :; }
|
|
source "$catalog"
|
|
eval "$filter"
|
|
eval "$loop"
|
|
eval "$deps"
|
|
install_extra_category "$flatpak_only"
|
|
)
|
|
flatpak_only_status=$?
|
|
(( flatpak_only_status == 0 )) \
|
|
|| note 'a flatpak-only category aborts the installer under set -euo pipefail'
|
|
grep -q 'flatpak install -y flathub org.example.OnlyFlatpak' <<<"$(cat "$calls" 2>/dev/null)" \
|
|
|| note 'a flatpak-only category installs nothing'
|
|
|
|
# Choosing nothing installs nothing.
|
|
: >"$calls"
|
|
(
|
|
PATH="$stub_dir:$PATH"
|
|
PANAMA_PATH="$repo_dir"
|
|
log() { :; }
|
|
source "$catalog"
|
|
eval "$filter"
|
|
eval "$loop"
|
|
eval "$deps"
|
|
EXTRAS_DIR="$extras_dir"
|
|
for extra in ${PANAMA_EXTRAS:-}; do
|
|
[[ -f "$EXTRAS_DIR/$extra" ]] && install_extra_category "$EXTRAS_DIR/$extra"
|
|
done
|
|
)
|
|
[[ -s "$calls" ]] && note 'with no categories chosen the installer still installed something'
|
|
|
|
# ── 3b. Labels and bundles ───────────────────────────────────────────────────
|
|
#
|
|
# Two pieces of syntax carry real weight, and both fail quietly when wrong: a
|
|
# label that leaked into an install command would be handed to dnf as a package
|
|
# name, and a bundle that did not resolve would install OBS without the plugins
|
|
# that are the reason to pick it.
|
|
|
|
bundle_fixture="$work/bundle"
|
|
cat >"$bundle_fixture" <<'LIST'
|
|
# A named entry, a bundle, and a plain one.
|
|
flatpak:com.example.Named | A Friendly Name
|
|
flatpak:com.example.Host | Host
|
|
flatpak:com.example.Host.Plugin.One
|
|
flatpak:com.example.Host.Plugin.Two
|
|
plain-package
|
|
LIST
|
|
|
|
# An indented line belongs to the entry above it and must never be offered on
|
|
# its own, or the menu lists plugins as though they were applications.
|
|
selectable="$(catalog_entries "$bundle_fixture" | wc -l)"
|
|
(( selectable == 3 )) \
|
|
|| note "a category with 3 entries and 2 attached lines offers $selectable choices, not 3"
|
|
catalog_entries "$bundle_fixture" | grep -q 'Plugin.One' \
|
|
&& note 'an indented line is offered as a selectable application'
|
|
|
|
# The label is for the menu and must not survive into an install target.
|
|
catalog_all_targets "$bundle_fixture" | grep -q '|' \
|
|
&& note 'a label reaches the install targets, where it would be treated as a package name'
|
|
catalog_entries "$bundle_fixture" | grep -q "$(printf 'flatpak:com.example.Named\tA Friendly Name')" \
|
|
|| note 'an explicit label is not carried through to the menu'
|
|
|
|
# Ticking a bundle installs the entry and everything attached to it.
|
|
host_targets="$(catalog_targets "$bundle_fixture" "flatpak:com.example.Host" | wc -l)"
|
|
(( host_targets == 3 )) \
|
|
|| note "selecting a bundle resolves to $host_targets targets, not the entry plus its 2 attached lines"
|
|
catalog_targets "$bundle_fixture" "flatpak:com.example.Host" | grep -q '^flatpak:com.example.Host$' \
|
|
|| note 'selecting a bundle does not install the entry itself'
|
|
|
|
# A plain entry stays plain: it must not absorb whatever follows it.
|
|
plain_targets="$(catalog_targets "$bundle_fixture" "plain-package" | wc -l)"
|
|
(( plain_targets == 1 )) \
|
|
|| note "a plain entry resolves to $plain_targets targets rather than just itself"
|
|
|
|
# `panama apps` maps a selection back to an entry by its menu label, because
|
|
# that is all gum returns. Two entries sharing a label would therefore install
|
|
# whichever came first, silently and with no way to pick the other.
|
|
for category in "${categories[@]}"; do
|
|
[[ -f "$category" ]] || continue
|
|
duplicate="$(catalog_entries "$category" | cut -f2 | sort | uniq -d)"
|
|
[[ -z "$duplicate" ]] \
|
|
|| note "$(basename "$category") has more than one entry labelled '$duplicate', which makes the menu ambiguous"
|
|
done
|
|
|
|
# ── 3c. Both front doors, one catalog ────────────────────────────────────────
|
|
#
|
|
# `panama apps` and the interview offer the same applications. They diverge the
|
|
# moment either grows its own parser, and the divergence would be invisible.
|
|
|
|
panama="$repo_dir/bin/panama"
|
|
grep -q 'source "$PANAMA_DIR/setup/lib/extras-catalog"' "$panama" \
|
|
|| note 'panama apps does not read the shared catalog'
|
|
grep -q 'source "$PANAMA_PATH/setup/lib/extras-catalog"' "$installer" \
|
|
|| note 'install-packages does not read the shared catalog'
|
|
grep -qE '^\s*apps\)' "$panama" \
|
|
|| note 'panama does not dispatch an apps subcommand'
|
|
|
|
# ── 4. Every name resolves ───────────────────────────────────────────────────
|
|
#
|
|
# Skipped rather than failed when the repositories cannot be reached, so this
|
|
# contract stays runnable on a train.
|
|
#
|
|
# One bulk query per manager, not one per name. Asking Flathub about forty-five
|
|
# ids individually took minutes and got slower every time an application was
|
|
# added -- a check nobody will wait for is a check that gets commented out.
|
|
|
|
dnf_wanted="$(for category in "${categories[@]}"; do
|
|
[[ -f "$category" ]] && catalog_all_targets "$category" | grep -v '^flatpak:'
|
|
done | sort -u)"
|
|
|
|
flatpak_wanted="$(for category in "${categories[@]}"; do
|
|
[[ -f "$category" ]] && catalog_all_targets "$category" | sed -n 's/^flatpak://p'
|
|
done | sort -u)"
|
|
|
|
if [[ -n "$dnf_wanted" ]] && timeout 60 dnf list --available --quiet bash >/dev/null 2>&1; then
|
|
# repoquery answers for every name at once and simply omits the ones it
|
|
# cannot resolve, so the difference is the finding.
|
|
resolved="$(timeout 180 dnf repoquery --qf '%{name}\n' $dnf_wanted 2>/dev/null | sort -u)"
|
|
while read -r package; do
|
|
[[ -n "$package" ]] || continue
|
|
grep -qx "$package" <<<"$resolved" \
|
|
|| note "$package is named by a category but dnf cannot resolve it"
|
|
done <<<"$dnf_wanted"
|
|
else
|
|
printf 'extras contract: dnf is unreachable, so package names were not resolved\n' >&2
|
|
fi
|
|
|
|
# --all matters: without it, remote-ls hides end-of-life applications, which
|
|
# still resolve and still install. Leaving it off reported yuzu as missing from
|
|
# Flathub when it is merely unmaintained -- a false negative that would have
|
|
# quietly deleted a working entry.
|
|
if [[ -n "$flatpak_wanted" ]] && timeout 120 flatpak remote-ls flathub --columns=application --all >"$work/flathub" 2>/dev/null \
|
|
&& [[ -s "$work/flathub" ]]; then
|
|
while read -r id; do
|
|
[[ -n "$id" ]] || continue
|
|
grep -qx "$id" "$work/flathub" \
|
|
|| note "$id is named by a category but is not on Flathub"
|
|
done <<<"$flatpak_wanted"
|
|
else
|
|
printf 'extras contract: Flathub is unreachable, so flatpak IDs were not resolved\n' >&2
|
|
fi
|
|
|
|
# ── Report ───────────────────────────────────────────────────────────────────
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
|
|
printf 'extras contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'extras contract: PASS (%d categories)\n' "${#categories[@]}"
|