Install the desktop this repository already describes

The shell named a font, a pointer theme and a wallpaper that no package list
installed and no stage placed. It went unnoticed because this machine collected
all three under sunhat and never lost them; a fresh Fedora box would have come
up with tofu for every shell glyph, the default pointer, and no wallpaper --
while Wallpaper.qml called that missing file `shippedPath`.

The dependency contract reported PASS throughout, because it reads commands that
scripts invoke and none of these are one. The new assets contract covers what it
structurally cannot: fonts and pointer themes named in configs and gsettings, a
shipped wallpaper that must exist, commands launched from QML, and directories
nothing puts into service. Written against the broken tree first, where it found
ten faults.

Four of those were packages nobody had noticed were missing -- gnome-calendar,
podman, pipewire-utils and flatpak -- alongside gnome-control-center, which backs
fifteen rows of Panama's own Settings and is commented so it is not mistaken for
GNOME-session residue later.

Fonts turned out to need no install stage at all. Terra, which install-packages
already enables, packages every Nerd Font, so sunhat's wget-and-unzip is replaced
by five lines in a package list. The pointer theme had no such luck: it is
packaged nowhere, so it is vendored rather than downloaded from a URL that can
rot.

espanso stays undeclared. It is in no enabled repository, and building it from
source is the install method this repository is trying to stop using.

Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
This commit is contained in:
Gabriel Brown
2026-08-20 18:57:47 -04:00
parent 6bcc351e07
commit 96e4085919
72 changed files with 341 additions and 31 deletions
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env bash
# Every asset Panama's desktop names must be something Panama installs.
#
# This is the sibling of declared-dependencies-contract.sh, and it exists because
# that contract has a structural blind spot. It checks commands that scripts
# invoke -- so it reports PASS on a machine where the shell's icon font, the
# pointer theme, the wallpaper and four GUI applications are all missing, because
# none of those are a command in a script.
#
# That was not hypothetical. At the time this was written:
#
# * change-settings set monospace-font-name to a Nerd Font that no package list
# installed, and Fonts.qml documents that a mono family without Nerd glyphs
# replaces every icon in the shell with tofu.
# * oreo_blue_cursors was named in six files and installed by none.
# * Wallpaper.qml declared a `shippedPath` that the repository did not ship,
# and panama-lock fell back to the same non-existent file.
# * gnome-control-center backed fifteen rows of Panama's own Settings and was
# declared nowhere, as were gnome-calendar, podman and pipewire-utils.
#
# Every one of those presents as a feature that quietly is not there, which is
# the same failure mode -- and the same argument -- as the dependency contract.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
findings=()
note() { findings+=("$1"); }
declared="$(cat "$repo_dir"/setup/packages/* 2>/dev/null \
| sed 's/#.*//' | tr -d ' ' | grep -v '^$' | sort -u)"
[[ -n "$declared" ]] || { printf 'declared assets contract: no package lists found\n' >&2; exit 1; }
is_declared() { grep -qix -- "$1" <<<"$declared"; }
# ── 1. Fonts ─────────────────────────────────────────────────────────────────
#
# A family name is not a package name. Nerd Font packages follow one rule --
# "VictorMono Nerd Font" is victormono-nerd-fonts -- and everything else needs
# naming, so an unmapped family is reported as a real omission rather than
# silently skipped.
font_package() {
local family="$1"
# Style and width suffixes are not part of the family that ships.
family="${family% Mono}"
family="${family% Propo}"
family="$(sed -E 's/ (Light|Regular|Medium|SemiBold|Bold|Thin|Black|Italic)$//' <<<"$family")"
if [[ "$family" == *" Nerd Font" ]]; then
printf '%s-nerd-fonts' "$(tr '[:upper:]' '[:lower:]' <<<"${family% Nerd Font}" | tr -d ' ')"
return
fi
case "$family" in
"Adwaita Sans"|"Adwaita Mono") printf 'adwaita-sans-fonts' ;;
Cantarell) printf 'abattis-cantarell-fonts' ;;
*) printf '?%s' "$family" ;;
esac
}
# Sources are named individually rather than crawled, because a font family is a
# bare string: crawling for one finds every English phrase in the repository.
fonts="$(
grep -hoE "font-name '[^']+'" "$repo_dir"/setup/scripts/* 2>/dev/null \
| sed -E "s/.*'(.*)'/\1/"
grep -hoE '^gtk-font-name=.*' "$repo_dir"/config/dot/gtk-""*/settings.ini.template 2>/dev/null \
| sed 's/^gtk-font-name=//'
grep -hoE '^font-family = ".*"' "$repo_dir"/config/dot/ghostty/config 2>/dev/null \
| sed -E 's/^font-family = "(.*)"/\1/'
grep -hoE '^\s*font_family = .*' "$repo_dir"/config/dot/hypr/hyprlock.conf.template 2>/dev/null \
| sed -E 's/^\s*font_family = //'
grep -hoE 'key: "(interfaceFont|iconFont|monospaceFont|documentFont|applicationFont)", type: "string", def: "[^"]+"' \
"$repo_dir"/config/dot/quickshell/config/PreferenceSchema.qml 2>/dev/null \
| sed -E 's/.*def: "([^"]+)".*/\1/'
)"
while IFS= read -r family; do
[[ -n "$family" ]] || continue
# Trailing point size, as gsettings and GTK both write it.
family="$(sed -E 's/ [0-9]+$//' <<<"$family")"
package="$(font_package "$family")"
if [[ "$package" == \?* ]]; then
note "font family '${package#?}' is named in the desktop but maps to no known package"
elif ! is_declared "$package"; then
note "font '$family' needs package '$package', which no package list declares"
fi
done < <(sort -u <<<"$fonts")
# ── 2. Cursor themes ─────────────────────────────────────────────────────────
#
# Satisfied either by a package or by a theme vendored into the repository --
# oreo_blue_cursors is packaged by no Fedora repository, so it ships here.
cursors="$(
grep -hoE "cursor-theme '[^']+'" "$repo_dir"/setup/scripts/* 2>/dev/null \
| sed -E "s/.*'(.*)'/\1/"
grep -hoE '^gtk-cursor-theme-name=.*' "$repo_dir"/config/dot/gtk-""*/settings.ini.template 2>/dev/null \
| sed 's/^gtk-cursor-theme-name=//'
grep -hoE '^export XCURSOR_THEME=.*' "$repo_dir"/config/dot/uwsm/env 2>/dev/null \
| sed 's/^export XCURSOR_THEME=//'
grep -hoE 'key: "cursorTheme", type: "string", def: "[^"]+"' \
"$repo_dir"/config/dot/quickshell/config/PreferenceSchema.qml 2>/dev/null \
| sed -E 's/.*def: "([^"]+)".*/\1/'
)"
while IFS= read -r theme; do
[[ -n "$theme" ]] || continue
[[ -d "$repo_dir/config/local/share/icons/$theme/cursors" ]] && continue
is_declared "${theme%%_*}-cursor-theme" && continue
is_declared "$theme" && continue
note "cursor theme '$theme' is named in the desktop but is neither vendored nor packaged"
done < <(sort -u <<<"$cursors")
# ── 3. The shipped wallpaper ─────────────────────────────────────────────────
#
# Wallpaper.qml calls one path `shippedPath` and panama-lock falls back to it.
# A fallback that does not exist is not a fallback.
while IFS= read -r image; do
[[ -n "$image" ]] || continue
[[ -f "$repo_dir/config/wallpapers/$image" ]] \
|| note "wallpaper '$image' is referenced as shipped, but config/wallpapers/$image does not exist"
done < <(grep -rhoE 'Pictures/Wallpapers/[A-Za-z0-9._-]+\.(jpg|jpeg|png|webp)' \
"$repo_dir"/config/dot/quickshell/services "$repo_dir"/config/dot/quickshell/scripts \
"$repo_dir"/config/dot/hypr 2>/dev/null | sed 's|.*/||' | sort -u)
# ── 3b. Shipping is not installing ───────────────────────────────────────────
#
# A vendored asset that link-dotfiles never puts into place is exactly as absent
# as one that was never vendored -- and it would satisfy the two checks above,
# which only prove the repository contains the file. Both directories must
# actually be consumed by the installer.
for vendored in config/local/share/icons config/wallpapers; do
[[ -e "$repo_dir/$vendored" ]] || continue
grep -qF "$vendored" "$repo_dir/setup/scripts/link-dotfiles" 2>/dev/null \
|| note "$vendored is shipped but link-dotfiles never installs it, so a fresh machine still does without"
done
# ── 4. Commands the shell launches ───────────────────────────────────────────
#
# The dependency contract reads scripts. These are launched from QML, so it
# never sees them -- which is how the application backing fifteen Settings rows
# went undeclared.
qml_package() {
case "$1" in
hyprctl) printf 'hyprland' ;;
wl-copy|wl-paste) printf 'wl-clipboard' ;;
xdg-open) printf 'xdg-utils' ;;
pw-dump|pw-play) printf 'pipewire-utils' ;;
*) printf '%s' "$1" ;;
esac
}
# Provided by the base system or the shell itself; nothing installs these
# separately, and listing them would be noise.
QML_BASELINE='^(sh|bash|rm|test|systemd-inhibit|loginctl|timedatectl|gsettings|gapplication|systemctl|busctl)$'
while IFS= read -r command_name; do
[[ -n "$command_name" ]] || continue
[[ "$command_name" =~ $QML_BASELINE ]] && continue
package="$(qml_package "$command_name")"
is_declared "$package" \
|| note "the shell launches '$command_name' from QML, but '$package' is undeclared"
done < <(grep -rhoE '(command: |execDetached\(|command = )\["[a-z][a-z0-9._-]*"' \
"$repo_dir"/config/dot/quickshell --include='*.qml' 2>/dev/null \
| sed -E 's/.*\["([^"]*)"/\1/' | sort -u)
# ── 5. Directories nothing puts into service ─────────────────────────────────
#
# A config directory nobody links is a directory nobody has. This catches the
# case where a new dot/ directory is added and link-dotfiles' `dirs` array is not
# updated, which fails silently -- the config is simply never in place.
#
# Linking is not the only way to be used: ohmyposh is loaded straight out of the
# repository by config/bash/shell, which needs no symlink at all. So the shell
# configuration counts as a consumer too, and only a directory nothing at all
# refers to is reported.
consumers=("$repo_dir/setup/scripts/link-dotfiles" "$repo_dir"/config/bash/*)
for dot_dir in "$repo_dir"/config/dot/*/; do
name="$(basename "$dot_dir")"
grep -qE "(^|[^A-Za-z0-9_-])$name([^A-Za-z0-9_-]|$)" "${consumers[@]}" 2>/dev/null \
|| note "config/dot/$name is referenced by neither link-dotfiles nor the shell configuration, so nothing puts it into service"
done
# ── Report ───────────────────────────────────────────────────────────────────
# One asset named by three different files is one finding, not three.
if (( ${#findings[@]} > 0 )); then
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
printf 'declared assets contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'declared assets contract: PASS\n'