Seeding is deliberately conservative: a role whose preferred applications are all missing is left alone rather than forced. That is right, and it is also silent -- so a curated handler nobody installs presents as the machine quietly going back to deciding defaults by installation order, which is the problem seeding exists to fix. None of Loupe, Papers, Decibels, Nautilus, mpv or sushi was declared anywhere. Preview works, on org.gnome.NautilusPreviewer2 rather than the interface its bus name suggests, but it opened tiled -- shoving the file manager aside for something meant to be an overlay -- so it gets a float and center rule sized to leave the file underneath visible. xdg-utils and desktop-file-utils were undeclared too; seeding from link-dotfiles started calling them, and the dependency contract said so. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
125 lines
6.1 KiB
Bash
Executable File
125 lines
6.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Every external command Panama's own scripts invoke must be installed by
|
|
# Panama's own package lists.
|
|
#
|
|
# This exists because the lists had drifted badly. jq is used by thirty-one call
|
|
# sites across the helpers and the contracts; kitty has a full shipped config
|
|
# and a dock pin; tmux and btop have shipped themes that the color scheme
|
|
# switches. None of the four were declared. So a fresh machine that followed
|
|
# this repository's own install instructions would not have them.
|
|
#
|
|
# The failure is quiet by design, which is what makes it worth a test: the
|
|
# helpers are written to report "not installed" rather than crash, so a missing
|
|
# dependency presents as a feature that silently is not there.
|
|
#
|
|
# Commands from coreutils and the shell itself are not checked -- nothing
|
|
# installs those separately, and listing them would be noise.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
|
fail() {
|
|
printf 'declared dependencies contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Shell syntax and builtins. These are not commands anyone installs, and the
|
|
# first version of this contract reported `then`, `esac` and `done` as missing
|
|
# packages, which buried the four real findings in a hundred lines of noise.
|
|
SHELL_WORDS='^(if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|time|coproc|break|continue|return|exit|local|readonly|declare|export|unset|shift|eval|exec|source|trap|set|shopt|alias|unalias|builtin|command|enable|help|let|read|mapfile|printf|echo|test|true|false|wait|jobs|bg|fg|kill|pwd|cd|dirs|pushd|popd|umask|type|hash|getopts|split|sync)$'
|
|
|
|
# Provided by any Fedora install: coreutils, util-linux, the shell, and the
|
|
# systemd/session tooling. Nothing here is a choice Panama makes.
|
|
BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|lsof)$'
|
|
|
|
SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$'
|
|
|
|
declared="$(cat "$repo_dir"/setup/packages/* 2>/dev/null | sed 's/#.*//' | tr -d ' ' | grep -v '^$' | sort -u)"
|
|
[[ -n "$declared" ]] || fail 'no package lists found'
|
|
|
|
# A package is not always named after its command. Only the genuine mismatches
|
|
# are mapped, so an unmapped command is a real omission rather than a lookup
|
|
# failure.
|
|
package_for() {
|
|
case "$1" in
|
|
zbarimg) printf 'zbar' ;;
|
|
fc-list|fc-match) printf 'fontconfig' ;;
|
|
lspci) printf 'pciutils' ;;
|
|
getenforce) printf 'libselinux-utils' ;;
|
|
nmcli) printf 'NetworkManager' ;;
|
|
wpctl) printf 'wireplumber' ;;
|
|
nvim) printf 'neovim' ;;
|
|
fwupdmgr) printf 'fwupd' ;;
|
|
dnf4) printf 'python3-dnf' ;;
|
|
notify-send) printf 'libnotify' ;;
|
|
wl-copy|wl-paste) printf 'wl-clipboard' ;;
|
|
rg) printf 'ripgrep' ;;
|
|
xdg-mime|xdg-settings|xdg-open) printf 'xdg-utils' ;;
|
|
update-desktop-database|desktop-file-validate) printf 'desktop-file-utils' ;;
|
|
python3) printf 'python3' ;;
|
|
*) printf '%s' "$1" ;;
|
|
esac
|
|
}
|
|
|
|
missing=()
|
|
checked=0
|
|
|
|
while read -r script; do
|
|
[[ -n "$script" ]] || continue
|
|
head -1 "$script" | grep -qE 'bash|/sh' || continue
|
|
|
|
# Commands appearing at the start of a statement or after a pipe. Crude, but
|
|
# it is looking for undeclared dependencies, not building a call graph.
|
|
#
|
|
# No minimum length. An earlier version required three characters, which
|
|
# quietly excluded the most-used dependency in the repository -- jq, at
|
|
# thirty-one call sites -- along with rg, ss and ip. A dependency checker
|
|
# with a blind spot for short names is worse than none, because it reports
|
|
# PASS.
|
|
while read -r cmd; do
|
|
[[ -n "$cmd" ]] || continue
|
|
[[ "$cmd" =~ $SHELL_WORDS ]] && continue
|
|
[[ "$cmd" =~ $BASELINE ]] && continue
|
|
[[ "$cmd" =~ $SESSION ]] && continue
|
|
|
|
pkg="$(package_for "$cmd")"
|
|
grep -qx "$pkg" <<<"$declared" && continue
|
|
|
|
# Only report a command that actually exists on this machine. An
|
|
# invented name in a comment or a heredoc is a false positive; a real
|
|
# binary that nothing declares is the thing being looked for.
|
|
command -v "$cmd" >/dev/null 2>&1 || continue
|
|
|
|
missing+=("$cmd (from $(basename "$script"), package: $pkg)")
|
|
done < <({
|
|
# Statement-initial or after a pipe.
|
|
grep -oE '(^|[|;&]|\$\()[[:space:]]*[a-z][a-z0-9_-]+' "$script" \
|
|
| grep -oE '[a-z][a-z0-9_-]+$'
|
|
|
|
# Behind a wrapper. ddcutil is always invoked as `timeout 10 ddcutil`,
|
|
# so it never appears statement-initial and was missed entirely.
|
|
grep -oE '\b(timeout[[:space:]]+[0-9.]+|sudo|nohup|env)[[:space:]]+[a-z][a-z0-9_-]+' "$script" \
|
|
| grep -oE '[a-z][a-z0-9_-]+$'
|
|
|
|
# `command -v X` is how these helpers probe for a tool before using it,
|
|
# which makes it the clearest possible statement of a dependency.
|
|
grep -oE 'command -v[[:space:]]+[a-z][a-z0-9_-]+' "$script" \
|
|
| grep -oE '[a-z][a-z0-9_-]+$'
|
|
} | sort -u)
|
|
|
|
checked=$((checked + 1))
|
|
done < <(find "$repo_dir/config/dot/quickshell/scripts" \
|
|
"$repo_dir/config/local/share/vicinae/scripts" \
|
|
"$repo_dir/setup/scripts" "$repo_dir/bin" \
|
|
-type f 2>/dev/null)
|
|
|
|
if (( ${#missing[@]} > 0 )); then
|
|
printf 'declared dependencies contract: commands used but never installed:\n' >&2
|
|
printf ' %s\n' "${missing[@]}" | sort -u >&2
|
|
fail 'add each to a list in setup/packages/, or the feature silently will not exist on a fresh machine'
|
|
fi
|
|
|
|
printf 'declared dependencies contract: PASS (%d scripts)\n' "$checked"
|