#!/usr/bin/env bash # The applications built from source, and the standard they have to meet. # # Every other application Panama installs comes from dnf or Flathub. These do # not, and the rule for admitting one is not "it was convenient": there has to # be no packaged form, the reason has to be written down, and what lands on the # system still has to be a package the system owns. # # The rules: # # 1. Every definition declares a repository, a description, and a build. # A file missing any of them is an entry that fails only when somebody # asks for it, which is the worst moment to find out. # 2. Every definition states why the exception exists. This is the whole # guard against the list growing by habit -- sunhat had seventy-odd # installers and no reason recorded for any of them. # 3. Nothing is pinned. A recorded version is a 404 waiting to happen: every # pinned URL sunhat carried had rotted within a release cycle, which is the # argument this repository's package rule is built on. # 4. `panama app` lists what the directory holds and refuses what it does not. # # Definitions are read, not run. Building one downloads an upstream release and # installs a package, which is not something a test suite does. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" apps_dir="$repo_dir/setup/apps" panama="$repo_dir/bin/panama" findings=() note() { findings+=("$1"); } [[ -d "$apps_dir" ]] || { printf 'apps contract: no %s\n' "$apps_dir" >&2; exit 1; } shopt -s nullglob definitions=("$apps_dir"/*) # ── 1 & 2. Each definition is complete, and says why it exists ─────────────── for definition in "${definitions[@]}"; do name="$(basename "$definition")" [[ -f "$definition" ]] || { note "$name is not a file"; continue; } # Sourced in a subshell so one definition cannot leak into the next, and so # a definition that runs something at source time is contained. problems="$( description="" repo="" unset -f build 2>/dev/null || true # shellcheck source=/dev/null source "$definition" >/dev/null 2>&1 [[ -n "$description" ]] || { printf 'no-description\n'; exit 0; } [[ -n "$repo" ]] || { printf 'no-repo\n'; exit 0; } declare -F build >/dev/null || { printf 'no-build\n'; exit 0; } [[ "$repo" == https://* ]] || { printf 'insecure-repo\n'; exit 0; } )" # Read back through a here-string rather than a pipe: a `while read` on the # right of a pipe runs in a subshell, and every finding it recorded was # being discarded at the end of the loop. Caught by standing in a broken # definition and watching two of the three checks stay silent. while read -r problem; do [[ -n "$problem" ]] || continue case "$problem" in no-description) note "$name has no description, so it cannot be listed" ;; no-repo) note "$name declares no repository" ;; no-build) note "$name declares no build function" ;; insecure-repo) note "$name is cloned over something other than https" ;; esac done <<<"$problems" # The comment block is the reason. A definition without one is an entry # somebody added because it was easy. reason="$(grep -c '^#' "$definition")" (( reason >= 3 )) \ || note "$name records no reason for being a source build rather than a package" # ── 3. Nothing pinned ─────────────────────────────────────────────────── if grep -qE 'git (checkout|clone).*(-b|--branch|--tag)|checkout [0-9a-f]{7,40}|v[0-9]+\.[0-9]+\.[0-9]+' "$definition"; then note "$name looks like it pins a version or tag, which is what goes stale" fi done # ── 4. The command agrees with the directory ──────────────────────────────── listing="$("$panama" app 2>&1)" for definition in "${definitions[@]}"; do [[ -f "$definition" ]] || continue grep -q "$(basename "$definition")" <<<"$listing" \ || note "$(basename "$definition") is not listed by 'panama app'" done "$panama" app definitely-not-an-app >/dev/null 2>&1 \ && note "'panama app' accepts a name that has no definition" # The build tree belongs in the cache: it is entirely rebuildable, and a # checkout kept beside the repository would eventually be mistaken for one. grep -q 'XDG_CACHE_HOME' "$panama" \ || note 'application checkouts are not placed under the cache directory' # Not part of the unattended run, for the reason the interview exists. grep -q 'app)' "$repo_dir/install" \ && note 'the installer runs a source build, which cannot be walked away from' # ── Report ─────────────────────────────────────────────────────────────────── if (( ${#findings[@]} > 0 )); then mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u) printf 'apps contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2 exit 1 fi printf 'apps contract: PASS (%d applications)\n' "${#definitions[@]}"