The audit's third tier: everything between this installer and a fresh machine it has never met. The one path that could cost a person their display: the interview probes Secure Boot with mokutil, which install-packages had not installed yet, so on a minimal base the MOK question silently never fired -- and install-hardware still installed akmod-nvidia and blacklisted nouveau, arming a reboot into an unloadable driver with its fallback disabled. The probe tools (pciutils, mokutil, fwupd) now bootstrap beside gum, and install-hardware re-checks Secure Boot for itself and refuses the driver rather than the display. Secrets leave the checkout: the personal environment moves to ~/.config/panama/env at mode 600 by migration, and .bashrc sources it with a permission check that quietly re-tightens drift. change-settings no longer overwrites /etc/dnf/dnf.conf -- two performance keys are set additively, the defaultyes=True that made every `dnf remove` treat Enter as yes is gone, and a migration strips it from machines that already received it. Package installation survives the world changing: the initial and desktop lists run with --skip-unavailable and a report_missing pass that names what was skipped (resolved through --whatprovides, so capability names like awk do not cry wolf); the openh264, appstream and core-group extras go through soft; RustDesk resolves its RPM for the machine's own architecture; and the Claude Desktop repository script is fetched to a kept file and run, never piped from the network into root. The hardware predicates stop guessing: a wireless mouse's scope=Device battery no longer turns a tower into a laptop, USB-PD-only machines read their power state from the battery's own status instead of being permanently "on AC", the lid falls back to logind's LidClosed where ACPI is silent, and charge limits reach every pack of a two-battery machine in one authorization -- with the reported percentage summed across packs. And the parsers stop assuming this machine: snapper is read through --machine-readable csv with named columns instead of a localized box-drawing table, and reports whether snapshots are even possible so ext4 and unconfigured-btrfs stop looking identical; fprintd is parsed under LC_ALL=C; the hypridle drop-in resolves the binary it points at; the recorder's render node became an "auto" token resolved at record time; update-grub writes the config its firmware actually boots; the nvm prompt hook and the SSH tmux takeover are guarded; hipblas and rocm-opencl move to an opt-in gpu-compute category; and the two interactive python tools' libraries are declared. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
182 lines
9.0 KiB
Bash
Executable File
182 lines
9.0 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.
|
|
#
|
|
# 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
|