#!/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. # # The interview's tools are bootstrapped first because it cannot install them # itself -- they are declared in the package lists, which install-packages # installs, which runs after this. gum is the interface; pciutils, mokutil and # fwupd are the interview's eyes. # It probes for an NVIDIA card, Secure Boot state and updatable firmware # BEFORE install-packages runs, and a missing probe tool degrades the answer # silently to "no" -- which for Secure Boot once meant installing a driver # that could never load. Workstation ships all four; a minimal base does not. bootstrap=() command -v gum >/dev/null 2>&1 || bootstrap+=(gum) command -v lspci >/dev/null 2>&1 || bootstrap+=(pciutils) command -v mokutil >/dev/null 2>&1 || bootstrap+=(mokutil) command -v fwupdmgr >/dev/null 2>&1 || bootstrap+=(fwupd) if (( ${#bootstrap[@]} > 0 )); then echo "Installing what the setup questions are built on: ${bootstrap[*]}" sudo dnf install -y "${bootstrap[@]}" >/dev/null || { echo "Could not install ${bootstrap[*]}, 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