Build the two applications nobody packages, on purpose rather than in passing
Claude Desktop and ChatGPT Desktop ship for macOS and Windows. The Linux path for both is a community wrapper that converts the official build into an RPM -- so what lands is still a package dnf owns and can remove, which is the part of the dnf/flatpak rule that actually matters. What they need an exception for is the build itself, and there is no packaged form to prefer over it. `panama app` builds one by name, and is deliberately not part of ./install. A source build is slow, wants the network throughout, and depends on an upstream that moves -- twenty minutes in, an error, with nobody at the keyboard, which is the exact failure the interview exists to prevent. Asking for one is something you do on purpose, and it is also the rebuild path when a new version ships. Nothing is pinned. Each build takes the current default branch and the current upstream release, and reports a failure rather than working around it, leaving the tree where the error can be read. sunhat pinned versions and every pin was a 404 within a release cycle. Adding one is adding a file to setup/apps/, and the file has to say why the exception exists -- the contract fails a definition that does not, because the guard against this list growing by habit is having to write the reason down. sunhat had seventy-odd installers and a reason recorded for none of them. The contract had a bug worth recording: `while read` on the right of a pipe runs in a subshell, so two of its three per-definition checks recorded findings into an array that went out of scope at the end of the loop. It reported PASS on a definition with no description and no build function. Found by standing one in deliberately and noticing only the third check spoke up. Also: nautilus-open-any-terminal is now declared, and Panama's copy of the extension is gone. Fedora packages that extension AND its gsettings schema, and Panama shipped its own fork of the .py over the same path while declaring neither -- so a fresh machine got an extension whose schema did not exist. It worked here only because the RPM has been installed since sunhat. The fork was also 63 lines behind the packaged version, missing its newer Nautilus and Caja handling. Auditing the rest of config/copy for the same shape found nothing else: dnf.conf is a config file its package expects to be replaced, and the GPU udev rules are Panama's own. 125 contracts pass. Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
This commit is contained in:
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/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[@]}"
|
||||
Reference in New Issue
Block a user