#!/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'