feat(installer): Offer repair chroot on post-pacstrap failure
Tests / tests (push) Successful in 3m5s

- offer_chroot(): optional arch-chroot shell into the still-mounted target,
  from install_cleanup() on success and from the EXIT trap when a step fails
  after pacstrap (TARGET_INSTALLED flag); temp NOPASSWD sudoers drop-ins are
  always removed before the shell is opened.
- INT/TERM handler re-exits with 128+N: without it bash runs the EXIT trap
  with $? = 0 for a signal-killed shell and the rc != 0 test would skip the
  offer. A signal during teardown only aborts the prompt (first) so the
  unmount still runs; a second forces the exit.
- cleanup(): report a failed umount -R / cryptsetup close instead of hiding
  stderr, so a repair shell leaving busy mounts is visible before reboot.
- Document the flow in AGENTS.md.
This commit is contained in:
alvinlollo
2026-10-14 23:08:18 +11:00
parent 00c89f23f0
commit e377f9313a
2 changed files with 83 additions and 6 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ The airootfs overlay ships it as `/usr/local/bin/alvin-install` (mode `0755` for
locale, login manager, sshd, summary) then a fixed order of `install_*` steps
(partition → LUKS → format/mount → pacstrap → fstab → repos → timezone → locale →
hostname → network → sshd → user → git/SSH → end-4 dots → mkinitcpio → bootloader →
TPM/unl0kr → login manager (none if unl0kr succeeded; with unl0kr selected the login screen only asks for a fallback) → autologin → update (reflector + keyrings + `pacman -Syu`, warn-only) → cleanup). Screens and install steps are
TPM/unl0kr → login manager (none if unl0kr succeeded; with unl0kr selected the login screen only asks for a fallback) → autologin → update (reflector + keyrings + `pacman -Syu`, warn-only) → cleanup, which offers an optional `arch-chroot` shell into the target before unmounting; the `EXIT` trap offers the same shell when a step after pacstrap fails or the install is interrupted (INT/TERM re-exit with 128+N so signals count as failure). Screens and install steps are
independent — edit one without
touching the other. An `EXIT` trap unmounts `$TARGET` and closes `cryptroot` on any exit.
- **Package list**: `install_pacstrap` builds the *installed* system's list from the medium's
+82 -5
View File
@@ -43,20 +43,74 @@ SSHD_APPLIED="false"
TPM_DISCLAIMER=""
TARGET_DB_SYNCED="false"
UPDATE_STATUS="not run"
# Flipped once pacstrap succeeds: from then on a failed step still leaves a
# chroot-able system, so the EXIT trap offers a repair shell into it.
TARGET_INSTALLED="false"
# Teardown state: a signal arriving while cleanup() runs must not skip the
# unmount - the first one only aborts what is on screen, a second forces exit.
CLEANUP_RUNNING="false"
SIGNALS_SEEN="0"
log() { echo "[$(date '+%H:%M:%S')] $*" >>"$LOG"; }
cleanup() {
set +e
# Temporary NOPASSWD drop-ins some install steps create; never leave them
# behind (EXIT) or expose them to the user's post-install chroot shell.
remove_temp_sudoers() {
rm -f "${TARGET}/etc/sudoers.d/99-end4-install" \
"${TARGET}/etc/sudoers.d/99-ssh-import-id" \
"${TARGET}/etc/sudoers.d/99-unl0kr-install" 2>/dev/null
umount -R "$TARGET" 2>/dev/null
cryptsetup close cryptroot 2>/dev/null
"${TARGET}/etc/sudoers.d/99-unl0kr-install" 2>/dev/null || true
}
cleanup() {
local rc=$?
CLEANUP_RUNNING="true"
set +e
trap - ERR
# A later step failed but the base system is on disk: let the user fix
# it from inside before everything is unmounted.
if ((rc != 0)) && [[ "$TARGET_INSTALLED" == "true" && -t 0 ]]; then
echo ""
warn "Installation failed after the base system was installed."
offer_chroot "The installed system is still mounted. Chroot into it to fix things?"
fi
remove_temp_sudoers
# A repair shell can leave mounts behind, and the LUKS mapping may still
# be open - report a failure instead of hiding it, so the user knows the
# target was not cleanly torn down before rebooting.
if mountpoint -q "$TARGET" 2>/dev/null; then
if ! umount -R "$TARGET"; then
log "Cleanup: umount -R $TARGET failed"
warn "Some mounts under ${TARGET} are busy - unmounting failed (released at reboot)."
fi
fi
if [[ -e /dev/mapper/cryptroot ]] && ! cryptsetup close cryptroot 2>/dev/null; then
log "Cleanup: cryptsetup close cryptroot failed"
warn "cryptroot is still open - it is released at reboot."
fi
true
}
trap cleanup EXIT
# Signal handler. Without one, bash runs the EXIT trap with $? = 0 when the
# shell itself is killed (SIGINT between commands, SIGTERM from a shutdown),
# and cleanup()'s rc != 0 test would read that as success and skip the repair
# shell. Re-exit with the conventional 128+N status so an interrupted install
# takes the same path as a failed step. Note: a SIGINT delivered while a
# foreground command is running is consumed together with that command (bash
# then suppresses both errexit and this handler) - that case is unchanged.
on_signal() {
local status="$1"
if [[ "$CLEANUP_RUNNING" == "true" && "$SIGNALS_SEEN" == "0" ]]; then
SIGNALS_SEEN="1"
return 0
fi
echo "" >&2
echo "Interrupted." >&2
exit "$status"
}
trap 'on_signal 130' INT
trap 'on_signal 143' TERM
# ── error surfacing ───────────────────────────────────────────────────────────
# Step output normally ends up in $LOG only. These helpers make sure a failure
# is ALSO shown on screen, paged through `gum pager` so long outputs stay
@@ -811,6 +865,7 @@ install_pacstrap() {
>>"$LOG" 2>&1 || warn "cert-sync failed - mono apps may reject TLS certificates."
fi
TARGET_INSTALLED="true"
ok "Packages installed."
}
@@ -2337,11 +2392,33 @@ install_cleanup() {
gum pager <"$LOG" || true
fi
echo ""
offer_chroot
# Last line on screen after the optional log view - the instruction to
# reboot must be the final thing the user reads.
echo "Run 'reboot' to boot into your new system."
}
# Optional shell inside the installed system. $TARGET (and cryptroot) are
# still mounted here - cleanup() only tears them down afterwards.
# Also called from the EXIT trap when a step after pacstrap failed ($1 then
# carries the failure prompt).
offer_chroot() {
gum confirm --affirmative "Open chroot" --negative "No thanks" \
"${1:-Chroot into the installed system to make changes before rebooting?}" ||
return 0
remove_temp_sudoers
# Copy first so the log is readable from inside (/var/log/alvin-install.log).
cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true
info "Entering ${TARGET} - type 'exit' to leave and finish."
log "Chroot: interactive shell opened"
# The user's last command may fail; that must not trip errexit/ERR.
arch-chroot "$TARGET" /bin/bash || true
log "Chroot: shell closed"
# Keep the copy in the target current with the lines logged above.
cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true
echo ""
}
# ── Main ──────────────────────────────────────────────────────────────────────
main() {