#!/usr/bin/env bash # archbaseos-guided-install.sh — guided (dialog-based) Arch Linux base installer # # If /answerfile.json exists (e.g. embedded via build.sh --preconf), all prompts # are answered from it. Missing fields fall back to interactive prompts. # -E: propagate ERR trap into functions/subshells; -e: abort on error; # -u: treat unset variables as errors; -o pipefail: fail on pipe errors. set -Eeuo pipefail ############################################ # LOGGING ############################################ LOGFILE="$HOME/archbaseos-guided-install.log" { echo echo "############################################" echo " Arch Guided Install Log - Started $(date)" echo "############################################" echo } >> "$LOGFILE" # Redirect all subsequent stdout/stderr to both the terminal and the log file. # `tee -a` appends so repeated runs accumulate without overwriting; process # substitution keeps the descriptor open for the full lifetime of the script. exec > >(tee -a "$LOGFILE") 2>&1 ############################################ # Error handler — TUI prompt to send log via croc ############################################ error_handler() { # Capture exit code before any other command can overwrite $?. # $LINENO is passed as $1 from the trap; default to '?' if absent. local exit_code=$? line_num="${1:-?}" echo "" >> "$LOGFILE" echo "ERROR: installer failed at line $line_num (exit code $exit_code)" >> "$LOGFILE" echo "" echo "Installation failed at line $line_num (exit code $exit_code)." read -rp "Send log via croc? [y/N]: " _croc_ans # ${_croc_ans,,} lowercases the answer so "Y" and "y" both match. if [[ "${_croc_ans,,}" == "y" ]]; then # Install croc on demand — it may not be present in the base live ISO. command -v croc &>/dev/null || pacman -Sy --noconfirm croc 2>/dev/null || true croc send "$LOGFILE" || true else echo "Log saved to: $LOGFILE" fi exit "$exit_code" } # Fire error_handler on any ERR signal, passing the current line number. # Single quotes prevent $LINENO from being evaluated at trap-definition time. trap 'error_handler $LINENO' ERR ############################################ # Helper Functions ############################################ ask_password() { # Prompt for a password and confirm it — BOTH shown in clear text (by request). # Loops until the two entries are identical and non-empty, so a typo can't # silently lock the operator out. The password is therefore typed exactly # twice (once + confirmation) and never again. The result is printed to stdout # for capture via $(...); the prompts and error messages go to stderr so they # never pollute the captured value. local label=$1 _p1 _p2 while true; do read -rp "$label: " _p1 if [[ -z "$_p1" ]]; then echo " Password cannot be empty — try again." >&2 continue fi read -rp "$label (confirm): " _p2 if [[ "$_p1" == "$_p2" ]]; then printf '%s' "$_p1" return 0 fi echo " Passwords do not match — try again." >&2 done } ask() { # Thin wrapper around read -rp so callers can capture the result via $(). local prompt=$1 local var read -rp "$prompt: " var echo "$var" } pause() { # Used before interactive hardware steps (e.g. FIDO2 enrollment) to give the # operator time to insert the key before the script continues. read -rp "Press ENTER to continue..." } # Returns the correct partition device for a given drive and partition number. # NVMe and eMMC use a 'p' separator (e.g. /dev/nvme0n1p1), others don't. part() { [[ "$1" == *nvme* || "$1" == *mmcblk* ]] && echo "${1}p${2}" || echo "${1}${2}"; } ############################################ # ANSWERFILE ############################################ # Allow an external override via the environment; default to /answerfile.json, # the path where build.sh --preconf embeds the file in the live ISO. ANSWERFILE="${ANSWERFILE:-/answerfile.json}" AF_MODE=false [[ -f "$ANSWERFILE" ]] && AF_MODE=true af_get() { # Reads one field from the answerfile using a jq filter expression. # `// empty` returns an empty string instead of "null" when the field is absent; # `|| true` prevents a jq parse error from aborting the script via set -e. local val val=$(jq -r "${1} // empty" "$ANSWERFILE" 2>/dev/null || true) if [[ -z "$val" ]]; then printf '%s' "${2:-}"; else printf '%s' "$val"; fi } af_bool() { # `// false` provides a safe default so missing boolean keys don't cause errors. local val; val=$(jq -r "${1} // false" "$ANSWERFILE" 2>/dev/null || true) [[ "$val" == "true" ]] && echo "YES" || echo "NO" } get_mac_suffix() { # Produce a compact MAC address (no colons) from the first non-loopback interface # to create a unique per-machine hostname suffix during unattended deployment. # The awk pattern skips "lo" by requiring the 3rd character to differ from 'o'. local mac mac=$(ip link show 2>/dev/null \ | awk '/^[0-9]+: [^l][^o]/{iface=1} iface && /link\/ether/{print $2; iface=0; exit}') # Remove colons via bash parameter substitution: ${var//pattern/replacement}. printf '%s' "${mac//:/}" } if $AF_MODE; then echo "== Arch Linux Guided Installer (answerfile mode) ==" # jq is required to parse the answerfile; it may not ship in the base live ISO. command -v jq &>/dev/null || pacman -Sy --noconfirm jq else echo "== Arch Linux FIDO2-Ready Installer ==" fi ############################################ # NETWORK CHECK ############################################ # Send one ICMP packet (-c1) with a 3-second timeout (-W3) to Cloudflare DNS. # In AF_MODE we warn but continue — packages may already be cached in the live env. if ! ping -c1 -W3 1.1.1.1 &>/dev/null; then if $AF_MODE; then echo "Warning: no internet connection detected — continuing in answerfile mode." else echo "No internet connection detected." echo " Wired: ensure the cable is plugged in." echo " WiFi: switch to another TTY (Alt+F2) and run: iwctl" echo "" read -rp "Press Enter once connected (or Ctrl+C to abort)..." if ! ping -c1 -W3 1.1.1.1 &>/dev/null; then echo "Warning: still offline. Packages cannot be downloaded without network." read -rp "Continue anyway? [y/N]: " _net_ans [[ "${_net_ans,,}" == "y" ]] || { echo "Aborted — no network."; exit 1; } fi fi fi ############################################ # Begin ############################################ # Each entry is "keymap-code|Human-readable name"; the pipe delimiter lets both # pieces of data live in a single array element without needing two parallel arrays. KEYMAPS=( "us|English US" "de|German" ) ############################################ # User input — gather EVERYTHING up front ############################################ # All operator input (including passwords) is collected here, before any # destructive action runs. Passwords are shown in clear text by request and are # each entered twice (once + confirmation) so a typo can't silently lock you out. # After this block the only remaining interaction is the single final "type YES" # gate plus any physical FIDO2 key taps. if $AF_MODE; then KERNEL=$(af_get '.kernel' 'linux') RAW_HOSTNAME=$(af_get '.hostname' '') # Append MAC suffix to make the hostname unique across machines when the same # answerfile is deployed to a fleet (lab rollout, reinstall, etc.). if [[ -n "$RAW_HOSTNAME" ]]; then HOSTNAME="${RAW_HOSTNAME}-$(get_mac_suffix)" else HOSTNAME="arch" fi USERNAME=$(af_get '.username' '') ENCRYPT_DISK=$(af_bool '.encrypt') ENABLE_FIDO_ROOT=$(af_bool '.fido2_root') ENABLE_FIDO_USER=$(af_bool '.fido2_user') RUN_TUI=$(af_bool '.run_tui') KEYMAP=$(af_get '.keymap' 'us') echo "Kernel: $KERNEL / Hostname: $HOSTNAME / Username: $USERNAME" echo "Encrypt: $ENCRYPT_DISK / FIDO2 root: $ENABLE_FIDO_ROOT / FIDO2 user: $ENABLE_FIDO_USER" echo "Keymap: $KEYMAP" else KERNEL=$(ask "Kernel (linux, linux-lts, linux-zen)") HOSTNAME=$(ask "Hostname") USERNAME=$(ask "Username") read -rp "Enable disk encryption? (YES/NO): " ENCRYPT_DISK # Default FIDO2 root to NO; only ask when encryption is active because FIDO2 # unlocking only makes sense on an encrypted partition. ENABLE_FIDO_ROOT="NO" if [[ "$ENCRYPT_DISK" == "YES" ]]; then read -rp "Enable FIDO2 for unlocking root? (YES/NO): " ENABLE_FIDO_ROOT fi read -rp "Enable FIDO2 for user login? (YES/NO): " ENABLE_FIDO_USER echo "" echo "Select keyboard layout for installed system:" for i in "${!KEYMAPS[@]}"; do # %%|* strips from the first '|' onward → keymap code. _km_code="${KEYMAPS[$i]%%|*}" # ##*| strips up to and including the last '|' → display name. _km_name="${KEYMAPS[$i]##*|}" printf " %d) %-14s (%s)\n" $((i+1)) "$_km_name" "$_km_code" done read -rp "Choice [1]: " _KM_CHOICE # Convert 1-based user input to 0-based array index; default to 1 if blank. _KM_CHOICE=$(( ${_KM_CHOICE:-1} - 1 )) if (( _KM_CHOICE >= 0 && _KM_CHOICE < ${#KEYMAPS[@]} )); then KEYMAP="${KEYMAPS[$_KM_CHOICE]%%|*}" else # Out-of-range input silently falls back to the first entry (us). KEYMAP="${KEYMAPS[0]%%|*}" fi # Whether to run the dotfiles TUI inside the chroot — asked now so the whole # back half of the install can proceed without further interaction. read -rp "Run dotfiles TUI setup inside chroot after base install? [YES/no]: " _TUI_IN _TUI_IN="${_TUI_IN:-YES}" [[ "${_TUI_IN^^}" == "YES" ]] && RUN_TUI="YES" || RUN_TUI="NO" fi # ── Target drive ────────────────────────────────────────────────────────────── # Print the block device layout so the operator can identify the correct disk. lsblk if $AF_MODE && [[ -n "$(af_get '.drive')" ]]; then DRIVE=$(af_get '.drive') echo "Drive (from answerfile): $DRIVE" else DRIVE=$(ask "Enter install drive (e.g., /dev/sda)") fi # ── Passwords (always interactive; shown in clear text, each entered twice) ──── # Never read from the answerfile. Captured once here and reused everywhere so the # operator is never prompted for them again mid-install. USERPASS=$(ask_password "Password for $USERNAME") # LUKS passphrase — only needed when encrypting. Reused below for luksFormat / # open / luksAddKey / cryptenroll so it is typed exactly once (plus confirmation), # never again during the destructive phase. LUKS_PASS="" if [[ "$ENCRYPT_DISK" == "YES" ]]; then LUKS_PASS=$(ask_password "Disk encryption (LUKS) passphrase") fi # ── Final confirmation ──────────────────────────────────────────────────────── # A single gate before anything destructive happens. Summarise the choices so the # operator can sanity-check them, then require one explicit all-caps YES. echo "" echo "──────────────────────────────────────────────" echo " Review your selections:" echo " Kernel: $KERNEL" echo " Hostname: $HOSTNAME" echo " Username: $USERNAME" echo " Keymap: $KEYMAP" echo " Encrypt disk: $ENCRYPT_DISK" echo " FIDO2 root: $ENABLE_FIDO_ROOT" echo " FIDO2 user: $ENABLE_FIDO_USER" echo " Run TUI: $RUN_TUI" echo " Target drive: $DRIVE" echo "──────────────────────────────────────────────" echo " WARNING: ALL DATA on $DRIVE will be PERMANENTLY ERASED." echo "" if $AF_MODE; then # Unattended deployment: no human to type YES — give a short abort window. echo "Answerfile mode — proceeding in 5 seconds (Ctrl-C to abort)..." sleep 5 else read -rp "Type YES (all caps) to begin installation: " _final_ans [[ "$_final_ans" == "YES" ]] || { echo "Aborted."; exit 1; } fi # Required packages — installed into the live environment before partitioning. # -d skips full dependency resolution for speed (these are standalone tools). # systemd-ukify included for Unified Kernel Image support if needed post-install. pacman -Syd --noconfirm parted cryptsetup libfido2 pam-u2f systemd-ukify jq ############################################ # Partitioning ############################################ # Read installed RAM in GiB for swap sizing; awk extracts the total-memory column. RAM_GB=$(free --giga | awk '/Mem/ {print $2}') # lsblk -b: bytes; -d: disk only (no partitions); -n: no header; -o SIZE: size column. DISK_GB=$(lsblk -dn -o SIZE -b "$DRIVE" | awk '{print int($1/1024/1024/1024)}') # Reserve 10 GiB for the EFI partition and 1 GiB for alignment/overhead. EFI_SIZE=10 SWAP_SIZE=$RAM_GB ROOT_SIZE=$((DISK_GB - SWAP_SIZE - EFI_SIZE - 1)) if (( ROOT_SIZE < 8 )); then echo "ERROR: Disk too small for layout." exit 1 fi echo "EFI=${EFI_SIZE}G, Root=${ROOT_SIZE}G, Swap=${SWAP_SIZE}G" # -s: script mode (no interactive prompts); GPT is required for UEFI booting. # `set 1 esp on` marks partition 1 as the EFI System Partition. # Starting at 1MiB aligns to SSD erase-block / spinner track boundaries. # The last partition uses 100% to consume all remaining space. parted -s "$DRIVE" mklabel gpt \ mkpart EFI fat32 1MiB "${EFI_SIZE}GiB" \ set 1 esp on \ mkpart ROOT "${EFI_SIZE}GiB" "$((EFI_SIZE + ROOT_SIZE))GiB" \ mkpart SWAP "$((EFI_SIZE + ROOT_SIZE))GiB" 100% # Resolve partition paths via `part()` which inserts the NVMe/eMMC 'p' suffix. EFI_PART=$(part "$DRIVE" 1) ROOT_PART=$(part "$DRIVE" 2) SWAP_PART=$(part "$DRIVE" 3) # FAT32 is mandated by the UEFI specification for the EFI System Partition. mkfs.fat -F32 "$EFI_PART" # mkswap writes the swap header; swapon activates it immediately for use during install. mkswap "$SWAP_PART" swapon "$SWAP_PART" ############################################ # Encryption (optional) ############################################ # Initialise to empty; gets set to a temp file path only when encryption is active. LUKS_BACKUP_KEY="" if [[ "$ENCRYPT_DISK" == "YES" ]]; then echo "Formatting LUKS2 root..." # --type luks2 uses the newer LUKS2 format required by systemd-cryptenroll for FIDO2. # Feed the pre-gathered passphrase on stdin (--key-file=-) and --batch-mode to # skip cryptsetup's own interactive "type uppercase YES" + passphrase prompts. printf '%s' "$LUKS_PASS" | cryptsetup luksFormat "$ROOT_PART" --type luks2 --batch-mode --key-file=- # Open (decrypt) the container; exposes it as /dev/mapper/cryptroot for formatting. printf '%s' "$LUKS_PASS" | cryptsetup open "$ROOT_PART" cryptroot --key-file=- # ── Auto-generate backup LUKS key ───────────────────────────────────────── # A random key enrolled in a second LUKS slot enables recovery without # the primary passphrase. Stored inside the encrypted volume so it is # only accessible when the system is unlocked. LUKS_BACKUP_KEY=$(mktemp /tmp/luks-backup-key.XXXXXX) # Read 64 bytes of entropy; base64-encode with -w0 to disable line wrapping. dd if=/dev/urandom bs=64 count=1 2>/dev/null | base64 -w0 > "$LUKS_BACKUP_KEY" echo "Enrolling auto-generated backup LUKS key..." # --key-file=- supplies the EXISTING passphrase (on stdin) to authorise the # operation; the positional file is the NEW backup key added to a second slot. printf '%s' "$LUKS_PASS" | cryptsetup luksAddKey "$ROOT_PART" "$LUKS_BACKUP_KEY" --key-file=- if [[ "$ENABLE_FIDO_ROOT" == "YES" ]]; then echo "Enroll FIDO2 key for LUKS2" # pause() gives the operator time to insert the FIDO2 hardware key before enrollment. pause # systemd-cryptenroll writes the FIDO2 credential into a LUKS2 token slot. # --fido2-with-client-pin=no: presence tap only, no PIN required at boot. # $PASSWORD supplies the existing passphrase to unlock a slot for enrollment, # so only the physical key tap remains interactive here. PASSWORD="$LUKS_PASS" systemd-cryptenroll "$ROOT_PART" --fido2-device=auto --fido2-with-client-pin=no fi # Format btrfs on the decrypted mapper device, not the raw partition. mkfs.btrfs /dev/mapper/cryptroot # Mount flat (no subvolume) first so the subvolume tree can be created. mount /dev/mapper/cryptroot /mnt # @ is the conventional root subvolume; @home separates user data for independent snapshots. btrfs subvolume create /mnt/@ btrfs subvolume create /mnt/@home # Unmount the flat view before remounting through named subvolumes. umount /mnt # Remount each subvolume at its intended mountpoint for pacstrap to populate. mount -o subvol=@ /dev/mapper/cryptroot /mnt mkdir -p /mnt/home mount -o subvol=@home /dev/mapper/cryptroot /mnt/home else echo "Skipping encryption — formatting root directly." # Same btrfs subvolume layout as the encrypted path for consistency. mkfs.btrfs "$ROOT_PART" mount "$ROOT_PART" /mnt btrfs subvolume create /mnt/@ btrfs subvolume create /mnt/@home umount /mnt mount -o subvol=@ "$ROOT_PART" /mnt mkdir -p /mnt/home mount -o subvol=@home "$ROOT_PART" /mnt/home fi mkdir -p /mnt/boot # Mount the EFI partition at /mnt/boot so GRUB and the kernel install inside the new tree. mount "$EFI_PART" /mnt/boot # Place backup key inside the new system (readable only by root, inside LUKS container) if [[ -n "$LUKS_BACKUP_KEY" ]]; then # `install -m 400` creates the destination with mode 0400 (root read-only) atomically, # avoiding an intermediate world-readable state from a separate chmod. install -m 400 "$LUKS_BACKUP_KEY" /mnt/_LUKS_BACKUP_KEY # Remove the temp file from the live environment immediately after copying. rm -f "$LUKS_BACKUP_KEY" echo "Backup LUKS key written to /_LUKS_BACKUP_KEY in new system." fi ############################################ # Base System Install ############################################ # lspci lists PCI devices; grep for VGA and 3D controller lines to identify the GPU. # `|| true` prevents set -e from aborting when no matching line is found. GPU_INFO=$(lspci | grep -E "VGA|3D" || true) GPU_PKGS="" if echo "$GPU_INFO" | grep -qi nvidia; then # nvidia-open is the open-source kernel module (Pascal+); preferred over nvidia-dkms. GPU_PKGS="nvidia-open" elif echo "$GPU_INFO" | grep -qi amd; then GPU_PKGS="xf86-video-amdgpu" elif echo "$GPU_INFO" | grep -qi intel; then GPU_PKGS="xf86-video-intel" fi # GPU_PKGS is intentionally unquoted so it word-splits into individual package names # (or expands to nothing when no GPU was detected). # shellcheck disable=SC2086 pacstrap /mnt \ base base-devel "$KERNEL" linux-firmware vim zsh git networkmanager grub efibootmgr \ btrfs-progs cryptsetup libfido2 pam-u2f sudo less jq $GPU_PKGS # -U: use UUIDs rather than device names so fstab entries survive hardware changes. genfstab -U /mnt >> /mnt/etc/fstab ############################################ # COPY ANSWERFILE INTO NEW SYSTEM ############################################ # Make the answerfile available inside the new system so the dotfiles TUI installer # can read the same selections without re-prompting. if $AF_MODE; then install -m 644 "$ANSWERFILE" /mnt/answerfile.json fi ############################################ # DOTFILES CLONE TO SKEL (with retry) ############################################ # Clone before entering the chroot so useradd -m inside will copy Dotfiles to # the new user's home. Interactive retry is available so a transient network # failure doesn't silently leave the system without dotfiles. echo "Cloning dotfiles into /mnt/etc/skel..." _clone_ok=false while ! $_clone_ok; do if git clone https://git.abdelbaki.eu/The_miro/Dotfiles.git /mnt/etc/skel/Dotfiles; then mkdir -p /mnt/etc/skel/{Desktop,Documents,Downloads,Music,Pictures,Public,Templates,Videos} _clone_ok=true else if $AF_MODE; then echo "Warning: dotfiles clone failed — continuing without dotfiles." _clone_ok=true else read -rp "Clone failed — retry? [y/N]: " _retry [[ "${_retry,,}" == "y" ]] || { echo "Skipping dotfiles — clone manually after first boot."; _clone_ok=true; } fi fi done ############################################ # CHROOT Configuration ############################################ # Capture the root partition UUID before entering the chroot; used in GRUB cmdline. ROOT_UUID=$(blkid -s UUID -o value "$ROOT_PART") # Variables are passed via `/usr/bin/env` on the arch-chroot command line rather # than `export` in the outer shell — this makes the environment explicit and # avoids leaking unrelated exported variables into the chroot. # The heredoc delimiter is single-quoted ('CHROOT_EOF') to prevent the outer # shell from expanding any $variables inside the heredoc body. arch-chroot /mnt /usr/bin/env \ HOSTNAME="$HOSTNAME" \ USERNAME="$USERNAME" \ USERPASS="$USERPASS" \ ENCRYPT_DISK="$ENCRYPT_DISK" \ ENABLE_FIDO_ROOT="$ENABLE_FIDO_ROOT" \ ENABLE_FIDO_USER="$ENABLE_FIDO_USER" \ ROOT_UUID="$ROOT_UUID" \ ROOT_PART="$ROOT_PART" \ KEYMAP="$KEYMAP" \ /bin/bash <<'CHROOT_EOF' set -euo pipefail # Uncomment (create) the UTF-8 locale entry so locale-gen can compile it. echo "en_US.UTF-8 UTF-8" > /etc/locale.gen locale-gen echo "LANG=en_US.UTF-8" > /etc/locale.conf # vconsole.conf sets the console keymap permanently for all TTY sessions. echo "KEYMAP=${KEYMAP}" > /etc/vconsole.conf # Symlink the zoneinfo file; hwclock --systohc syncs the RTC to current system time. ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime hwclock --systohc echo "$HOSTNAME" > /etc/hostname # Enable NetworkManager at boot so the installed system has networking on first login. systemctl enable NetworkManager # -m: create home from /etc/skel (dotfiles cloned before chroot); -G wheel: allow sudo; -s /bin/zsh: default shell. useradd -m -G wheel -s /bin/zsh "$USERNAME" # chpasswd reads "user:pass" from stdin to set the password non-interactively. echo "$USERNAME:$USERPASS" | chpasswd # Ensure all files under the new home dir are owned by the user (skel copy may be root-owned). chown -R "$USERNAME:$USERNAME" "/home/$USERNAME" # Grant wheel group full sudo access (ALL covers any host/user/group runas context). echo "%wheel ALL=(ALL) ALL" >> /etc/sudoers # Initramfs hook selection: # 1. FIDO2 root unlock: needs `systemd` + `sd-encrypt` for systemd-cryptsetup. # 2. Password-only LUKS: classic `encrypt` hook (no systemd dependency in initramfs). # 3. Unencrypted: minimal hook set — no encrypt hook needed. if [[ "$ENCRYPT_DISK" == "YES" && "$ENABLE_FIDO_ROOT" == "YES" ]]; then sed -i 's/^HOOKS=.*/HOOKS=(base udev systemd autodetect microcode modconf kms consolefont block sd-encrypt btrfs filesystems keyboard keymap fsck)/' /etc/mkinitcpio.conf elif [[ "$ENCRYPT_DISK" == "YES" ]]; then sed -i 's/^HOOKS=.*/HOOKS=(base udev autodetect microcode modconf kms consolefont block encrypt btrfs filesystems keyboard keymap fsck)/' /etc/mkinitcpio.conf else sed -i 's/^HOOKS=.*/HOOKS=(base udev autodetect microcode modconf kms consolefont block btrfs filesystems keyboard fsck)/' /etc/mkinitcpio.conf fi # -P regenerates all installed kernel presets (more thorough than -p ). mkinitcpio -P # GRUB if [[ "$ENCRYPT_DISK" == "YES" ]]; then if [[ "$ENABLE_FIDO_ROOT" == "YES" ]]; then # systemd-cryptsetup (sd-encrypt hook) reads rd.luks.name==. # rd.luks.options=fido2-device=auto instructs it to probe for a FIDO2 token. GRUB_CMDLINE="rd.luks.name=$ROOT_UUID=cryptroot rd.luks.options=fido2-device=auto root=/dev/mapper/cryptroot" else # Classic encrypt hook syntax: cryptdevice=UUID=:. GRUB_CMDLINE="cryptdevice=UUID=$ROOT_UUID:cryptroot root=/dev/mapper/cryptroot" fi else # Unencrypted btrfs: root= by UUID; rootflags selects the @ subvolume. GRUB_CMDLINE="root=UUID=${ROOT_UUID} rootflags=subvol=@" fi # Inject the kernel command line; `|` delimiter avoids escaping `/` in device paths. sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\"$GRUB_CMDLINE\"|" /etc/default/grub # Install GRUB to the EFI partition; --bootloader-id names the NVRAM/EFI menu entry. grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB # Generate grub.cfg from /etc/default/grub and discovered kernels/initramfs images. grub-mkconfig -o /boot/grub/grub.cfg # User login FIDO2 — directory + PAM only; key enrollment happens outside chroot if [[ "$ENABLE_FIDO_USER" == "YES" ]]; then # Create the Yubico config dir that pam_u2f expects for the u2f_keys file. # `mkdir -p` here creates ~/.config itself as root, so chown the whole tree # (not just Yubico/) — otherwise ~/.config stays root-owned and every later # user-level step (shell-setup symlinks, systemd --user timers, Hyprland's # own ~/.config/hypr) fails with "Permission denied". mkdir -p "/home/$USERNAME/.config/Yubico" chown -R "$USERNAME:$USERNAME" "/home/$USERNAME/.config" # `cue` option: pam_u2f prints a prompt so the user knows to touch the key. echo "auth required pam_u2f.so cue" >> /etc/pam.d/system-local-login fi CHROOT_EOF # pamu2fcfg must run outside arch-chroot: inside the chroot the host's udev manages # /dev/hidraw* permissions and the new user has no access to the device. if [[ "$ENABLE_FIDO_USER" == "YES" ]]; then echo "Enrolling FIDO2 key for user login (outside chroot)..." U2F_KEYFILE="/mnt/home/${USERNAME}/.config/Yubico/u2f_keys" mkdir -p "/mnt/home/${USERNAME}/.config/Yubico" # -o and -i set the origin/app-id to the hostname, scoping the credential so it # cannot be replayed on a different system's PAM stack. pamu2fcfg -u "$USERNAME" -o "pam://$HOSTNAME" -i "pam://$HOSTNAME" > "$U2F_KEYFILE" # Query the UID/GID from inside the chroot to get the correct numeric IDs, since # the live environment may have different /etc/passwd entries. _NEWUID=$(arch-chroot /mnt id -u "$USERNAME" 2>/dev/null || echo "1000") _NEWGID=$(arch-chroot /mnt id -g "$USERNAME" 2>/dev/null || echo "1000") # chown the whole ~/.config tree: the mkdir above created ~/.config as root, # so reclaiming only Yubico/ would leave ~/.config itself root-owned. chown -R "$_NEWUID:$_NEWGID" "/mnt/home/${USERNAME}/.config" # 600: only the owning user can read or write the key file. chmod 600 "$U2F_KEYFILE" echo "FIDO2 key enrolled for $USERNAME." fi ############################################ # DOTFILES SETUP (in-chroot, optional) ############################################ # RUN_TUI was gathered up front (answerfile field or the initial prompt), so no # additional interaction is needed at this point. _DO_TUI="${RUN_TUI}" if [[ "${_DO_TUI^^}" == "YES" ]]; then # Grant temporary passwordless sudo so the TUI installer can call pacman/yay # inside the chroot without a password. Removed immediately after the script exits. echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" \ | arch-chroot /mnt tee /etc/sudoers.d/99-setup-nopasswd > /dev/null echo "Running tui-install.sh as ${USERNAME} inside chroot..." # `runuser -u` switches to the unprivileged user inside the chroot so that # AUR helpers and dotfiles are owned/built by the correct UID. arch-chroot /mnt runuser -u "${USERNAME}" -- \ bash "/home/${USERNAME}/Dotfiles/setup/tui-install.sh" \ || echo "Warning: tui-install exited with errors — check ~/dotfiles-install.log in the new system." # Remove the temporary no-password sudoers drop-in after setup completes. arch-chroot /mnt rm -f /etc/sudoers.d/99-setup-nopasswd fi # Remove answerfile from new system after setup completes # (it contains drive paths and config that are no longer needed post-install). if $AF_MODE && [[ -f /mnt/answerfile.json ]]; then rm -f /mnt/answerfile.json fi # Copy the install log into /boot so it is readable before the first login. cp "$LOGFILE" /mnt/boot/ 2>/dev/null || true echo "Installation complete! Log saved to /mnt/boot/$(basename "$LOGFILE")" echo " umount -R /mnt && reboot" if [[ "${_DO_TUI^^}" != "YES" ]]; then echo echo "After first boot, login as ${USERNAME} and run:" echo " ~/Dotfiles/setup/tui-install.sh" fi if [[ "$ENCRYPT_DISK" == "YES" ]]; then echo echo "LUKS backup key stored at /_LUKS_BACKUP_KEY in the new system." echo "Keep this file secure — it can unlock the root partition." fi