Files
Panama/install
T
Gabriel Brown c2eddc2c5b Ask for the password once, and own the plugin the COPR needs
Two ways a fresh machine's unattended run could still stall. The stages call
sudo dozens of times across twenty-plus minutes, and the timestamp expires
five minutes after whichever call came last -- so any single dnf step that
outlasted it turned the next stage into a password prompt nobody was there to
answer. ./install now authenticates once, right after the interview, and a
refresher holds the timestamp for exactly as long as the script lives;
cleanup() kills it on every exit path, so nothing outlives the install with
ambient credentials.

And `dnf copr` comes from dnf5-plugins, which a machine that has used copr
carries and a fresh Workstation does not -- a missing plugin there would have
cost the entire desktop. Declared in initial-packages, where the repositories
stage can rely on it.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 11:07:37 -04:00

173 lines
8.5 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"
[[ -n "${SUDO_KEEPALIVE:-}" ]] && kill "$SUDO_KEEPALIVE" 2>/dev/null
}
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);
# link-user runs before setup-identity so tracked personal content wins over
# what the interview would otherwise seed; 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 PANAMA_USER_CONTENT
# One password, before anything long runs, and then never again. The stages
# call sudo dozens of times across twenty-plus minutes, and the timestamp
# expires five minutes after whichever call came last -- so a single dnf step
# that outlasts it turned the next stage into a password prompt nobody was
# there to answer. The refresher holds the timestamp open for exactly as long
# as this script lives; cleanup() kills it on every exit path, so nothing
# outlives the install with ambient credentials.
echo "Panama needs administrator rights for the rest of the run."
sudo -v || exit 1
( while kill -0 "$$" 2>/dev/null; do sudo -n true 2>/dev/null || true; sleep 60; done ) &
SUDO_KEEPALIVE=$!
# Applied here rather than in a stage, and applied early: it needs sudo, and
# sudo is warm right now.
if [[ -n "${PANAMA_HOSTNAME:-}" ]]; then
sudo hostnamectl set-hostname "$PANAMA_HOSTNAME"
echo "Hostname set to: $(hostname)"
fi
STAGES=(install-packages link-dotfiles link-user 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