feat(gum): Add further additions of gum to install script
Tests / tests (push) Successful in 2m29s

This commit is contained in:
alvinlollo
2026-10-08 14:48:20 +11:00
parent 59d82f06f5
commit db83549996
+357 -86
View File
@@ -48,7 +48,110 @@ cleanup() {
true true
} }
trap cleanup EXIT trap cleanup EXIT
trap 'gum style --foreground 9 --bold "Failed at line $LINENO. See $LOG"' ERR
# ── 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
# inspectable instead of scrolling away or hiding in a file nobody opens.
# Page stdin to the user with a context line. $1 = context. gum is a hard
# prerequisite of this installer (check_prereqs), so its pager is used
# instead of depending on less being present.
page_output() {
local ctx="$1"
if [[ -c /dev/tty && -t 1 ]]; then
{ gum style --foreground 11 --bold "$ctx"; echo ""; cat; } | gum pager || true
else
gum style --foreground 9 --bold "$ctx" || true
cat >&2 || true
fi
}
# Page the last $2 lines (default 200) of $LOG - for output that was already
# redirected there. $1 = context.
page_log_tail() {
[[ -f "$LOG" ]] || return 0
tail -n "${2:-200}" "$LOG" | page_output "$1"
}
# Page the $LOG lines appended after $1 (a line count taken with
# `wc -l < "$LOG"` before the step ran). $2 = context. A mark at or past the
# end of the log means nothing new was written - skip instead of opening a
# pager on an empty screen.
page_log_since() {
local mark="$1" total=0
[[ -f "$LOG" ]] || return 0
total=$(wc -l <"$LOG") || total=0
(( mark < total )) || return 0
sed -n "$((mark + 1)),\$p" "$LOG" | page_output "$2"
}
# Run a command, tee its combined output into $LOG (as the old
# `>>"$LOG" 2>&1` redirects did - live, so an interrupted command keeps its
# partial output) and - on failure - page the lines it added so the error
# reaches the user, not just the log. Returns the command's exit status.
# Usage: run_logged [-s SPINNER [-t TITLE]] "what failed" cmd [args...]
# -s show a gum spinner while cmd runs: dot for linear commands, jump for
# ones that compile. Frames render on stderr (the terminal), never $LOG.
# -t spinner title; omit when an info line already says what is running.
run_logged() {
local ctx="" spinner="" title="" rc=0 mark=0 total=0
while [[ "${1:-}" == -[st] ]]; do
case "$1" in
-s) spinner="$2"; shift 2 ;;
-t) title="$2"; shift 2 ;;
esac
done
ctx="$1"
shift
# Line count before the command, so only ITS output gets paged on failure.
# A missing $LOG (mark 0) is fine: tee creates it, so lines 1..n are
# exactly this command's output.
if [[ -f "$LOG" ]]; then
mark=$(wc -l <"$LOG") || mark=0
fi
# tee appends to $LOG while /dev/null keeps the terminal clean. With
# pipefail the pipeline status is the command's own; the only false
# failure possible is tee itself failing to write (e.g. disk full).
if [[ -n "$spinner" ]]; then
# gum spin propagates the command's exit status; --show-output keeps
# its stdout (the command's output) free of spinner frames for tee,
# while the spinner itself renders on stderr = the terminal. The sh
# wrapper merges the command's stderr into that stdout first, so no
# output bypasses $LOG.
local -a spin=(gum spin --show-output --spinner "$spinner")
if [[ -n "$title" ]]; then
spin+=(--title "$title")
fi
"${spin[@]}" -- sh -c '"$@" 2>&1' sh "$@" \
| tee -a "$LOG" >/dev/null || rc=$?
else
"$@" 2>&1 | tee -a "$LOG" >/dev/null || rc=$?
fi
if (( rc != 0 )); then
if [[ -f "$LOG" ]]; then
total=$(wc -l <"$LOG") || total=0
fi
if (( mark < total )); then
page_log_since "$mark" "${ctx} (exit ${rc}) - output below, full log: $LOG"
else
# Silent failure: nothing to page, but still surface where and why.
gum style --foreground 11 --bold \
"${ctx} (exit ${rc}) - no output captured, full log: $LOG" || true
fi
fi
return "$rc"
}
# ERR trap: a fatal failure aborts the install (errexit) - say where, then
# page the recent log so the actual error is on screen, not log-only.
on_err() {
local rc=$?
trap - ERR
gum style --foreground 9 --bold "Installation failed (exit ${rc}) at line $1. Full log: $LOG" || true
page_log_tail "Last output before the failure - q to quit, scroll for more" 300 || true
}
trap 'on_err "$LINENO"' ERR
(( EUID == 0 )) || { echo "Run as root" >&2; exit 1; } (( EUID == 0 )) || { echo "Run as root" >&2; exit 1; }
@@ -122,13 +225,14 @@ gvariant_unescape() {
sync_target_dbs() { sync_target_dbs() {
[[ "${TARGET_DB_SYNCED}" == "true" ]] && return 0 [[ "${TARGET_DB_SYNCED}" == "true" ]] && return 0
info "Syncing target package databases..." info "Syncing target package databases..."
arch-chroot "$TARGET" pacman-key --populate archlinux cachyos chaotic \ run_logged -s dot "pacman-key --populate failed" \
>>"$LOG" 2>&1 || true arch-chroot "$TARGET" pacman-key --populate archlinux cachyos chaotic || true
if arch-chroot "$TARGET" pacman -Syy --noconfirm >>"$LOG" 2>&1; then if run_logged -s dot "Target pacman -Syy failed" \
arch-chroot "$TARGET" pacman -Syy --noconfirm; then
TARGET_DB_SYNCED="true" TARGET_DB_SYNCED="true"
ok "Target databases synced." ok "Target databases synced."
else else
warn "Target pacman -Syy failed (see $LOG) - continuing with existing databases." warn "Target pacman -Syy failed (output above) - continuing with existing databases."
fi fi
} }
@@ -552,7 +656,8 @@ install_pacstrap() {
# Parse only the first field, then drop names not resolvable in the live # Parse only the first field, then drop names not resolvable in the live
# session's repos (e.g. local-aur packages are not configured there). # session's repos (e.g. local-aur packages are not configured there).
info "Syncing package databases for resolvability check..." info "Syncing package databases for resolvability check..."
if pacman -Syy --noconfirm >>"$LOG" 2>&1; then if run_logged -s dot "pacman -Syy failed (resolvability check)" \
pacman -Syy --noconfirm; then
declare -A available=() declare -A available=()
local db_line local db_line
while IFS= read -r db_line; do while IFS= read -r db_line; do
@@ -560,7 +665,7 @@ install_pacstrap() {
done < <(pacman -Slq 2>>"$LOG" || true) done < <(pacman -Slq 2>>"$LOG" || true)
(( ${#available[@]} > 0 )) && filter_ok=true (( ${#available[@]} > 0 )) && filter_ok=true
else else
warn "pacman -Syy failed (see $LOG) — installing without resolvability filter." warn "pacman -Syy failed (output above) — installing without resolvability filter."
fi fi
local line pkg local line pkg
@@ -608,9 +713,11 @@ install_pacstrap() {
log "Pacstrap: openssh guaranteed in package list (sshd requested)" log "Pacstrap: openssh guaranteed in package list (sshd requested)"
fi fi
info "Installing ${#pkgs[@]} packages (this may take a while)..."
log "Pacstrap: ${#pkgs[@]} packages, ${#dropped[@]} skipped" log "Pacstrap: ${#pkgs[@]} packages, ${#dropped[@]} skipped"
pacstrap "$TARGET" "${pkgs[@]}" --noconfirm 2>>"$LOG" run_logged -s dot -t "Installing ${#pkgs[@]} packages (this may take a while)" \
"pacstrap failed" \
pacstrap "$TARGET" "${pkgs[@]}" --noconfirm \
|| die "pacstrap failed - the target system is incomplete (output above, full log: $LOG)."
ok "Packages installed." ok "Packages installed."
} }
@@ -696,12 +803,13 @@ install_sshd() {
log "SSH server: enable skipped (sshd binary missing in target)" log "SSH server: enable skipped (sshd binary missing in target)"
return 0 return 0
fi fi
if arch-chroot "$TARGET" systemctl enable sshd.service >>"$LOG" 2>&1; then if run_logged "Could not enable sshd.service" \
arch-chroot "$TARGET" systemctl enable sshd.service; then
SSHD_APPLIED="true" SSHD_APPLIED="true"
log "SSH server: enabled" log "SSH server: enabled"
ok "sshd enabled (starts at boot)." ok "sshd enabled (starts at boot)."
else else
warn "Could not enable sshd.service (see $LOG)." warn "Could not enable sshd.service (output above)."
log "SSH server: enable failed" log "SSH server: enable failed"
fi fi
} }
@@ -732,12 +840,14 @@ install_git_ssh() {
if [[ -n "$GIT_EMAIL" ]]; then if [[ -n "$GIT_EMAIL" ]]; then
info "Configuring git..." info "Configuring git..."
local git_name="${GIT_NAME:-$USERNAME}" local git_name="${GIT_NAME:-$USERNAME}"
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ run_logged "git config user.name failed" \
git config --global user.name "$git_name" >>"$LOG" 2>&1 \ arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|| warn "git config user.name failed (see $LOG)." git config --global user.name "$git_name" \
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ || warn "git config user.name failed (output above)."
git config --global user.email "$GIT_EMAIL" >>"$LOG" 2>&1 \ run_logged "git config user.email failed" \
|| warn "git config user.email failed (see $LOG)." arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
git config --global user.email "$GIT_EMAIL" \
|| warn "git config user.email failed (output above)."
ok "Git configured for ${USERNAME} (${git_name} <${GIT_EMAIL}>)." ok "Git configured for ${USERNAME} (${git_name} <${GIT_EMAIL}>)."
log "Git: ${git_name} <${GIT_EMAIL}>" log "Git: ${git_name} <${GIT_EMAIL}>"
else else
@@ -775,27 +885,33 @@ install_git_ssh() {
else else
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "${TARGET}/etc/sudoers.d/99-ssh-import-id" echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "${TARGET}/etc/sudoers.d/99-ssh-import-id"
chmod 440 "${TARGET}/etc/sudoers.d/99-ssh-import-id" chmod 440 "${TARGET}/etc/sudoers.d/99-ssh-import-id"
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ run_logged "yay -Ss ssh-import-id found nothing" \
env HOME="$home_in_target" yay -Ss ssh-import-id >>"$LOG" 2>&1 \ arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|| warn "yay -Ss ssh-import-id found nothing (see $LOG)." env HOME="$home_in_target" yay -Ss ssh-import-id \
if arch-chroot "$TARGET" pacman -S --noconfirm python-distro >>"$LOG" 2>&1 \ || warn "yay -Ss ssh-import-id found nothing (output above)."
&& arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ if run_logged -s dot -t "Installing ssh-import-id" \
env HOME="$home_in_target" yay -S --noconfirm ssh-import-id >>"$LOG" 2>&1; then "pacman -S python-distro failed" \
arch-chroot "$TARGET" pacman -S --noconfirm python-distro \
&& run_logged -s jump -t "Installing ssh-import-id" \
"yay failed to install ssh-import-id" \
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env HOME="$home_in_target" yay -S --noconfirm ssh-import-id; then
sid_ok="true" sid_ok="true"
log "ssh-import-id: installed via yay" log "ssh-import-id: installed via yay"
else else
warn "yay could not install ssh-import-id (see $LOG) - falling back to github.com keys." warn "yay could not install ssh-import-id (output above) - falling back to github.com keys."
fi fi
rm -f "${TARGET}/etc/sudoers.d/99-ssh-import-id" rm -f "${TARGET}/etc/sudoers.d/99-ssh-import-id"
fi fi
if [[ "$sid_ok" == "true" ]]; then if [[ "$sid_ok" == "true" ]]; then
if arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ if run_logged "ssh-import-id failed" \
env HOME="$home_in_target" ssh-import-id "gh:${GH_USERNAME}" >>"$LOG" 2>&1; then arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env HOME="$home_in_target" ssh-import-id "gh:${GH_USERNAME}"; then
ok "SSH keys imported via ssh-import-id." ok "SSH keys imported via ssh-import-id."
log "SSH keys: ssh-import-id gh:${GH_USERNAME}" log "SSH keys: ssh-import-id gh:${GH_USERNAME}"
else else
warn "ssh-import-id failed (see $LOG) - falling back to github.com keys." warn "ssh-import-id failed (output above) - falling back to github.com keys."
sid_ok="false" sid_ok="false"
fi fi
fi fi
@@ -808,6 +924,8 @@ install_git_ssh() {
keys=$(curl -fsSL --max-time 30 "$keys_url" 2>>"$LOG") || curl_rc=$? keys=$(curl -fsSL --max-time 30 "$keys_url" 2>>"$LOG") || curl_rc=$?
if (( curl_rc != 0 )); then if (( curl_rc != 0 )); then
warn "Could not fetch ${keys_url} (curl exit ${curl_rc}) - no SSH keys imported." warn "Could not fetch ${keys_url} (curl exit ${curl_rc}) - no SSH keys imported."
# curl's stderr went to $LOG only - put it in front of the user.
page_log_tail "curl error output (q to quit)" 60 || true
log "SSH keys: curl failed (exit ${curl_rc}) for ${keys_url}" log "SSH keys: curl failed (exit ${curl_rc}) for ${keys_url}"
elif [[ -z "$keys" ]]; then elif [[ -z "$keys" ]]; then
# HTTP 200 with an empty body: the account exists but simply has # HTTP 200 with an empty body: the account exists but simply has
@@ -882,16 +1000,27 @@ install_end4_dots() {
# --skip-alldeps: every dependency was already installed at ISO build time # --skip-alldeps: every dependency was already installed at ISO build time
# (customize_airootfs.sh runs ./setup install-deps), so this only copies # (customize_airootfs.sh runs ./setup install-deps), so this only copies
# configs, sets up services and builds the python venv from the warm cache. # configs, sets up services and builds the python venv from the warm cache.
if ! arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ # Both attempts run under a jump spinner (this step compiles the venv and
env HOME="$home_in_target" bash -c \ # pip bits): --show-output + the sh stderr-merge keep every line flowing
"cd ${repo_in_target} && ./setup install -f -s --skip-alldeps" \ # into $LOG, the frames render on the terminal, and stdin still feeds the
< <(yes i) >>"$LOG" 2>&1; then # `yes i` process substitution through to ./setup.
warn "end-4 setup failed, retrying files only (see $LOG)." if ! gum spin --show-output --spinner jump -- \
setups_done=false sh -c '"$@" 2>&1' sh \
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env HOME="$home_in_target" bash -c \ env HOME="$home_in_target" bash -c \
"cd ${repo_in_target} && ./setup install -f -s --skip-alldeps --skip-allsetups" \ "cd ${repo_in_target} && ./setup install -f -s --skip-alldeps" \
< <(yes i) >>"$LOG" 2>&1 || dots_ok=false < <(yes i) >>"$LOG"; then
warn "end-4 setup failed, retrying files only (see $LOG)."
setups_done=false
gum spin --show-output --spinner jump -- \
sh -c '"$@" 2>&1' sh \
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env HOME="$home_in_target" bash -c \
"cd ${repo_in_target} && ./setup install -f -s --skip-alldeps --skip-allsetups" \
< <(yes i) >>"$LOG" || dots_ok=false
# Both attempts' output exists only in $LOG - page it so the user can
# inspect what end-4 actually choked on.
page_log_since "$log_mark" "end-4 setup output (both attempts) - q to quit, scroll for more" || true
fi fi
rm -f "${TARGET}/etc/sudoers.d/99-end4-install" rm -f "${TARGET}/etc/sudoers.d/99-end4-install"
@@ -912,8 +1041,9 @@ install_end4_dots() {
fi fi
if [[ -f "${TARGET}/usr/lib/systemd/system/bluetooth.service" ]]; then if [[ -f "${TARGET}/usr/lib/systemd/system/bluetooth.service" ]]; then
arch-chroot "$TARGET" systemctl enable bluetooth.service >>"$LOG" 2>&1 \ run_logged "Could not enable bluetooth.service" \
|| warn "Could not enable bluetooth.service (see $LOG)." arch-chroot "$TARGET" systemctl enable bluetooth.service \
|| warn "Could not enable bluetooth.service (output above)."
fi fi
# `systemctl --user` cannot work in this chroot either: it needs # `systemctl --user` cannot work in this chroot either: it needs
# /run/user/UID/bus, which does not exist until the target boots (it only # /run/user/UID/bus, which does not exist until the target boots (it only
@@ -921,13 +1051,21 @@ install_end4_dots() {
# `enable` would write — the same absolute link systemctl produces. # `enable` would write — the same absolute link systemctl produces.
if [[ -f "${TARGET}/usr/lib/systemd/user/ydotool.service" ]]; then if [[ -f "${TARGET}/usr/lib/systemd/user/ydotool.service" ]]; then
local yd_wants="${TARGET}${home_in_target}/.config/systemd/user/default.target.wants" local yd_wants="${TARGET}${home_in_target}/.config/systemd/user/default.target.wants"
# stderr goes to $LOG so the failure warn below points at something # Capture instead of redirecting: on failure the output is both
# that actually records it (as the bluetooth block does). # appended to $LOG and paged to the user (as the bluetooth block's
if { mkdir -p "$yd_wants" \ # run_logged does), never kept in the log alone.
&& ln -sfn /usr/lib/systemd/user/ydotool.service "$yd_wants/ydotool.service"; } 2>>"$LOG"; then local yd_rc=0 yd_err=""
yd_err=$( { mkdir -p "$yd_wants" \
&& ln -sfn /usr/lib/systemd/user/ydotool.service "$yd_wants/ydotool.service"; } 2>&1 ) \
|| yd_rc=$?
if (( yd_rc == 0 )); then
log "ydotool.service enabled for ${USERNAME} (symlinked in user unit dir)." log "ydotool.service enabled for ${USERNAME} (symlinked in user unit dir)."
else else
warn "Could not enable ydotool.service for ${USERNAME} (see $LOG)." if [[ -n "$yd_err" ]]; then
printf '%s\n' "$yd_err" >>"$LOG"
printf '%s\n' "$yd_err" | page_output "ydotool.service symlink failed (exit ${yd_rc}) - full log: $LOG"
fi
warn "Could not enable ydotool.service for ${USERNAME} (output above)."
fi fi
fi fi
@@ -941,7 +1079,7 @@ install_end4_dots() {
# matching value counts as persisted. # matching value counts as persisted.
if ((log_mark > 0)) \ if ((log_mark > 0)) \
&& [[ -x "${TARGET}/usr/bin/dbus-run-session" && -x "${TARGET}/usr/bin/gsettings" ]]; then && [[ -x "${TARGET}/usr/bin/dbus-run-session" && -x "${TARGET}/usr/bin/gsettings" ]]; then
local gs_line gs_schema gs_key gs_val gs_got gs_norm gs_raw local gs_line gs_schema gs_key gs_val gs_got gs_norm gs_raw gs_err gs_rc
# Snapshot and parse first (the loop below appends to $LOG), keeping # Snapshot and parse first (the loop below appends to $LOG), keeping
# log order: a key set twice replays chronologically so the last # log order: a key set twice replays chronologically so the last
# write wins, exactly as end-4 ran it. Dedupe identical repeats only. # write wins, exactly as end-4 ran it. Dedupe identical repeats only.
@@ -971,12 +1109,21 @@ install_end4_dots() {
gs_val=${BASH_REMATCH[3]} gs_val=${BASH_REMATCH[3]}
# env -i: root's env is exactly what broke dconf; rebuild it for # env -i: root's env is exactly what broke dconf; rebuild it for
# the target user with only what gsettings/dbus need. # the target user with only what gsettings/dbus need.
if arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ # gsettings exits 0 even when the commit fails, so both the exit
# status AND the read-back below decide success; its output is
# kept in $LOG and paged on failure.
gs_err=""
gs_rc=0
gs_err=$(arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env -i HOME="$home_in_target" USER="$USERNAME" LOGNAME="$USERNAME" \ env -i HOME="$home_in_target" USER="$USERNAME" LOGNAME="$USERNAME" \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin \
XDG_CONFIG_HOME="${home_in_target}/.config" \ XDG_CONFIG_HOME="${home_in_target}/.config" \
dbus-run-session -- \ dbus-run-session -- \
gsettings set "$gs_schema" "$gs_key" "$gs_val" >>"$LOG" 2>&1; then gsettings set "$gs_schema" "$gs_key" "$gs_val" 2>&1) || gs_rc=$?
if [[ -n "$gs_err" ]]; then
printf '%s\n' "$gs_err" >>"$LOG"
fi
if (( gs_rc == 0 )); then
gs_got=$(arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ gs_got=$(arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env -i HOME="$home_in_target" USER="$USERNAME" LOGNAME="$USERNAME" \ env -i HOME="$home_in_target" USER="$USERNAME" LOGNAME="$USERNAME" \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin \
@@ -990,9 +1137,15 @@ install_end4_dots() {
if [[ "$gs_norm" == "$gs_val" ]]; then if [[ "$gs_norm" == "$gs_val" ]]; then
log "gsettings persisted: ${gs_schema} ${gs_key} = ${gs_val}" log "gsettings persisted: ${gs_schema} ${gs_key} = ${gs_val}"
else else
if [[ -n "$gs_err" ]]; then
printf '%s\n' "$gs_err" | page_output "gsettings ${gs_schema} ${gs_key} command output (q to quit) - full log: $LOG"
fi
warn "gsettings ${gs_schema} ${gs_key} did not persist (read back: ${gs_got:-nothing}, see $LOG)." warn "gsettings ${gs_schema} ${gs_key} did not persist (read back: ${gs_got:-nothing}, see $LOG)."
fi fi
else else
if [[ -n "$gs_err" ]]; then
printf '%s\n' "$gs_err" | page_output "gsettings set failed (exit ${gs_rc}) - output below, full log: $LOG"
fi
warn "Could not replay gsettings ${gs_schema} ${gs_key} (see $LOG)." warn "Could not replay gsettings ${gs_schema} ${gs_key} (see $LOG)."
fi fi
done done
@@ -1207,20 +1360,96 @@ install_bootloader() {
printf '\nGRUB_DISABLE_BOOTNEXT=true\n' >> "${TARGET}/etc/default/grub" printf '\nGRUB_DISABLE_BOOTNEXT=true\n' >> "${TARGET}/etc/default/grub"
fi fi
arch-chroot "$TARGET" grub-install \ # Two installs, both verified - this is the step whose failure strands a
--target=x86_64-efi --efi-directory=/boot --bootloader-id=ALVIN # machine with no bootable firmware entry:
# 1. --removable → writes the removable fallback \EFI\BOOT\BOOTX64.EFI
# to the ESP FIRST, so it exists no matter what the NVRAM step does
# 2. normal → registers the ALVIN entry in the firmware NVRAM
# Some firmwares drop their NVRAM entries over time (the classic
# boot-stranding failure); with the fallback on the ESP the firmware
# still finds GRUB and the machine is never bricked.
# Caveat: the fallback is an unsigned direct copy of grubx64.efi, so it
# only boots with Secure Boot disabled or in setup mode - which booting
# the unsigned Arch ISO already implies. If Secure Boot support is ever
# added, the fallback MUST be shimx64.efi instead - otherwise the
# firmware rejects BOOTX64.EFI.
if ! run_logged -s dot "grub-install (removable fallback) failed" \
arch-chroot "$TARGET" grub-install \
--target=x86_64-efi --efi-directory=/boot --bootloader-id=ALVIN \
--removable --recheck; then
die "grub-install --removable failed - \\EFI\\BOOT\\BOOTX64.EFI was not written (output above, full log: $LOG)."
fi
# Verify the fallback file really landed: $TARGET/boot IS the ESP mount,
# so this checks the exact file firmware will look for. This is the "is
# there any bootloader at all" gate - it must run before any NVRAM
# failure is allowed to abort the install.
if [[ ! -f "${TARGET}/boot/EFI/BOOT/BOOTX64.EFI" ]]; then
die "Fallback bootloader missing: ${TARGET}/boot/EFI/BOOT/BOOTX64.EFI was not created - this system could be unbootable if the firmware drops its boot entry."
fi
log "Bootloader: removable fallback present (EFI/BOOT/BOOTX64.EFI)"
if ! run_logged -s dot "grub-install (NVRAM boot entry) failed" \
arch-chroot "$TARGET" grub-install \
--target=x86_64-efi --efi-directory=/boot --bootloader-id=ALVIN \
--recheck; then
# grub copies its EFI binaries before it asks efibootmgr to register
# the entry, so "files present + non-zero exit" means only the NVRAM
# registration failed - fall through to the host-side creation below.
if [[ -f "${TARGET}/boot/EFI/ALVIN/grubx64.efi" ]]; then
warn "grub-install could not register the NVRAM entry (output above) - trying from the live medium."
log "Bootloader: grub-install NVRAM registration failed; EFI/ALVIN/grubx64.efi present"
else
# Note the fallback honestly: BOOTX64.EFI exists, but grub.cfg is
# only generated at the END of this function - an install aborted
# here leaves no config, so the fallback alone does NOT boot yet.
die "grub-install failed - EFI/ALVIN/grubx64.efi was not written (output above, full log: $LOG). \\EFI\\BOOT\\BOOTX64.EFI exists, but the install stops before grub-mkconfig runs, so no bootable configuration was written."
fi
fi
# Verify the NVRAM entry persisted. arch-chroot rbinds /sys and mounts
# efivarfs, so grub-install inside the chroot wrote the same variables
# efibootmgr reads here - there is no visibility difference; we verify
# and create on the live medium because that is the direct path to the
# real disk. Create the entry if grub-install could not persist it
# (write-protected or flaky NVRAM), then re-verify. Verification failure
# aborts the install.
local esp_partnum boot_entry
esp_partnum=$(lsblk -no PARTN "$(part 1)" 2>/dev/null | head -n1) || esp_partnum=""
esp_partnum=${esp_partnum:-1}
# Match the loader path, not just the label: a stale ALVIN entry left by
# a previous install on another disk would satisfy a bare label match.
boot_entry=$(efibootmgr -v 2>/dev/null \
| grep -iF '\EFI\ALVIN\grubx64.efi' | head -n1) || boot_entry=""
if [[ -z "$boot_entry" ]]; then
warn "No ALVIN entry in the firmware NVRAM - creating one now."
if ! run_logged "efibootmgr could not create the NVRAM boot entry" \
efibootmgr -c -d "$DISK" -p "$esp_partnum" -L ALVIN \
-l '\EFI\ALVIN\grubx64.efi'; then
die "Could not create the firmware NVRAM boot entry (output above). Refusing to finish: the install stops before grub-mkconfig runs, so \\EFI\\BOOT\\BOOTX64.EFI exists but is not bootable yet. Check UEFI setup (NVRAM write access) and re-run."
fi
boot_entry=$(efibootmgr -v 2>/dev/null \
| grep -iF '\EFI\ALVIN\grubx64.efi' | head -n1) || boot_entry=""
if [[ -z "$boot_entry" ]]; then
die "NVRAM boot entry still missing after efibootmgr -c. Refusing to finish: the install stops before grub-mkconfig runs, so the system is not bootable yet - check UEFI setup and re-run."
fi
fi
log "Bootloader: NVRAM entry verified: ${boot_entry}"
if [[ "$UNL0KR" == "true" ]]; then if [[ "$UNL0KR" == "true" ]]; then
# Mode B: grub.cfg is written in install_unl0kr together with the # Mode B: grub.cfg is written in install_unl0kr together with the
# final initramfs (a config generated now would not match it). # final initramfs (a config generated now would not match it).
log "grub-mkconfig deferred (Mode B, after unl0kr build)" log "grub-mkconfig deferred (Mode B, after unl0kr build)"
ok "Bootloader installed (config finalized after unl0kr)." ok "Bootloader installed (NVRAM entry + fallback verified; config finalized after unl0kr)."
return return
fi fi
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg if ! run_logged -s dot "grub-mkconfig failed" \
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg; then
die "grub-mkconfig failed - the installed system would boot into an empty GRUB menu (output above, full log: $LOG)."
fi
ok "Bootloader installed." ok "Bootloader installed (NVRAM entry + removable fallback verified)."
} }
# ── TPM2 auto-unlock ────────────────────────────────────────────────────────── # ── TPM2 auto-unlock ──────────────────────────────────────────────────────────
@@ -1280,8 +1509,16 @@ install_tpm() {
# $PASSWORD (stdin would instead trigger the ask-password agent and block # $PASSWORD (stdin would instead trigger the ask-password agent and block
# until someone typed it on a console). It only adds a keyslot - no # until someone typed it on a console). It only adds a keyslot - no
# --wipe-slot - so the passphrase slot always stays as a fallback. # --wipe-slot - so the passphrase slot always stays as a fallback.
if PASSWORD="$PASS" systemd-cryptenroll --tpm2-device=auto \ # Exported rather than put on the command line (env PASSWORD=...): argv
--tpm2-pcrs=0+7 "$(part 2)" >>"$LOG" 2>&1; then # is world-readable via /proc, the environment is not - and unset right
# after, so the secret exists only for the duration of this call.
export PASSWORD="$PASS"
local cenrc=0
run_logged "systemd-cryptenroll failed" \
systemd-cryptenroll --tpm2-device=auto \
--tpm2-pcrs=0+7 "$(part 2)" || cenrc=$?
unset PASSWORD
if (( cenrc == 0 )); then
TPM_STATUS="enrolled" TPM_STATUS="enrolled"
# Enrolment alone does nothing at boot: systemd-cryptsetup only uses # Enrolment alone does nothing at boot: systemd-cryptsetup only uses
# the TPM2 keyslot when the cmdline asks for it. install_bootloader # the TPM2 keyslot when the cmdline asks for it. install_bootloader
@@ -1291,12 +1528,13 @@ install_tpm() {
local luks_uuid local luks_uuid
luks_uuid=$(blkid -s UUID -o value "$(part 2)") luks_uuid=$(blkid -s UUID -o value "$(part 2)")
write_mode_a_cmdline "$luks_uuid" "rd.luks.options=${luks_uuid}=tpm2-device=auto" write_mode_a_cmdline "$luks_uuid" "rd.luks.options=${luks_uuid}=tpm2-device=auto"
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg >>"$LOG" 2>&1 \ run_logged -s dot "grub-mkconfig failed (rd.luks.options)" \
|| warn "grub-mkconfig failed after adding rd.luks.options (see $LOG) - TPM2 unlock will not activate at boot." arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg \
|| warn "grub-mkconfig failed after adding rd.luks.options (output above) - TPM2 unlock will not activate at boot."
ok "TPM2 auto-unlock enrolled and activated (rd.luks.options, PCR 0+7)." ok "TPM2 auto-unlock enrolled and activated (rd.luks.options, PCR 0+7)."
else else
TPM_STATUS="failed" TPM_STATUS="failed"
warn "TPM enrollment failed (see $LOG) - passphrase unlock still works." warn "TPM enrollment failed (output above) - passphrase unlock still works."
fi fi
# TPM-only unlock with an unencrypted /boot is weak: anyone with physical # TPM-only unlock with an unencrypted /boot is weak: anyone with physical
@@ -1336,10 +1574,12 @@ unl0kr_fallback_mode_a() {
TPM_STATUS="skipped-unl0kr-fallback" TPM_STATUS="skipped-unl0kr-fallback"
fi fi
arch-chroot "$TARGET" mkinitcpio -P >>"$LOG" 2>&1 \ run_logged -s dot "mkinitcpio -P failed (unl0kr fallback)" \
|| warn "mkinitcpio -P failed during unl0kr fallback (see $LOG)." arch-chroot "$TARGET" mkinitcpio -P \
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg >>"$LOG" 2>&1 \ || warn "mkinitcpio -P failed during unl0kr fallback (output above)."
|| warn "grub-mkconfig failed during unl0kr fallback (see $LOG)." run_logged -s dot "grub-mkconfig failed (unl0kr fallback)" \
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg \
|| warn "grub-mkconfig failed during unl0kr fallback (output above)."
warn "The system will ask for the LUKS passphrase at boot - test a reboot before relying on it." warn "The system will ask for the LUKS passphrase at boot - test a reboot before relying on it."
} }
@@ -1355,8 +1595,10 @@ install_unl0kr() {
# yay runs as the new user and needs passwordless sudo for this one call. # yay runs as the new user and needs passwordless sudo for this one call.
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "${TARGET}/etc/sudoers.d/99-unl0kr-install" echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "${TARGET}/etc/sudoers.d/99-unl0kr-install"
chmod 440 "${TARGET}/etc/sudoers.d/99-unl0kr-install" chmod 440 "${TARGET}/etc/sudoers.d/99-unl0kr-install"
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ run_logged -s jump -t "Building unl0kr from the AUR (this may take a while)" \
env HOME="$home_in_target" yay -S --noconfirm unl0kr >>"$LOG" 2>&1 || build_rc=$? "yay failed to build/install unl0kr" \
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env HOME="$home_in_target" yay -S --noconfirm unl0kr || build_rc=$?
rm -f "${TARGET}/etc/sudoers.d/99-unl0kr-install" rm -f "${TARGET}/etc/sudoers.d/99-unl0kr-install"
if (( build_rc != 0 )); then if (( build_rc != 0 )); then
@@ -1388,7 +1630,10 @@ install_unl0kr() {
cat "$hook" cat "$hook"
echo "-- install/unl0kr --" echo "-- install/unl0kr --"
cat "$hook_install" cat "$hook_install"
} >>"$LOG" 2>&1 || warn "Could not log the unl0kr hook files (see $LOG)." } >>"$LOG" 2>&1 || {
warn "Could not log the unl0kr hook files (see $LOG)."
page_log_tail "unl0kr hook file output (q to quit)" 40 || true
}
# Touchscreen/input stack for the initramfs: mkinitcpio fails the build # Touchscreen/input stack for the initramfs: mkinitcpio fails the build
# on a MODULES entry it cannot resolve, and -P applies the list to every # on a MODULES entry it cannot resolve, and -P applies the list to every
@@ -1423,12 +1668,14 @@ install_unl0kr() {
fi fi
info "Rebuilding initramfs with unl0kr..." info "Rebuilding initramfs with unl0kr..."
if ! arch-chroot "$TARGET" mkinitcpio -P >>"$LOG" 2>&1; then if ! run_logged -s dot "mkinitcpio -P failed (unl0kr image)" \
arch-chroot "$TARGET" mkinitcpio -P; then
unl0kr_fallback_mode_a "mkinitcpio -P failed" unl0kr_fallback_mode_a "mkinitcpio -P failed"
return 0 return 0
fi fi
# grub.cfg is written now, next to the final image (Mode B deferred it). # grub.cfg is written now, next to the final image (Mode B deferred it).
if ! arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg >>"$LOG" 2>&1; then if ! run_logged -s dot "grub-mkconfig failed (unl0kr image)" \
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg; then
unl0kr_fallback_mode_a "grub-mkconfig failed" unl0kr_fallback_mode_a "grub-mkconfig failed"
return 0 return 0
fi fi
@@ -1437,6 +1684,7 @@ install_unl0kr() {
# `lsinitcpio -a` prints the hook run order and the included module list. # `lsinitcpio -a` prints the hook run order and the included module list.
local analysis local analysis
if ! analysis=$(lsinitcpio -a "${TARGET}/boot/initramfs-linux.img" 2>>"$LOG"); then if ! analysis=$(lsinitcpio -a "${TARGET}/boot/initramfs-linux.img" 2>>"$LOG"); then
page_log_tail "lsinitcpio error output (q to quit)" 40 || true
unl0kr_fallback_mode_a "lsinitcpio could not read the image" unl0kr_fallback_mode_a "lsinitcpio could not read the image"
return 0 return 0
fi fi
@@ -1567,8 +1815,9 @@ install_login_manager() {
local u local u
for u in greetd sddm gdm; do for u in greetd sddm gdm; do
if [[ -f "${TARGET}/usr/lib/systemd/system/${u}.service" ]]; then if [[ -f "${TARGET}/usr/lib/systemd/system/${u}.service" ]]; then
arch-chroot "$TARGET" systemctl disable "$u.service" >>"$LOG" 2>&1 \ run_logged "Could not disable ${u}.service" \
|| warn "Could not disable ${u}.service (see $LOG)." arch-chroot "$TARGET" systemctl disable "$u.service" \
|| warn "Could not disable ${u}.service (output above)."
fi fi
done done
rm -f "${TARGET}/etc/systemd/system/getty@tty1.service.d/autologin.conf" rm -f "${TARGET}/etc/systemd/system/getty@tty1.service.d/autologin.conf"
@@ -1586,9 +1835,10 @@ install_login_manager() {
install_none_profile install_none_profile
return return
fi fi
if ! arch-chroot "$TARGET" pacman -S --noconfirm greetd greetd-tuigreet \ if ! run_logged -s dot -t "Installing greetd + tuigreet" \
>>"$LOG" 2>&1; then "greetd install failed" \
warn "greetd install failed (see $LOG) - falling back to 'none'." arch-chroot "$TARGET" pacman -S --noconfirm greetd greetd-tuigreet; then
warn "greetd install failed (output above) - falling back to 'none'."
LOGIN_MANAGER="none" LOGIN_MANAGER="none"
install_none_profile install_none_profile
return return
@@ -1596,8 +1846,9 @@ install_login_manager() {
# The package normally provides the greeter account; create it if # The package normally provides the greeter account; create it if
# this version does not (ArchWiki: user = "greeter"). # this version does not (ArchWiki: user = "greeter").
if ! arch-chroot "$TARGET" id greeter >/dev/null 2>&1; then if ! arch-chroot "$TARGET" id greeter >/dev/null 2>&1; then
arch-chroot "$TARGET" useradd -r -M -s /usr/bin/nologin greeter \ run_logged "Could not create the greeter user" \
>>"$LOG" 2>&1 || warn "Could not create the greeter user (see $LOG)." arch-chroot "$TARGET" useradd -r -M -s /usr/bin/nologin greeter \
|| warn "Could not create the greeter user (output above)."
fi fi
mkdir -p "${TARGET}/etc/greetd" mkdir -p "${TARGET}/etc/greetd"
cat > "${TARGET}/etc/greetd/config.toml" <<'TOML' cat > "${TARGET}/etc/greetd/config.toml" <<'TOML'
@@ -1608,11 +1859,13 @@ vt = 1
command = "tuigreet --cmd Hyprland" command = "tuigreet --cmd Hyprland"
user = "greeter" user = "greeter"
TOML TOML
if arch-chroot "$TARGET" systemctl enable greetd.service >>"$LOG" 2>&1; then if run_logged "Could not enable greetd.service" \
arch-chroot "$TARGET" systemctl disable getty@tty1.service >>"$LOG" 2>&1 || true arch-chroot "$TARGET" systemctl enable greetd.service; then
run_logged "Could not disable getty@tty1.service" \
arch-chroot "$TARGET" systemctl disable getty@tty1.service || true
ok "greetd + tuigreet enabled (Hyprland)." ok "greetd + tuigreet enabled (Hyprland)."
else else
warn "Could not enable greetd.service (see $LOG) - keeping getty@tty1 as the login path." warn "Could not enable greetd.service (output above) - keeping getty@tty1 as the login path."
fi fi
;; ;;
sddm) sddm)
@@ -1623,18 +1876,22 @@ TOML
install_none_profile install_none_profile
return return
fi fi
if ! arch-chroot "$TARGET" pacman -S --noconfirm sddm >>"$LOG" 2>&1; then if ! run_logged -s dot -t "Installing sddm" \
warn "sddm install failed (see $LOG) - falling back to 'none'." "sddm install failed" \
arch-chroot "$TARGET" pacman -S --noconfirm sddm; then
warn "sddm install failed (output above) - falling back to 'none'."
LOGIN_MANAGER="none" LOGIN_MANAGER="none"
install_none_profile install_none_profile
return return
fi fi
ensure_hyprland_session ensure_hyprland_session
if arch-chroot "$TARGET" systemctl enable sddm.service >>"$LOG" 2>&1; then if run_logged "Could not enable sddm.service" \
arch-chroot "$TARGET" systemctl disable getty@tty1.service >>"$LOG" 2>&1 || true arch-chroot "$TARGET" systemctl enable sddm.service; then
run_logged "Could not disable getty@tty1.service" \
arch-chroot "$TARGET" systemctl disable getty@tty1.service || true
ok "sddm enabled (Hyprland)." ok "sddm enabled (Hyprland)."
else else
warn "Could not enable sddm.service (see $LOG) - keeping getty@tty1 as the login path." warn "Could not enable sddm.service (output above) - keeping getty@tty1 as the login path."
fi fi
;; ;;
gdm) gdm)
@@ -1646,25 +1903,29 @@ TOML
install_none_profile install_none_profile
return return
fi fi
if ! arch-chroot "$TARGET" pacman -S --noconfirm gdm >>"$LOG" 2>&1; then if ! run_logged -s dot -t "Installing gdm" \
warn "gdm install failed (see $LOG) - falling back to 'none'." "gdm install failed" \
arch-chroot "$TARGET" pacman -S --noconfirm gdm; then
warn "gdm install failed (output above) - falling back to 'none'."
LOGIN_MANAGER="none" LOGIN_MANAGER="none"
install_none_profile install_none_profile
return return
fi fi
ensure_hyprland_session ensure_hyprland_session
if arch-chroot "$TARGET" systemctl enable gdm.service >>"$LOG" 2>&1; then if run_logged "Could not enable gdm.service" \
arch-chroot "$TARGET" systemctl disable getty@tty1.service >>"$LOG" 2>&1 || true arch-chroot "$TARGET" systemctl enable gdm.service; then
run_logged "Could not disable getty@tty1.service" \
arch-chroot "$TARGET" systemctl disable getty@tty1.service || true
ok "gdm enabled (Hyprland session)." ok "gdm enabled (Hyprland session)."
else else
warn "Could not enable gdm.service (see $LOG) - keeping getty@tty1 as the login path." warn "Could not enable gdm.service (output above) - keeping getty@tty1 as the login path."
fi fi
;; ;;
*) *)
# none: keep getty on tty1; install_none_profile writes the # none: keep getty on tty1; install_none_profile writes the
# login profile that starts the compositor. # login profile that starts the compositor.
arch-chroot "$TARGET" systemctl enable getty@tty1.service >>"$LOG" 2>&1 \ run_logged "Could not enable getty@tty1.service" \
|| true arch-chroot "$TARGET" systemctl enable getty@tty1.service || true
install_none_profile install_none_profile
;; ;;
esac esac
@@ -1829,6 +2090,16 @@ install_cleanup() {
gum style --foreground 10 --align center \ gum style --foreground 10 --align center \
"Run 'reboot' to boot into your new system." "Run 'reboot' to boot into your new system."
echo "" echo ""
# Debug aid: every failure was already paged at the moment it happened;
# this offers the complete record (all step output) on demand.
if gum confirm --affirmative "View install log" --negative "No thanks" \
"Open the full install log (${LOG})?"; then
gum pager <"$LOG" || true
fi
echo ""
# 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."
} }
# ── Main ────────────────────────────────────────────────────────────────────── # ── Main ──────────────────────────────────────────────────────────────────────