diff --git a/AGENTS.md b/AGENTS.md index 6d9b12e..d84f5ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/airootfs/usr/local/bin/alvin-install b/airootfs/usr/local/bin/alvin-install index 9988d1e..89c2a4a 100755 --- a/airootfs/usr/local/bin/alvin-install +++ b/airootfs/usr/local/bin/alvin-install @@ -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() {