Files
Panama/install
T
Gabriel Brown 7a5e990439 Let somebody extend this without forking it, and say when things die
Two of Section F.

Hooks are the pressure valve. "Can Panama also do X when the theme
changes" is now a five-line file in ~/.config/panama/hooks rather than
a fork, a feature request, or a patch somebody rebases forever. Each
name takes a single file and a .d directory so several things can react
without fighting over one, and a broken hook is reported and stepped
over: somebody's script must never cost a theme change, an upgrade or a
login. Wired at theme-set, post-upgrade and post-migrate. This is the
thirty-line version of the plugin host the upstream ledger defers, and
it has no API to keep stable beyond "we will run your script and tell
you what happened".

Testing it caught a real bug the reading would not have: run_one
captured the script path but never shifted it off, so every hook got
its own filename as $1 and the real arguments arrived one place late. A
hook reading $1 as the colour scheme got a path.

The crash watcher notices when a program dumps core and says so. Under
GNOME, ABRT does this; here nothing did, and applications died silently,
which is most of how "Linux is flaky" gets earned.

Once per program per session is the entire design, not a nicety. This
machine's portal backend crashes between eleven and sixty times a day,
and a notification per crash would be one every few minutes for
something nobody can act on. The first is news; the fortieth is why
people turn notifications off. The health page keeps the running count.

It waits for the notification server before reporting, because the
crash most worth hearing about is the one that took the shell with it,
and it names the executable rather than the kernel's comm field, which
truncates at fifteen characters. Verified against real segfaults.
2026-08-22 08:11:49 -04:00

160 lines
7.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Panama's installer. Safe to re-run: every stage is idempotent, and this is
# also the upgrade path.
set -uo pipefail
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
source "$PANAMA_PATH/bin/ascii"
# ── The interview ────────────────────────────────────────────────────────────
#
# Everything Panama needs to be told is asked here, before a single package is
# installed, and nothing asks again afterwards. That is the whole bargain: the
# rest of the run takes twenty minutes and needs nobody watching it.
#
# gum is bootstrapped first because the interview is built on it and it cannot
# install itself -- it is declared in initial-packages, which install-packages
# installs, which runs after this. One small dnf call buys a real interface for
# the only part of the install a person actually interacts with.
if ! command -v gum >/dev/null 2>&1; then
echo "Installing gum, which the setup questions are built on"
sudo dnf install -y gum >/dev/null || {
echo "Could not install gum, so the setup questions cannot be asked." >&2
exit 1
}
fi
# ── Keep the machine awake for the duration ──────────────────────────────────
# Package installation takes long enough to hit an idle lock, and being locked
# out mid-transaction is unpleasant. Restored on every exit path, including
# failure and Ctrl-C, so an interrupted install does not leave the screen
# permanently awake.
cleanup() {
gsettings set org.gnome.desktop.screensaver lock-enabled true 2>/dev/null || true
gsettings set org.gnome.desktop.session idle-delay 300 2>/dev/null || true
# Deleted on every exit path, including Ctrl-C. The answers are transient by
# design, and one of them is an email address.
[[ -n "${PANAMA_ANSWERS:-}" ]] && rm -f "$PANAMA_ANSWERS"
}
trap cleanup EXIT
# A bare `trap cleanup INT` is not an abort: bash runs the handler and then
# carries on with the script, so Ctrl-C would kill only the current stage and
# the remaining ones -- MOK enrollment, firmware -- would still run. Exit
# explicitly instead; the EXIT trap above does the actual cleanup.
trap 'exit 130' INT
trap 'exit 143' TERM
gsettings set org.gnome.desktop.screensaver lock-enabled false 2>/dev/null || true
gsettings set org.gnome.desktop.session idle-delay 0 2>/dev/null || true
# ── Stages ───────────────────────────────────────────────────────────────────
# Each runs in its own process so strict-shell options and helper variables stay
# local to the script that owns them. A failing stage is reported and the rest
# still run: a missing optional package should not stop the dotfiles being
# linked. The summary at the end is what decides whether the install worked,
# because a failure scrolled past twenty minutes ago is a failure nobody saw.
#
# Explicit order, not glob order: change-settings runs `vicinae theme set`,
# which needs both vicinae itself (installed by install-packages) and the
# theme files it selects among (symlinked into place by link-dotfiles);
# setup-identity needs the gh and git-all that install-packages provides; and
# install-hardware is last because MOK enrollment arms a prompt consumed at the
# next boot and a firmware update may ask for a reboot -- a machine that reboots
# out of the final stage has already been completely configured. New scripts
# must be added here explicitly, or they will not run at all.
# The interview is not in that list, because it is the one stage whose output the
# installer reads back -- and because declining it must stop everything rather
# than be recorded as one failure among several.
#
# The answers live for exactly one run. There is no state file to go stale and
# nothing personal reaches a durable path, which is what keeps this repository
# something somebody else could clone. Created here rather than earlier so the
# trap that deletes it is already armed before the file exists.
PANAMA_ANSWERS="$(mktemp -t panama-answers.XXXXXX)"
export PANAMA_ANSWERS
if ! "$PANAMA_PATH/setup/scripts/interview"; then
exit 1
fi
# shellcheck source=/dev/null
source "$PANAMA_ANSWERS"
export PANAMA_HOSTNAME PANAMA_GIT_NAME PANAMA_GIT_EMAIL PANAMA_GIT_EDITOR \
PANAMA_GH_LOGIN PANAMA_SSH_KEY PANAMA_NVIDIA PANAMA_MOK_HASH \
PANAMA_DEBLOAT PANAMA_FIRMWARE PANAMA_EXTRAS
# Applied here rather than in a stage, and applied early: it needs sudo, and
# sudo is warm right now. At the end of a long unattended run the timestamp has
# expired, and a password prompt then is exactly the interruption the interview
# exists to prevent.
if [[ -n "${PANAMA_HOSTNAME:-}" ]]; then
sudo hostnamectl set-hostname "$PANAMA_HOSTNAME"
echo "Hostname set to: $(hostname)"
fi
STAGES=(install-packages link-dotfiles change-settings link-vicinae-scripts setup-identity install-hardware)
failed=()
for stage in "${STAGES[@]}"; do
script="$PANAMA_PATH/setup/scripts/$stage"
[[ -x "$script" ]] || continue
printf '\n=== %s ===\n' "$stage"
if ! "$script"; then
failed+=("$stage")
printf '!!! %s failed\n' "$stage" >&2
fi
done
# ── Migrations ───────────────────────────────────────────────────────────────
#
# Repairs for machines that installed an older Panama: removing a file this
# repository stopped shipping, disabling a unit it stopped wanting. The
# installer itself cannot do any of that, because it only ever adds.
#
# A machine that has never seen migrations before is one of two things, and
# the difference matters. If it has no marker directory at all it was just
# built from THIS checkout, so every repair those migrations describe is
# already true of it -- they are marked applied without running, exactly as
# Migrations.qml stamps a pre-versioning settings file at its baseline rather
# than replaying upgrades it never needed. Otherwise the pending ones run.
migrate="$PANAMA_PATH/bin/panama-migrate"
if [[ -x "$migrate" ]]; then
printf '\n=== migrations ===\n'
if [[ -d "${XDG_STATE_HOME:-$HOME/.local/state}/panama/migrations" ]]; then
"$migrate" run || failed+=(migrations)
else
"$migrate" --baseline || true
fi
fi
# ── Did it actually work? ────────────────────────────────────────────────────
#
# A failed-stage count only reports what exited non-zero. It says nothing about a
# service that did not start or a font that did not land, and those are the
# failures that survive an install unnoticed. Doctor answers the question the
# stage list cannot.
#
# It never changes the exit code. On a fresh machine it legitimately reports
# things as unconfigured -- no Home Assistant token yet, Nextcloud not signed in
# -- and failing an install over those would be crying wolf.
doctor="$PANAMA_PATH/config/dot/quickshell/scripts/panama-doctor"
if [[ -x "$doctor" ]]; then
printf '\n=== health ===\n'
"$doctor" --summary || true
fi
# Whatever this particular machine wants doing that Panama should not carry for
# everyone. Runs last, after every stage, migrations and the health summary.
hook="$PANAMA_PATH/bin/panama-hook"
[[ -x "$hook" ]] && "$hook" post-upgrade || true
printf '\n'
if (( ${#failed[@]} == 0 )); then
echo "Panama installed. Log out and choose the Hyprland session to start it."
else
printf 'Panama installed with %d failed stage(s): %s\n' "${#failed[@]}" "${failed[*]}" >&2
printf 'Re-running ./install is safe and will retry them.\n' >&2
exit 1
fi