Merge remote-tracking branch 'origin/main' into feat/panama-health
# Conflicts: # config/dot/quickshell/modules/settings/HealthPage.qml # tests/quickshell/health-ui-contract.sh
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
#!/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 colour 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' ;;
|
||||
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"
|
||||
@@ -68,6 +68,24 @@ rg -Fq 'SystemSettings.openGnomePanel("color")' "$settings_dir/HealthPage.qml" \
|
||||
|| fail 'Fedora ownership boundary lost the Colour profiles handoff'
|
||||
rg -Fq 'SystemSettings.openGnomePanel("wellbeing")' "$settings_dir/HealthPage.qml" \
|
||||
|| fail 'Fedora ownership boundary lost the Digital wellbeing handoff'
|
||||
# Exact authored handoffs are asserted above. Also prove every panel named by
|
||||
# this boundary is accepted by SystemSettings, so a typo cannot ship a dead
|
||||
# button even if its copy still looks correct.
|
||||
rg -Fq 'title: "Fedora system settings"' "$settings_dir/HealthPage.qml" \
|
||||
|| fail 'the Fedora ownership boundary card is gone'
|
||||
|
||||
allowed="$(rg -o '"[a-z-]+"' "$repo_dir/config/dot/quickshell/services/SystemSettings.qml" \
|
||||
| sed -n '/"\(applications\|background\|bluetooth\|color\|display\|keyboard\|mouse\|multitasking\|network\|notifications\|online-accounts\|power\|printers\|privacy\|search\|sharing\|sound\|system\|universal-access\|wacom\|wellbeing\|wifi\|wwan\)"/p' \
|
||||
| tr -d '"' | sort -u)"
|
||||
|
||||
while read -r panel; do
|
||||
[[ -n "$panel" ]] || continue
|
||||
grep -qx "$panel" <<<"$allowed" \
|
||||
|| fail "the Fedora card opens \"$panel\", which openGnomePanel does not allow -- that button does nothing"
|
||||
done < <(rg -o 'openGnomePanel\("([a-z-]+)"' -r '$1' "$settings_dir/HealthPage.qml" | sort -u)
|
||||
|
||||
rg -q 'openGnomePanel\(' "$settings_dir/HealthPage.qml" \
|
||||
|| fail 'the Fedora ownership boundary does not open GNOME Settings at all'
|
||||
rg -Fq 'Health.repair(check.id, false)' "$settings_dir/HealthPage.qml" \
|
||||
|| fail 'Settings repair does not stay inline/non-external'
|
||||
rg -Fq 'ShellState.openSettings(check.action.target)' "$settings_dir/HealthPage.qml" \
|
||||
|
||||
@@ -25,8 +25,16 @@ trap cleanup EXIT
|
||||
if rg -q 'setup/scripts/link-vicinae-scripts' "$dotfile_installer"; then
|
||||
fail 'link-dotfiles also invokes the command installer'
|
||||
fi
|
||||
rg -Fq 'do "$script"; done' "$top_level_installer" \
|
||||
|| fail 'top-level installer sources setup scripts into one shared shell'
|
||||
# Each setup stage must run in its OWN process, so strict-shell options and
|
||||
# helper variables stay local to the script that owns them. What matters is
|
||||
# that the stages are executed rather than sourced -- this previously matched
|
||||
# the literal one-liner `do "$script"; done`, which failed the moment the loop
|
||||
# gained error reporting and spanned more than one line, despite the property
|
||||
# it cares about being unchanged.
|
||||
rg -q '(^|[^a-z-])(\.|source)\s+[^;]*setup/scripts' "$top_level_installer" \
|
||||
&& fail 'top-level installer sources setup scripts into one shared shell'
|
||||
rg -q '"\$script"' "$top_level_installer" \
|
||||
|| fail 'top-level installer does not execute the setup scripts'
|
||||
|
||||
mkdir -p "$data_dir/scripts/panama" "$fake_bin"
|
||||
printf 'user-owned\n' >"$data_dir/scripts/panama/keep.sh"
|
||||
|
||||
@@ -137,10 +137,20 @@ last_error="$(qs_for_harness ipc call settings-system-test status | jq -r .lastE
|
||||
|
||||
# ── A rejected value must be refused, not silently accepted ──────────────────
|
||||
qs_for_harness ipc call settings-system-test apply "$target_auto_hdr" 7 "$target_direct" >/dev/null
|
||||
sleep 0.3
|
||||
|
||||
# The refusal is reported asynchronously, so wait for it rather than sleeping a
|
||||
# fixed 0.3s and hoping. That sleep made this fail roughly one run in three,
|
||||
# reporting "a rejected value did not surface an error" when the error simply
|
||||
# had not arrived yet -- which reads as a missing guard rather than a slow one.
|
||||
rejected=""
|
||||
for _ in $(seq 1 60); do
|
||||
rejected="$(qs_for_harness ipc call settings-system-test status | jq -r .lastError)"
|
||||
[[ -n "$rejected" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
[[ "$(read_option misc:vrr)" == "$target_vrr" ]] || fail 'an out-of-allow-list VRR value reached the compositor'
|
||||
[[ -n "$(qs_for_harness ipc call settings-system-test status | jq -r .lastError)" ]] \
|
||||
|| fail 'a rejected VRR value did not surface an error'
|
||||
[[ -n "$rejected" ]] || fail 'a rejected VRR value did not surface an error'
|
||||
|
||||
# ── Every getoption answer shape must be handled, not just integers ──────────
|
||||
# The compositor reports each option in a different JSON field depending on its
|
||||
|
||||
Reference in New Issue
Block a user