Make ./install something you could hand a stranger

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
This commit is contained in:
Gabriel Brown
2026-08-23 12:10:03 -04:00
parent 44124d72fa
commit 153554b5df
22 changed files with 331 additions and 87 deletions
+12 -1
View File
@@ -15,7 +15,18 @@ exists() { command -v "$1" >/dev/null 2>&1; }
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
echo -e "\n--- Copying System & User files ---"
log "Changing DNF Settings"
# dnf options are applied as individual keys rather than shipped as a file.
# config/copy once carried a whole /etc/dnf/dnf.conf, which clobbered proxies
# and mirrors a machine already had -- and set defaultyes=True system-wide, so
# every `dnf remove` for every user treated a bare Enter as yes. These two are
# performance knobs with no behavior change; anything else stays the machine's
# own business. (fastestmirror and deltarpm were dnf4-era keys dnf5 ignores,
# so they simply stop being written.)
log "Setting dnf performance options (max_parallel_downloads, keepcache)"
sudo dnf config-manager setopt max_parallel_downloads=10 keepcache=True 2>/dev/null \
|| log "Could not set dnf options; defaults apply"
log "Copying system files (udev rules, systemd drop-ins)"
sudo cp -r "$PANAMA_PATH/config/copy/." "/"
# The GPU symlink rules land via the copy above. Apply them without a reboot so
+13
View File
@@ -40,6 +40,18 @@ fi
# ── NVIDIA ───────────────────────────────────────────────────────────────────
if [[ "${PANAMA_NVIDIA:-no}" == yes ]]; then
# The interview asks about MOK enrollment only when mokutil was present to
# see Secure Boot at all. Re-check here rather than trusting that the
# question was ever asked: installing akmod-nvidia and blacklisting
# nouveau under Secure Boot with no key to enroll produces a machine that
# reboots into an unloadable driver with its fallback disabled -- the one
# failure in this installer that costs a person their display.
if mokutil --sb-state 2>/dev/null | grep -qi 'secureboot enabled' \
&& [[ -z "${PANAMA_MOK_HASH:-}" ]]; then
warn "Secure Boot is on and no MOK enrollment was prepared; refusing to install"
warn "the NVIDIA driver, which could not load. Re-run ./install and answer the"
warn "Secure Boot question, or disable Secure Boot first."
else
log "Installing the NVIDIA driver"
if sudo dnf install -y akmod-nvidia xorg-x11-drv-nvidia-cuda; then
# nouveau has to be out of the way before the kernel would otherwise
@@ -61,6 +73,7 @@ if [[ "${PANAMA_NVIDIA:-no}" == yes ]]; then
else
warn "The NVIDIA driver did not install; skipping its kernel arguments and services"
fi
fi
fi
# ── Secure Boot ──────────────────────────────────────────────────────────────
+42 -8
View File
@@ -22,6 +22,20 @@ packages_in() {
sed 's/#.*//' "$1" | tr "\n" " "
}
# Names a list asked for that still are not installed, so --skip-unavailable
# above can never silently shrink a list: a skipped font is a warning somebody
# reads, not an absence somebody debugs a month later.
report_missing() {
local file="$1" name missing=()
for name in $(packages_in "$file"); do
# --whatprovides, because several list entries are capabilities a
# differently-named package satisfies: awk is gawk, wget is wget2-wget.
rpm -q --whatprovides "$name" >/dev/null 2>&1 || missing+=("$name")
done
(( ${#missing[@]} > 0 )) && log "WARNING: not available on this machine: ${missing[*]}"
return 0
}
# Runs something whose failure must not cost you the desktop.
#
# `set -e` above is right for the packages Panama cannot work without and wrong
@@ -58,10 +72,13 @@ echo -e "\n--- Installing Repositories ---"
log "Installing RPM Fusion Free and Nonfree Repositories"
sudo dnf install -y https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm > /dev/null
log "Enabling Fedora Cisco OpenH264 Repository"
sudo dnf config-manager setopt fedora-cisco-openh264.enabled=1
# soft: this repo does not exist on every spin, and its absence must not cost
# the desktop -- the ordering rule at soft()'s definition applies to the
# repository extras just as much as to the codec swaps below.
soft "enabling the openh264 repository" sudo dnf config-manager setopt fedora-cisco-openh264.enabled=1
log "Installing RPM Fusion AppStream Metadata"
sudo dnf update @core -y > /dev/null
sudo dnf install -y rpmfusion-\*-appstream-data > /dev/null
soft "the core group update" sudo dnf update @core -y
soft "the RPM Fusion appstream metadata" sudo dnf install -y rpmfusion-\*-appstream-data
# Terra bootstraps itself: --repofrompath defines a throwaway repo just long
# enough to install terra-release, which then writes the real /etc/yum.repos.d
# entry. Doing that a second time is not harmless -- dnf5 refuses the whole
@@ -90,7 +107,12 @@ if [[ -f "$PACKAGES_FILE" ]]; then
log "Installing Initial Packages"
echo -e "Includes the following packages:"
echo -e "$(<"$PACKAGES_FILE")"
sudo dnf install -y $INITIAL_PACKAGES > /dev/null
# --skip-unavailable: dnf5 refuses a whole transaction over one missing
# name, so a single rotted entry in this list used to cost every package
# in it -- and the desktop below never installed. The skipped names are
# reported afterwards rather than silently dropped.
sudo dnf install -y --skip-unavailable $INITIAL_PACKAGES > /dev/null
report_missing "$PACKAGES_FILE"
log "Initial packages installed!"
else
log "Package list was not in specified path: $PACKAGES_FILE"
@@ -103,7 +125,8 @@ if [[ -f "$DESKTOP_FILE" ]]; then
log "Installing Desktop Packages"
echo -e "Includes the following packages:"
echo -e "$(<"$DESKTOP_FILE")"
sudo dnf install -y $DESKTOP_PACKAGES > /dev/null
sudo dnf install -y --skip-unavailable $DESKTOP_PACKAGES > /dev/null
report_missing "$DESKTOP_FILE"
log "Desktop packages installed!"
else
log "Package list was not in specified path: $DESKTOP_FILE"
@@ -263,8 +286,19 @@ if rpm -q claude-desktop-extra >/dev/null 2>&1; then
else
if [[ ! -f /etc/yum.repos.d/claude-desktop.repo ]]; then
log "Adding the Claude Desktop repository..."
curl -fsSL https://patrickjaja.github.io/claude-desktop-extra/install-rpm.sh \
| sudo bash > /dev/null 2>&1 || log "Could not add the Claude Desktop repository"
# Fetched to a file and then run, never piped into root: a pipe executes
# whatever the network answered with no chance to look, and this one is an
# unpinned script from a personal GitHub Pages site -- the least trusted
# thing this installer touches. The file is kept next to the run so what
# executed is still on disk to read afterwards.
claude_repo_script="$(mktemp -t claude-desktop-repo.XXXXXX.sh)"
if curl -fsSL https://patrickjaja.github.io/claude-desktop-extra/install-rpm.sh \
-o "$claude_repo_script"; then
sudo bash "$claude_repo_script" > /dev/null 2>&1 \
|| log "Could not add the Claude Desktop repository (script kept at $claude_repo_script)"
else
log "Could not download the Claude Desktop repository script"
fi
fi
log "Installing Claude Desktop..."
sudo dnf install -y claude-desktop-extra > /dev/null \
@@ -283,7 +317,7 @@ else
# rate-limited -- would otherwise trip set -e and kill the stage before the
# empty-result fallback below could do its job.
rustdesk_url="$(curl -fsSL https://api.github.com/repos/rustdesk/rustdesk/releases/latest 2>/dev/null \
| jq -r '.assets[].browser_download_url | select(test("x86_64\\.rpm$")) | select(test("suse") | not)' \
| jq -r --arg arch "$(uname -m)" '.assets[].browser_download_url | select(test($arch + "\\.rpm$")) | select(test("suse") | not)' \
| head -1 || true)"
if [[ -n "$rustdesk_url" ]]; then
log "Installing RustDesk from $rustdesk_url"