116 lines
5.2 KiB
Bash
Executable File
116 lines
5.2 KiB
Bash
Executable File
#!/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"); }
|
|
|
|
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[@]}"
|