panama update: one command, no questions, and no gap it cannot see
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -2,12 +2,103 @@
|
||||
|
||||
# Panama's installer. Safe to re-run: every stage is idempotent, and this is
|
||||
# also the upgrade path.
|
||||
#
|
||||
# ./install A machine being built. Asks the interview, runs
|
||||
# every stage, enrolls hardware.
|
||||
# ./install --upgrade A machine that already exists. Asks nothing.
|
||||
#
|
||||
# Two entry points, one stage list, deliberately in one file. `panama update`
|
||||
# passes --upgrade; if the upgrade path owned a second copy of STAGES the two
|
||||
# would drift the first time somebody added a stage to one of them, and the
|
||||
# symptom would be a stage that silently never runs. Keeping the lists together
|
||||
# means the decision about which path owns a new stage is made in view of the
|
||||
# other one.
|
||||
#
|
||||
# What --upgrade changes, and nothing else:
|
||||
#
|
||||
# * The interview is skipped, so every PANAMA_* answer is unset and each
|
||||
# stage takes its documented empty-answer path. Five of the seven need no
|
||||
# answer at all; link-user falls back to the decision it recorded.
|
||||
# * setup-identity and install-hardware are dropped. They exist only to
|
||||
# consume interview answers -- git identity, NVIDIA, Secure Boot, firmware
|
||||
# -- and every one of those is a first-run decision.
|
||||
# * install-packages runs only when the package lists actually changed.
|
||||
# * Migrations always run rather than baseline. See the migrations block.
|
||||
#
|
||||
# Everything else is shared on purpose: the sudo keepalive, the per-stage
|
||||
# failure collection, migrations, the health summary and the post-upgrade hook.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
|
||||
UPGRADE=0
|
||||
FORCE_PACKAGES=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--upgrade) UPGRADE=1 ;;
|
||||
--packages) FORCE_PACKAGES=1 ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
usage: install [--upgrade] [--packages]
|
||||
|
||||
(no arguments) Build this machine. Asks the interview, runs every stage.
|
||||
--upgrade Update a machine that already exists. Asks nothing, and
|
||||
skips setup-identity and install-hardware.
|
||||
--packages Run install-packages even when the lists are unchanged.
|
||||
Only meaningful with --upgrade; a full install always runs it.
|
||||
USAGE
|
||||
exit 0 ;;
|
||||
*)
|
||||
printf 'install: unknown argument: %s\n' "$arg" >&2
|
||||
printf "Run './install --help' to see what it takes.\n" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
source "$PANAMA_PATH/bin/ascii"
|
||||
|
||||
# ── Have the package lists changed? ──────────────────────────────────────────
|
||||
#
|
||||
# install-packages is the slow stage -- a dnf metadata refresh, a Flathub
|
||||
# round-trip, and a transaction that resolves to "nothing to do" almost every
|
||||
# time. On an upgrade it is worth running only when the lists it reads actually
|
||||
# changed, so this hashes them and remembers the result.
|
||||
#
|
||||
# A content hash rather than a git range, because Panama is developed in place:
|
||||
# a package added to a list and not yet committed must still install. A range
|
||||
# check would see nothing, and the package would arrive whenever the commit
|
||||
# happened to be pulled somewhere else.
|
||||
#
|
||||
# -maxdepth 1 excludes setup/packages/extras/. An answer-free run has an empty
|
||||
# PANAMA_EXTRAS and installs no optional category, so hashing those files would
|
||||
# flip the hash, run the stage, install nothing, and record the new hash as
|
||||
# though it had. Optional categories cannot be re-applied by an upgrade at all
|
||||
# -- which ones this machine chose is nowhere on disk, because the interview's
|
||||
# answers are deliberately transient -- and `panama apps` is the tool for that.
|
||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
|
||||
PACKAGES_HASH="$STATE_DIR/packages-hash"
|
||||
|
||||
hash_packages() {
|
||||
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -exec sha256sum {} + \
|
||||
| sort | sha256sum | cut -d' ' -f1
|
||||
}
|
||||
|
||||
packages_needed() {
|
||||
(( FORCE_PACKAGES )) && return 0
|
||||
(( UPGRADE )) || return 0
|
||||
[[ -r "$PACKAGES_HASH" ]] || return 0
|
||||
[[ "$(hash_packages)" != "$(cat "$PACKAGES_HASH")" ]]
|
||||
}
|
||||
|
||||
# Written only after the stage succeeds, mirroring the rule panama-migrate
|
||||
# documents for its markers: a step that did not complete has not happened, and
|
||||
# recording it as done hides it forever.
|
||||
record_packages_hash() {
|
||||
mkdir -p "$STATE_DIR"
|
||||
hash_packages >"$PACKAGES_HASH"
|
||||
}
|
||||
|
||||
# ── The interview ────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Everything Panama needs to be told is asked here, before a single package is
|
||||
@@ -22,17 +113,22 @@ source "$PANAMA_PATH/bin/ascii"
|
||||
# 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
|
||||
}
|
||||
# Gated exactly like the interview itself: under --upgrade no questions are
|
||||
# asked, so nothing here is used, and a machine that cannot install gum must
|
||||
# not have that stop an upgrade that never needed it.
|
||||
if (( ! UPGRADE )); then
|
||||
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
|
||||
fi
|
||||
|
||||
# ── Keep the machine awake for the duration ──────────────────────────────────
|
||||
@@ -85,17 +181,23 @@ gsettings set org.gnome.desktop.session idle-delay 0 2>/dev/null || true
|
||||
# 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
|
||||
#
|
||||
# Skipped entirely under --upgrade. Nothing is exported, so every answer below
|
||||
# is unset and each stage takes the empty-answer path it already documents --
|
||||
# which is why this is a flag rather than a rewrite of seven stage scripts.
|
||||
if (( ! UPGRADE )); then
|
||||
PANAMA_ANSWERS="$(mktemp -t panama-answers.XXXXXX)"
|
||||
export PANAMA_ANSWERS
|
||||
|
||||
if ! "$PANAMA_PATH/setup/scripts/interview"; then
|
||||
exit 1
|
||||
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
|
||||
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
|
||||
@@ -104,7 +206,11 @@ export PANAMA_HOSTNAME PANAMA_GIT_NAME PANAMA_GIT_EMAIL PANAMA_GIT_EDITOR \
|
||||
# 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."
|
||||
if (( UPGRADE )); then
|
||||
echo "Panama needs administrator rights to apply system settings and packages."
|
||||
else
|
||||
echo "Panama needs administrator rights for the rest of the run."
|
||||
fi
|
||||
sudo -v || exit 1
|
||||
( while kill -0 "$$" 2>/dev/null; do sudo -n true 2>/dev/null || true; sleep 60; done ) &
|
||||
SUDO_KEEPALIVE=$!
|
||||
@@ -117,14 +223,37 @@ if [[ -n "${PANAMA_HOSTNAME:-}" ]]; then
|
||||
fi
|
||||
|
||||
STAGES=(install-packages link-dotfiles link-user change-settings link-vicinae-scripts setup-identity install-hardware)
|
||||
|
||||
# The two an upgrade drops. Both exist only to act on interview answers, and
|
||||
# both are first-run decisions: who you are and what hardware this is. Filtered
|
||||
# by name rather than by position so reordering STAGES cannot silently change
|
||||
# which stages an upgrade runs.
|
||||
if (( UPGRADE )); then
|
||||
upgrade_stages=()
|
||||
for stage in "${STAGES[@]}"; do
|
||||
case "$stage" in
|
||||
setup-identity|install-hardware) continue ;;
|
||||
esac
|
||||
upgrade_stages+=("$stage")
|
||||
done
|
||||
STAGES=("${upgrade_stages[@]}")
|
||||
fi
|
||||
|
||||
failed=()
|
||||
for stage in "${STAGES[@]}"; do
|
||||
script="$PANAMA_PATH/setup/scripts/$stage"
|
||||
[[ -x "$script" ]] || continue
|
||||
printf '\n=== %s ===\n' "$stage"
|
||||
if [[ "$stage" == install-packages ]] && ! packages_needed; then
|
||||
echo "The package lists have not changed since the last run; skipping."
|
||||
echo "Run with --packages to install them anyway."
|
||||
continue
|
||||
fi
|
||||
if ! "$script"; then
|
||||
failed+=("$stage")
|
||||
printf '!!! %s failed\n' "$stage" >&2
|
||||
elif [[ "$stage" == install-packages ]]; then
|
||||
record_packages_hash
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -140,10 +269,17 @@ done
|
||||
# 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.
|
||||
#
|
||||
# That inference is only sound during a real install. Under --upgrade the
|
||||
# machine demonstrably existed before this run, so an absent marker directory
|
||||
# means it predates migrations entirely -- exactly the machine the repairs were
|
||||
# written for -- and baselining would skip every one of them forever. Every
|
||||
# migration is self-guarding and a no-op where it does not apply, so running
|
||||
# them is the safe direction.
|
||||
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
|
||||
if (( UPGRADE )) || [[ -d "$STATE_DIR/migrations" ]]; then
|
||||
"$migrate" run || failed+=(migrations)
|
||||
else
|
||||
"$migrate" --baseline || true
|
||||
@@ -172,10 +308,24 @@ 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."
|
||||
if (( UPGRADE )); then
|
||||
retry='panama update'
|
||||
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
|
||||
retry='./install'
|
||||
fi
|
||||
|
||||
if (( ${#failed[@]} == 0 )); then
|
||||
if (( UPGRADE )); then
|
||||
echo "Panama is up to date."
|
||||
else
|
||||
echo "Panama installed. Log out and choose the Hyprland session to start it."
|
||||
fi
|
||||
else
|
||||
if (( UPGRADE )); then
|
||||
printf 'Panama updated with %d failed stage(s): %s\n' "${#failed[@]}" "${failed[*]}" >&2
|
||||
else
|
||||
printf 'Panama installed with %d failed stage(s): %s\n' "${#failed[@]}" "${failed[*]}" >&2
|
||||
fi
|
||||
printf 'Re-running %s is safe and will retry them.\n' "$retry" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user