Files
Panama/tests/setup/package-lists-contract
Gabriel Brown b319d1a5e1 Stop handing dnf the comments that explain the package lists
Every list in setup/packages/ is annotated -- which package exists for which
settings page, why an exception was made -- and install-packages passed the whole
file to dnf, comment lines included.

dnf does not ignore an argument it cannot match. It reports "No match for
argument: #" and exits 1, and with set -euo pipefail at the top of that script
the first annotated list ends the stage. initial-packages carries four comments
and is the first list read, so a fresh machine got the repositories, the group
updates, and then nothing.

Two things hid it. On a machine that already has everything, a re-run matches
every real name and fails only on the comments, so the failure looks like noise
rather than the stage dying. And every contract that reads these lists strips
comments with sed before comparing -- the tests were reading a file the installer
was not, which is why a repository with a dependency contract, an assets contract
and a doctor still reported PASS across the board.

The fix is one filter used at all five call sites. The contract lifts that filter
out of the script and runs it, rather than describing what it should do, so
deleting or renaming it fails here instead of passing quietly. It also checks the
inverse -- that stripping comments does not strip packages -- because trading a
loud failure for a silent one would be worse than the bug.

Found while adding the extras lists for phase 4, which are annotated the same way
and would have hit the same wall.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 21:11:44 -04:00

109 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# The comments in a package list are for the reader, not for dnf.
#
# Every list in setup/packages/ is annotated -- which package exists for which
# settings page, why an exception was made -- and install-packages passed the
# whole file to dnf, comment lines included. dnf does not ignore an argument it
# cannot match; it reports "No match for argument: #" and exits 1. With
# `set -euo pipefail` at the top of that script, the first annotated list ends
# the stage, and a fresh machine gets almost no packages.
#
# It could not show up here. This machine has everything already, so a re-run
# matches every real name and only fails on the comments; and every contract
# that reads these lists strips comments with sed before comparing, so the tests
# were reading a file the installer was not. That is the exact shape of bug this
# repository's fresh-install work exists to find: invisible on the machine it
# was written on, fatal on the next one.
#
# The check runs the installer's own extraction rather than describing it, so
# renaming or deleting the filter fails here instead of silently passing.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
installer="$repo_dir/setup/scripts/install-packages"
lists_dir="$repo_dir/setup/packages"
findings=()
note() { findings+=("$1"); }
# ── The installer's own filter, lifted out and run ───────────────────────────
filter="$(sed -n '/^packages_in()/,/^}/p' "$installer")"
if [[ -z "$filter" ]]; then
printf 'package lists contract: install-packages defines no packages_in filter, so it reads lists raw\n' >&2
exit 1
fi
eval "$filter"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
fixture="$work/list"
cat >"$fixture" <<'LIST'
# A whole-line comment, which is what broke the install.
alpha
bravo
#Indented and unspaced forms of the same thing.
# spaced
charlie
LIST
read -ra extracted <<<"$(packages_in "$fixture")"
expected=(alpha bravo charlie)
if [[ "${extracted[*]}" != "${expected[*]}" ]]; then
note "a commented list extracts as '${extracted[*]}' rather than '${expected[*]}'"
fi
for token in "${extracted[@]}"; do
[[ "$token" == *"#"* ]] && note "the extracted list still carries a comment marker: $token"
done
# ── Every real list survives it ──────────────────────────────────────────────
#
# Stripping comments must not also strip packages. Each name that is not a
# comment has to come out the other side, or this fix trades a loud failure for
# a quiet one.
shopt -s nullglob
for list in "$lists_dir"/*; do
[[ -f "$list" ]] || continue
name="$(basename "$list")"
# What a reader would say the file declares.
mapfile -t declared < <(sed 's/#.*//' "$list" | tr -d ' \t' | grep -v '^$' | sort)
read -ra passed <<<"$(packages_in "$list")"
mapfile -t passed_sorted < <(printf '%s\n' "${passed[@]}" | sort)
if [[ "${declared[*]}" != "${passed_sorted[*]}" ]]; then
note "$name declares ${#declared[@]} packages but the installer would pass ${#passed_sorted[@]}"
fi
for token in "${passed[@]}"; do
[[ "$token" == *"#"* ]] && note "$name would pass a comment marker to the package manager"
done
done
# ── No list is read raw any more ─────────────────────────────────────────────
#
# The filter existing is not the same as it being used. A single missed call
# site is a stage that still dies on the list it forgot.
if grep -nE 'tr "\\n" " " *<' "$installer" >/dev/null; then
note 'a package list is still read with tr rather than through packages_in'
fi
# ── Report ───────────────────────────────────────────────────────────────────
if (( ${#findings[@]} > 0 )); then
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
printf 'package lists contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'package lists contract: PASS\n'