diff --git a/public/archsetup.sh b/public/archsetup.sh new file mode 100755 index 0000000..769bf4a --- /dev/null +++ b/public/archsetup.sh @@ -0,0 +1,703 @@ +#!/usr/bin/env bash +# Interactive, single-file Arch Linux installer for the official live ISO. + +set -Eeuo pipefail + +readonly LOG_FILE=/tmp/arch-installer.log +readonly MNT=/mnt +exec 4>>"$LOG_FILE" + +BOOT_MODE="" +CPU_VENDOR="" +TARGET_DISK="" +TARGET_DISK_LABEL="Not configured" +PARTITION_MODE="" +FILESYSTEM="ext4" +KERNEL="linux" +HOSTNAME="archlinux" +TIMEZONE="America/Chicago" +LOCALE="en_US.UTF-8" +KEYMAP="us" +NETWORK="NetworkManager" +BOOTLOADER="GRUB" +ENABLE_TESTING_REPOS=0 +INSTALL_YAY=0 +ROOT_PASSWORD="" +ROOT_PASSWORD_SET=0 +INSTALL_STARTED=0 +CURRENT_STAGE="Starting" + +declare -a DISKS=() DISK_MENU=() EXTRA_PACKAGES=() USERS=() +declare -A USER_PASSWORDS=() USER_ADMINS=() +declare -a MAN_PARTS=() MAN_ROLES=() MAN_MOUNTS=() MAN_FS=() MAN_FORMAT=() + +cleanup_secrets() { + ROOT_PASSWORD="" + local user + for user in "${USERS[@]}"; do USER_PASSWORDS["$user"]=""; done +} + +on_error() { + local rc=$? line=${BASH_LINENO[0]:-unknown} + cleanup_secrets + printf 'ERROR rc=%s stage=%s line=%s\n' "$rc" "$CURRENT_STAGE" "$line" >&4 + if command -v dialog >/dev/null 2>&1 && [[ -r /dev/tty ]]; then + dialog --title "Installation failed" --msgbox \ + "A critical error occurred during:\n\n$CURRENT_STAGE\n\nSee $LOG_FILE for details.\nThe installer will not continue." 12 68 \ + /dev/tty 2>/dev/tty || true + else + printf 'Installation failed during %s. See %s\n' "$CURRENT_STAGE" "$LOG_FILE" >/dev/tty 2>/dev/null || true + fi + exit "$rc" +} +trap on_error ERR +trap cleanup_secrets EXIT + +log() { printf '[%(%F %T)T] %s\n' -1 "$*" >&4; } + +# dialog writes answers to stderr. This ordering captures stderr while keeping +# its screen output and keyboard input attached to the terminal, never stdin. +dlg() { dialog "$@" &1 >/dev/tty; } +msg() { dialog --title "Arch Linux Installer" --msgbox "$1" "${2:-12}" "${3:-70}" /dev/tty 2>/dev/tty; } +yesno() { dialog --title "Arch Linux Installer" --yesno "$1" "${2:-12}" "${3:-70}" /dev/tty 2>/dev/tty; } + +run_logged() { + log "+ $*" + "$@" >>"$LOG_FILE" 2>&1 +} + +stage() { + CURRENT_STAGE=$1 + log "STAGE: $CURRENT_STAGE" + dialog --title "Installing Arch Linux" --infobox \ + "$CURRENT_STAGE\n\nPlease wait. Detailed output: $LOG_FILE" 8 68 \ + /dev/tty 2>/dev/tty +} + +die() { msg "$1" 12 72; log "FATAL: $1"; exit 1; } + +require_live_environment() { + [[ $EUID -eq 0 ]] || { printf 'This installer must run as root.\n' >&2; exit 1; } + [[ -r /etc/arch-release ]] || { printf 'This installer requires the Arch Linux live environment.\n' >&2; exit 1; } + [[ -d /run/archiso ]] || { printf 'This installer must be run from the official Arch Linux ISO.\n' >&2; exit 1; } + [[ -r /dev/tty && -w /dev/tty ]] || { printf 'An interactive /dev/tty is required.\n' >&2; exit 1; } +} + +install_dialog() { + if ! command -v dialog >/dev/null 2>&1; then + printf 'Installing dialog in the live environment...\n' >/dev/tty + pacman -Sy --noconfirm dialog >>"$LOG_FILE" 2>&1 || { printf 'Could not install dialog. See %s\n' "$LOG_FILE" >/dev/tty; exit 1; } + fi +} + +check_internet() { + if ! curl -fsS --connect-timeout 8 https://archlinux.org/ >/dev/null; then + die "Internet connectivity check failed. Connect to the internet and try again." + fi +} + +sync_clock() { + run_logged timedatectl set-ntp true + local i + for i in {1..15}; do + [[ $(timedatectl show -p NTPSynchronized --value 2>/dev/null || true) == yes ]] && return 0 + sleep 1 + done + log "Warning: NTP did not report synchronized within 15 seconds" +} + +check_commands() { + local -a required=(arch-chroot awk blkid cfdisk curl findmnt genfstab grep lsblk mkfs.ext4 mkfs.fat mkswap mount pacstrap partprobe sed sfdisk swapoff swapon udevadm wipefs) + local -a missing=() + local cmd + for cmd in "${required[@]}"; do command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd"); done + ((${#missing[@]} == 0)) || die "Required live-environment commands are missing:\n\n${missing[*]}" +} + +detect_platform() { + [[ -d /sys/firmware/efi ]] && BOOT_MODE=UEFI || BOOT_MODE=BIOS + case $(awk -F: '/vendor_id/{gsub(/[[:space:]]/,"",$2); print $2; exit}' /proc/cpuinfo) in + GenuineIntel) CPU_VENDOR=Intel ;; + AuthenticAMD) CPU_VENDOR=AMD ;; + *) CPU_VENDOR=Other ;; + esac +} + +scan_disks() { + DISKS=(); DISK_MENU=() + local path type model size ro rm + while IFS=$'\t' read -r path type model size ro rm; do + [[ $type == disk && $ro == 0 ]] || continue + [[ $path != /dev/loop* && $path != /dev/zram* && $path != /dev/sr* && $path != /dev/fd* ]] || continue + [[ -b $path ]] || continue + lsblk -nrpo MOUNTPOINT "$path" | grep -Eq '^/run/archiso(/|$)' && continue + DISKS+=("$path") + model=${model:-Unknown model} + DISK_MENU+=("$path" "$size $model") + done < <(lsblk -dnP -o PATH,TYPE,MODEL,SIZE,RO,RM | awk ' + { p=t=m=s=ro=rm=""; while (match($0,/([A-Z]+)="([^"]*)"/,a)) { if(a[1]=="PATH")p=a[2]; if(a[1]=="TYPE")t=a[2]; if(a[1]=="MODEL")m=a[2]; if(a[1]=="SIZE")s=a[2]; if(a[1]=="RO")ro=a[2]; if(a[1]=="RM")rm=a[2]; $0=substr($0,RSTART+RLENGTH) } print p"\t"t"\t"m"\t"s"\t"ro"\t"rm }') + ((${#DISKS[@]} > 0)) || die "No suitable installation disks were detected." +} + +preflight() { + require_live_environment + install_dialog + CURRENT_STAGE="Preflight checks" + check_internet + sync_clock + check_commands + detect_platform + scan_disks + command -v mkfs.btrfs >/dev/null 2>&1 || run_logged pacman -Sy --noconfirm btrfs-progs +} + +select_disk() { + local choice + choice=$(dlg --title "Select Target Disk" --menu \ + "Choose carefully. No disk is selected automatically." 18 76 10 "${DISK_MENU[@]}") || return 0 + TARGET_DISK=$choice + TARGET_DISK_LABEL="$choice ($(lsblk -dn -o SIZE,MODEL "$choice" | sed 's/^[[:space:]]*//'))" +} + +configure_disk() { + local mode + mode=$(dlg --title "Disk Configuration" --menu "Choose a partitioning method. Disk changes occur only after final confirmation." 14 72 4 \ + automatic "Automatic Partitioning" manual "Manual Partitioning with cfdisk") || return 0 + select_disk + [[ -n $TARGET_DISK ]] || return 0 + PARTITION_MODE=$mode + if [[ $BOOT_MODE == BIOS && $mode == automatic ]]; then FILESYSTEM=ext4; fi +} + +choose_filesystem() { + if [[ $BOOT_MODE == BIOS && $PARTITION_MODE == automatic ]]; then + msg "Automatic BIOS installations require ext4." + FILESYSTEM=ext4 + return + fi + local choice + choice=$(dlg --title "Filesystem" --menu "Select the root filesystem." 12 54 2 ext4 "Ext4" btrfs "Btrfs") || return 0 + FILESYSTEM=$choice +} + +choose_kernel() { + local choice + choice=$(dlg --title "Kernel" --menu "Select a kernel." 13 55 3 linux "Standard" linux-lts "Long-term support" linux-zen "Desktop-tuned") || return 0 + KERNEL=$choice +} + +input_value() { + local variable=$1 title=$2 prompt=$3 current value + current=${!variable} + value=$(dlg --title "$title" --inputbox "$prompt" 10 68 "$current") || return 0 + [[ -n $value ]] || { msg "$title cannot be empty."; return; } + printf -v "$variable" '%s' "$value" +} + +set_root_password() { + local p1 p2 + p1=$(dlg --title "Root Password" --insecure --passwordbox "Enter the root password. Leave empty to keep the root account locked." 10 68) || return 0 + if [[ -z $p1 ]]; then ROOT_PASSWORD=""; ROOT_PASSWORD_SET=0; return; fi + p2=$(dlg --title "Root Password" --insecure --passwordbox "Confirm the root password." 10 68) || { p1=""; return 0; } + if [[ $p1 != "$p2" ]]; then p1=""; p2=""; msg "Passwords did not match."; return; fi + ROOT_PASSWORD=$p1; p1=""; p2=""; ROOT_PASSWORD_SET=1 +} + +valid_username() { [[ $1 =~ ^[a-z_][a-z0-9_-]*$ && ${#1} -le 32 ]]; } + +manage_users() { + local action user p1 p2 admin summary + while true; do + summary=${USERS[*]:-None} + action=$(dlg --title "Users" --menu "Configured: $summary" 15 65 5 add "Add a user" remove "Remove a user" done "Return to main menu") || return 0 + case $action in + add) + user=$(dlg --title "Add User" --inputbox "Username:" 9 55) || continue + valid_username "$user" || { msg "Invalid username."; continue; } + [[ $user != root && $user != _yaybuilder && -z ${USER_PASSWORDS[$user]+x} ]] || { msg "That user already exists or is reserved."; continue; } + p1=$(dlg --title "User Password" --insecure --passwordbox "Password for $user:" 10 60) || continue + [[ -n $p1 ]] || { msg "User passwords may not be empty."; continue; } + p2=$(dlg --title "User Password" --insecure --passwordbox "Confirm password for $user:" 10 60) || { p1=""; continue; } + [[ $p1 == "$p2" ]] || { p1=""; p2=""; msg "Passwords did not match."; continue; } + admin=0; yesno "Grant $user administrator (sudo) privileges?" 10 60 && admin=1 + USERS+=("$user"); USER_PASSWORDS["$user"]=$p1; USER_ADMINS["$user"]=$admin + p1=""; p2="" + ;; + remove) + ((${#USERS[@]})) || { msg "No users are configured."; continue; } + local -a items=(); local u + for u in "${USERS[@]}"; do items+=("$u" "Remove this user"); done + user=$(dlg --title "Remove User" --menu "Select a user." 15 55 7 "${items[@]}") || continue + local -a kept=() + for u in "${USERS[@]}"; do [[ $u == "$user" ]] || kept+=("$u"); done + USER_PASSWORDS["$user"]=""; unset 'USER_PASSWORDS[$user]' 'USER_ADMINS[$user]'; USERS=("${kept[@]}") + ;; + done) return ;; + esac + done +} + +set_extra_packages() { + local input + input=$(dlg --title "Additional Packages" --inputbox "Enter official repository package names separated by spaces:" 10 74 "${EXTRA_PACKAGES[*]:-}") || return 0 + read -r -a EXTRA_PACKAGES <<<"$input" +} + +toggle_testing_repos() { + if ((ENABLE_TESTING_REPOS)); then + ENABLE_TESTING_REPOS=0 + return + fi + yesno "Enable Arch's core-testing and extra-testing repositories?\n\nTesting packages may be unstable and can require manual intervention. The repositories will be enabled in both the live environment and installed system." 14 72 && ENABLE_TESTING_REPOS=1 +} + +toggle_yay() { + if ((INSTALL_YAY)); then INSTALL_YAY=0; else INSTALL_YAY=1; fi +} + +main_menu() { + local choice root_status users_status extras_status partition_status testing_status yay_status + while true; do + root_status="Not set"; ((ROOT_PASSWORD_SET)) && root_status="Set" + users_status=${USERS[*]:-None}; extras_status=${EXTRA_PACKAGES[*]:-None} + partition_status="Not configured" + [[ -n $PARTITION_MODE ]] && partition_status="${PARTITION_MODE^}: $TARGET_DISK" + testing_status=Disabled; ((ENABLE_TESTING_REPOS)) && testing_status=Enabled + yay_status="Do not install"; ((INSTALL_YAY)) && yay_status="Install yay" + choice=$(dlg --title "Arch Linux Installer" --menu "Boot mode: $BOOT_MODE CPU: $CPU_VENDOR" 23 78 14 \ + disk "Disk Configuration $partition_status" \ + fs "Filesystem $FILESYSTEM" \ + kernel "Kernel $KERNEL" \ + host "Hostname $HOSTNAME" \ + tz "Timezone $TIMEZONE" \ + locale "Locale $LOCALE" \ + keymap "Keymap $KEYMAP" \ + network "Network $NETWORK" \ + rootpw "Root Password $root_status" \ + users "Users $users_status" \ + packages "Additional Packages $extras_status" \ + testing "Testing Repositories $testing_status" \ + yay "Yay AUR Helper $yay_status" \ + boot "Bootloader $BOOTLOADER" \ + install "Install" exit "Exit") || choice=exit + case $choice in + disk) configure_disk ;; fs) choose_filesystem ;; kernel) choose_kernel ;; + host) input_value HOSTNAME Hostname "Enter the system hostname:" ;; + tz) input_value TIMEZONE Timezone "Enter an IANA timezone (for example America/Chicago):" ;; + locale) input_value LOCALE Locale "Enter a UTF-8 locale (for example en_US.UTF-8):" ;; + keymap) input_value KEYMAP Keymap "Enter a console keymap (for example us):" ;; + network) msg "NetworkManager is the supported network service." ;; + rootpw) set_root_password ;; users) manage_users ;; packages) set_extra_packages ;; + testing) toggle_testing_repos ;; yay) toggle_yay ;; + boot) msg "GRUB is the supported bootloader." ;; + install) confirm_install && return 0 ;; + exit) yesno "Exit the installer? No disk changes have been made." 10 60 && exit 0 ;; + esac + done +} + +validate_config() { + [[ -n $TARGET_DISK && -b $TARGET_DISK ]] || { msg "Select a valid target disk."; return 1; } + [[ $PARTITION_MODE == automatic || $PARTITION_MODE == manual ]] || { msg "Configure partitioning first."; return 1; } + [[ $FILESYSTEM == ext4 || $FILESYSTEM == btrfs ]] || { msg "Invalid filesystem."; return 1; } + [[ $HOSTNAME =~ ^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$ ]] || { msg "The hostname is invalid."; return 1; } + [[ $TIMEZONE =~ ^[A-Za-z0-9_+-]+(/[A-Za-z0-9_+-]+)+$ && $TIMEZONE != *..* ]] || { msg "The timezone name is invalid."; return 1; } + [[ -e /usr/share/zoneinfo/$TIMEZONE ]] || { msg "Timezone '$TIMEZONE' does not exist."; return 1; } + [[ $LOCALE =~ ^[A-Za-z_]+\.[A-Za-z0-9@_-]+$ ]] || { msg "The locale name is invalid."; return 1; } + [[ $KEYMAP =~ ^[A-Za-z0-9_-]+$ ]] || { msg "The keymap name is invalid."; return 1; } + local locale_re=${LOCALE//./\\.} + grep -Eq "^[#[:space:]]*${locale_re}[[:space:]]" /etc/locale.gen || { msg "Locale '$LOCALE' is not available in /etc/locale.gen."; return 1; } + local user + if ((ROOT_PASSWORD_SET == 0 && ${#USERS[@]} == 0)); then msg "Set a root password or create at least one normal user."; return 1; fi + for user in "${USERS[@]}"; do [[ -n ${USER_PASSWORDS[$user]:-} ]] || { msg "Missing password for $user."; return 1; }; done + local package + for package in "${EXTRA_PACKAGES[@]}"; do + [[ $package =~ ^[A-Za-z0-9@._+:-]+$ && $package != -* ]] || { msg "Invalid package name: $package"; return 1; } + done + return 0 +} + +confirm_install() { + validate_config || return 1 + local warning partition_label + partition_label=${PARTITION_MODE^} + if [[ $PARTITION_MODE == automatic ]]; then + warning="ALL DATA ON $TARGET_DISK WILL BE DESTROYED." + else + warning="cfdisk will edit $TARGET_DISK. Only partitions you explicitly mark for formatting will be formatted." + fi + local testing_summary=Disabled yay_summary="Do not install" + ((ENABLE_TESTING_REPOS)) && testing_summary=Enabled + ((INSTALL_YAY)) && yay_summary="Install yay" + yesno "Installation Summary\n\nBoot Mode: $BOOT_MODE\nTarget Disk: $TARGET_DISK_LABEL\nPartitioning: $partition_label\nFilesystem: $FILESYSTEM\nKernel: $KERNEL\nHostname: $HOSTNAME\nTimezone: $TIMEZONE\nLocale: $LOCALE\nKeymap: $KEYMAP\nNetwork: $NETWORK\nUsers: ${USERS[*]:-None}\nExtra Packages: ${EXTRA_PACKAGES[*]:-None}\nTesting Repos: $testing_summary\nYay AUR Helper: $yay_summary\nBootloader: $BOOTLOADER\n\n$warning\n\nProceed?" 0 78 || return 1 + if [[ $PARTITION_MODE == automatic ]]; then + local typed + typed=$(dlg --title "Final Destructive Confirmation" --inputbox "Type the exact disk path to authorize erasing it:\n\n$TARGET_DISK" 12 68) || return 1 + [[ $typed == "$TARGET_DISK" ]] || { msg "Disk path did not match. Installation cancelled."; return 1; } + fi + INSTALL_STARTED=1 + return 0 +} + +assert_target_disk() { + [[ $INSTALL_STARTED == 1 && -n $TARGET_DISK && -b $TARGET_DISK ]] || die "Unsafe or missing target disk." + local d + for d in "${DISKS[@]}"; do [[ $d == "$TARGET_DISK" ]] && return 0; done + die "The selected target is no longer in the validated disk list." +} + +partition_path() { + local disk=$1 number=$2 + [[ $disk =~ [0-9]$ ]] && printf '%sp%s' "$disk" "$number" || printf '%s%s' "$disk" "$number" +} + +unmount_target() { + if findmnt -rn -M "$MNT" >/dev/null 2>&1; then + local source + source=$(findmnt -rn -o SOURCE -M "$MNT") + [[ $source == "$TARGET_DISK"* ]] || die "$MNT is mounted from $source, not the selected disk. Unmount it manually before continuing." + run_logged umount -R "$MNT" + fi +} + +automatic_partition() { + assert_target_disk + unmount_target + if lsblk -nrpo MOUNTPOINT "$TARGET_DISK" | grep -q '/'; then + die "A partition on $TARGET_DISK is mounted outside $MNT. Unmount it before installing." + fi + local child + while read -r child; do + [[ -n $child ]] && run_logged swapoff "$child" + done < <(lsblk -rpn -o PATH,TYPE "$TARGET_DISK" | awk '$2=="part"{print $1}' | while read -r child; do swapon --noheadings --raw --output NAME | grep -Fx "$child" || true; done) + run_logged wipefs --all --force "$TARGET_DISK" + if [[ $BOOT_MODE == BIOS ]]; then + printf 'label: dos\nunit: sectors\n\n,,83,*\n' | sfdisk "$TARGET_DISK" >>"$LOG_FILE" 2>&1 + else + printf 'label: gpt\nunit: sectors\n\n,1GiB,U\n,,L\n' | sfdisk "$TARGET_DISK" >>"$LOG_FILE" 2>&1 + fi + run_logged partprobe "$TARGET_DISK" + run_logged udevadm settle +} + +manual_partition_editor() { + assert_target_disk + CURRENT_STAGE="Manual partitioning with cfdisk" + log "+ cfdisk $TARGET_DISK" + cfdisk "$TARGET_DISK" /dev/tty 2>/dev/tty || die "cfdisk failed or was cancelled." + run_logged partprobe "$TARGET_DISK" + run_logged udevadm settle + assign_manual_partitions +} + +partitions_for_target() { + lsblk -rpn -o PATH,TYPE "$TARGET_DISK" | awk '$2=="part"{print $1}' +} + +partition_description() { + lsblk -dn -o SIZE,FSTYPE,LABEL "$1" | sed 's/^[[:space:]]*//' +} + +manual_add_role() { + local part=$1 role=$2 mountpoint=$3 fs=$4 format=$5 + MAN_PARTS+=("$part"); MAN_ROLES+=("$role"); MAN_MOUNTS+=("$mountpoint"); MAN_FS+=("$fs"); MAN_FORMAT+=("$format") +} + +choose_manual_format() { + local part=$1 role=$2 choice + case $role in + boot) + choice=$(dlg --title "EFI Partition" --menu "$part: choose an action." 13 66 3 vfat "FORMAT as FAT32" preserve "Preserve existing FAT filesystem") || return 1 + ;; + swap) + choice=$(dlg --title "Swap Partition" --menu "$part: choose an action." 13 66 3 swap "FORMAT as swap" preserve "Preserve existing swap signature") || return 1 + ;; + *) + choice=$(dlg --title "Filesystem Action" --menu "$part: choose an action." 14 70 4 ext4 "FORMAT as ext4" btrfs "FORMAT as Btrfs" preserve "Preserve existing filesystem") || return 1 + ;; + esac + printf '%s' "$choice" +} + +assign_manual_partitions() { + MAN_PARTS=(); MAN_ROLES=(); MAN_MOUNTS=(); MAN_FS=(); MAN_FORMAT=() + local -a parts=() menu=() available=() + mapfile -t parts < <(partitions_for_target) + ((${#parts[@]})) || die "No partitions were found on $TARGET_DISK after cfdisk." + local p role mountpoint action existing fs i root_count=0 boot_count=0 + available=("${parts[@]}") + while ((${#available[@]})); do + menu=() + for p in "${available[@]}"; do menu+=("$p" "$(partition_description "$p")"); done + menu+=(done "Finish assigning partitions") + p=$(dlg --title "Assign Partitions" --menu "Select a partition to assign. A root partition is required." 20 78 11 "${menu[@]}") || die "Manual partition assignment cancelled." + [[ $p == done ]] && break + role=$(dlg --title "Partition Role" --menu "Assign $p:" 17 66 7 root "/" boot "/boot (UEFI ESP)" home "/home" swap "swap" custom "Custom mount point" unused "Do not use") || continue + if [[ $role == unused ]]; then + local -a next=(); for existing in "${available[@]}"; do [[ $existing == "$p" ]] || next+=("$existing"); done; available=("${next[@]}"); continue + fi + mountpoint="" + case $role in + root) ((root_count == 0)) || { msg "Only one root partition is allowed."; continue; }; mountpoint=/ ;; + boot) [[ $BOOT_MODE == UEFI ]] || { msg "A separate /boot partition is not used for BIOS installations."; continue; }; ((boot_count == 0)) || { msg "Only one EFI /boot partition is allowed."; continue; }; mountpoint=/boot ;; + home) mountpoint=/home ;; + custom) + mountpoint=$(dlg --title "Custom Mount Point" --inputbox "Enter an absolute mount point (for example /srv):" 10 68) || continue + [[ $mountpoint == /* && $mountpoint != / && $mountpoint != /boot && $mountpoint != /home && $mountpoint != *..* ]] || { msg "Invalid or reserved mount point."; continue; } + ;; + esac + if [[ -n $mountpoint ]]; then + for i in "${!MAN_MOUNTS[@]}"; do + [[ ${MAN_MOUNTS[$i]} != "$mountpoint" ]] || { msg "$mountpoint is already assigned."; mountpoint=""; break; } + done + [[ -n $mountpoint ]] || continue + fi + action=$(choose_manual_format "$p" "$role") || continue + if [[ $action == preserve ]]; then + existing=$(blkid -s TYPE -o value "$p" 2>/dev/null || true) + case $role in + boot) [[ $existing == vfat ]] || { msg "$p is not currently FAT32/vfat."; continue; }; fs=vfat ;; + swap) [[ $existing == swap ]] || { msg "$p is not currently swap."; continue; }; fs=swap ;; + *) [[ $existing == ext4 || $existing == btrfs ]] || { msg "$p must contain ext4 or Btrfs to preserve it."; continue; }; fs=$existing ;; + esac + else + fs=$action + yesno "Explicit format confirmation:\n\nFORMAT $p as $fs?\n\nAll data on this partition will be destroyed." 13 70 || continue + fi + manual_add_role "$p" "$role" "$mountpoint" "$fs" "$([[ $action == preserve ]] && echo 0 || echo 1)" + [[ $role == root ]] && ((root_count+=1)); [[ $role == boot ]] && ((boot_count+=1)) + local -a next=(); for existing in "${available[@]}"; do [[ $existing == "$p" ]] || next+=("$existing"); done; available=("${next[@]}") + done + ((root_count == 1)) || die "Manual partitioning requires exactly one root partition." + [[ $BOOT_MODE == BIOS || $boot_count == 1 ]] || die "UEFI installation requires one FAT32 partition assigned to /boot." +} + +format_automatic() { + local p1 p2 + p1=$(partition_path "$TARGET_DISK" 1) + [[ -b $p1 ]] || die "Expected partition $p1 was not created." + if [[ $BOOT_MODE == BIOS ]]; then + run_logged mkfs.ext4 -F "$p1" + else + p2=$(partition_path "$TARGET_DISK" 2); [[ -b $p2 ]] || die "Expected partition $p2 was not created." + run_logged mkfs.fat -F 32 "$p1" + if [[ $FILESYSTEM == ext4 ]]; then run_logged mkfs.ext4 -F "$p2"; else run_logged mkfs.btrfs -f "$p2"; fi + fi +} + +format_manual() { + local i p fs + for i in "${!MAN_PARTS[@]}"; do + [[ ${MAN_FORMAT[$i]} == 1 ]] || continue + p=${MAN_PARTS[$i]}; fs=${MAN_FS[$i]} + [[ -b $p ]] || die "Unsafe manual partition target: $p" + partitions_for_target | grep -Fxq "$p" || die "$p is not a partition of $TARGET_DISK." + case $fs in + ext4) run_logged mkfs.ext4 -F "$p" ;; btrfs) run_logged mkfs.btrfs -f "$p" ;; + vfat) run_logged mkfs.fat -F 32 "$p" ;; swap) run_logged mkswap -f "$p" ;; + *) die "Unsupported format requested: $fs" ;; + esac + [[ $(blkid -s TYPE -o value "$p" 2>/dev/null || true) == "$fs" ]] || die "Formatting validation failed for $p." + done +} + +mount_automatic() { + local p1 p2 + p1=$(partition_path "$TARGET_DISK" 1) + run_logged mkdir -p "$MNT" + if [[ $BOOT_MODE == BIOS ]]; then + run_logged mount "$p1" "$MNT" + else + p2=$(partition_path "$TARGET_DISK" 2) + if [[ $FILESYSTEM == ext4 ]]; then + run_logged mount "$p2" "$MNT" + else + run_logged mount "$p2" "$MNT" + run_logged btrfs subvolume create "$MNT/@" + run_logged btrfs subvolume create "$MNT/@home" + run_logged umount "$MNT" + run_logged mount -o subvol=@,compress=zstd,noatime "$p2" "$MNT" + run_logged mkdir -p "$MNT/home" + run_logged mount -o subvol=@home,compress=zstd,noatime "$p2" "$MNT/home" + fi + run_logged mkdir -p "$MNT/boot" + run_logged mount "$p1" "$MNT/boot" + fi +} + +mount_manual() { + local root_i=-1 i p mp fs + for i in "${!MAN_ROLES[@]}"; do [[ ${MAN_ROLES[$i]} == root ]] && root_i=$i; done + ((root_i >= 0)) || die "No root partition assigned." + run_logged mkdir -p "$MNT" + run_logged mount "${MAN_PARTS[$root_i]}" "$MNT" + for i in "${!MAN_PARTS[@]}"; do + [[ ${MAN_ROLES[$i]} == swap ]] && run_logged swapon "${MAN_PARTS[$i]}" + done + local ordered + while IFS= read -r ordered; do + i=${ordered#* } + p=${MAN_PARTS[$i]}; mp=${MAN_MOUNTS[$i]}; fs=${MAN_FS[$i]} + run_logged mkdir -p "$MNT$mp" + if [[ $fs == btrfs ]]; then run_logged mount -o compress=zstd,noatime "$p" "$MNT$mp"; else run_logged mount "$p" "$MNT$mp"; fi + done < <(for i in "${!MAN_PARTS[@]}"; do + [[ $i -eq $root_i || ${MAN_ROLES[$i]} == swap ]] && continue + printf '%08d %s\n' "${#MAN_MOUNTS[$i]}" "$i" + done | sort -n) +} + +validate_mounts() { + findmnt -rn -M "$MNT" >/dev/null || die "Root filesystem is not mounted at $MNT." + if [[ $BOOT_MODE == UEFI ]]; then + findmnt -rn -M "$MNT/boot" >/dev/null || die "EFI system partition is not mounted at $MNT/boot." + [[ $(findmnt -rn -o FSTYPE -M "$MNT/boot") == vfat ]] || die "The EFI system partition is not FAT32/vfat." + fi +} + +build_packages() { + PACKAGES=(base "$KERNEL" linux-firmware sudo networkmanager grub) + [[ $BOOT_MODE == UEFI ]] && PACKAGES+=(efibootmgr) + [[ $CPU_VENDOR == Intel ]] && PACKAGES+=(intel-ucode) + [[ $CPU_VENDOR == AMD ]] && PACKAGES+=(amd-ucode) + if [[ $FILESYSTEM == btrfs ]] || printf '%s\n' "${MAN_FS[@]:-}" | grep -qx btrfs; then PACKAGES+=(btrfs-progs); fi + ((INSTALL_YAY)) && PACKAGES+=(base-devel git go) + PACKAGES+=("${EXTRA_PACKAGES[@]}") +} +declare -a PACKAGES=() + +enable_testing_repositories() { + local config=$1 + [[ -f $config ]] || die "Pacman configuration not found: $config" + sed -i -E \ + -e '/^#\[core-testing\]$/,/^#?Include[[:space:]]*=/ s/^#//' \ + -e '/^#\[extra-testing\]$/,/^#?Include[[:space:]]*=/ s/^#//' \ + "$config" + grep -qx '\[core-testing\]' "$config" || die "Could not enable core-testing in $config." + grep -qx '\[extra-testing\]' "$config" || die "Could not enable extra-testing in $config." +} + +install_yay_helper() { + stage "Build and install yay AUR helper" + local builder=_yaybuilder artifact relative_artifact + run_logged arch-chroot "$MNT" useradd -m -s /bin/bash "$builder" + run_logged arch-chroot "$MNT" runuser -u "$builder" -- git clone --depth 1 https://aur.archlinux.org/yay.git /tmp/yay + run_logged arch-chroot "$MNT" runuser -u "$builder" -- bash -c 'cd /tmp/yay && makepkg --noconfirm --clean' + artifact=$(find "$MNT/tmp/yay" -maxdepth 1 -type f -name 'yay-*.pkg.tar.*' ! -name '*-debug-*' -print -quit) + [[ -n $artifact && -f $artifact ]] || die "The yay package was not produced." + relative_artifact=${artifact#"$MNT"} + run_logged arch-chroot "$MNT" pacman -U --noconfirm "$relative_artifact" + run_logged arch-chroot "$MNT" userdel -r "$builder" + run_logged rm -rf -- "$MNT/tmp/yay" + arch-chroot "$MNT" pacman -Q yay >>"$LOG_FILE" 2>&1 || die "yay installation validation failed." +} + +configure_system() { + stage "Generate fstab" + genfstab -U "$MNT" >"$MNT/etc/fstab" + [[ -s $MNT/etc/fstab ]] || die "Generated fstab is empty." + + stage "Configure timezone and hardware clock" + run_logged arch-chroot "$MNT" ln -sf "/usr/share/zoneinfo/$TIMEZONE" /etc/localtime + run_logged arch-chroot "$MNT" hwclock --systohc + + stage "Configure locale and console keymap" + sed -i -E "s|^#[[:space:]]*(${LOCALE//./\\.}[[:space:]].*)$|\\1|" "$MNT/etc/locale.gen" + run_logged arch-chroot "$MNT" locale-gen + printf 'LANG=%s\n' "$LOCALE" >"$MNT/etc/locale.conf" + printf 'KEYMAP=%s\n' "$KEYMAP" >"$MNT/etc/vconsole.conf" + + stage "Configure hostname" + printf '%s\n' "$HOSTNAME" >"$MNT/etc/hostname" + printf '127.0.0.1\tlocalhost\n::1\t\tlocalhost\n127.0.1.1\t%s.localdomain %s\n' "$HOSTNAME" "$HOSTNAME" >"$MNT/etc/hosts" + + stage "Configure users and sudo" + local user + if ((ROOT_PASSWORD_SET)); then printf 'root:%s\n' "$ROOT_PASSWORD" | arch-chroot "$MNT" chpasswd >>"$LOG_FILE" 2>&1; fi + for user in "${USERS[@]}"; do + if [[ ${USER_ADMINS[$user]} == 1 ]]; then run_logged arch-chroot "$MNT" useradd -m -G wheel -s /bin/bash "$user"; else run_logged arch-chroot "$MNT" useradd -m -s /bin/bash "$user"; fi + printf '%s:%s\n' "$user" "${USER_PASSWORDS[$user]}" | arch-chroot "$MNT" chpasswd >>"$LOG_FILE" 2>&1 + done + if printf '%s\n' "${USER_ADMINS[@]:-}" | grep -qx 1; then + printf '%%wheel ALL=(ALL:ALL) ALL\n' >"$MNT/etc/sudoers.d/10-wheel" + chmod 0440 "$MNT/etc/sudoers.d/10-wheel" + run_logged arch-chroot "$MNT" visudo -cf /etc/sudoers.d/10-wheel + fi + cleanup_secrets + + stage "Configure networking and enable services" + run_logged arch-chroot "$MNT" systemctl enable NetworkManager + + ((INSTALL_YAY)) && install_yay_helper + + stage "Generate initramfs" + run_logged arch-chroot "$MNT" mkinitcpio -P + + stage "Install GRUB" + assert_target_disk + if [[ $BOOT_MODE == UEFI ]]; then + run_logged arch-chroot "$MNT" grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --recheck + else + run_logged arch-chroot "$MNT" grub-install --target=i386-pc --recheck "$TARGET_DISK" + fi + stage "Generate GRUB configuration" + run_logged arch-chroot "$MNT" grub-mkconfig -o /boot/grub/grub.cfg + [[ -s $MNT/boot/grub/grub.cfg ]] || die "GRUB configuration was not created." +} + +validate_installation() { + stage "Validate installation" + [[ -x $MNT/usr/bin/bash && -s $MNT/etc/fstab && -s $MNT/etc/hostname ]] || die "Installed system validation failed." + arch-chroot "$MNT" pacman -Q "$KERNEL" grub networkmanager >>"$LOG_FILE" 2>&1 || die "Required installed packages could not be validated." + if [[ $BOOT_MODE == UEFI ]]; then [[ -d $MNT/boot/EFI/GRUB ]] || die "GRUB EFI files were not found."; fi +} + +install_system() { + stage "Validate configuration" + validate_config || die "Configuration became invalid." + assert_target_disk + + stage "Partition disk" + if [[ $PARTITION_MODE == automatic ]]; then automatic_partition; else manual_partition_editor; fi + + stage "Format filesystems" + if [[ $PARTITION_MODE == automatic ]]; then format_automatic; else format_manual; fi + + stage "Mount filesystems" + if [[ $PARTITION_MODE == automatic ]]; then mount_automatic; else mount_manual; fi + validate_mounts + + stage "Pacstrap base system" + build_packages + if ((ENABLE_TESTING_REPOS)); then + stage "Enable testing repositories in live environment" + enable_testing_repositories /etc/pacman.conf + run_logged pacman -Syy --noconfirm + stage "Pacstrap base system" + fi + run_logged pacstrap -K "$MNT" "${PACKAGES[@]}" + [[ -x $MNT/usr/bin/bash ]] || die "pacstrap did not create a usable base system." + + if ((ENABLE_TESTING_REPOS)); then + stage "Enable testing repositories in installed system" + enable_testing_repositories "$MNT/etc/pacman.conf" + run_logged arch-chroot "$MNT" pacman -Syy --noconfirm + fi + + configure_system + validate_installation + stage "Finish" + sync + dialog --title "Installation Complete" --msgbox \ + "Arch Linux was installed successfully.\n\nThe new system remains mounted at $MNT.\nYou may exit, run 'umount -R $MNT', and reboot when ready.\n\nLog: $LOG_FILE" 14 72 \ + /dev/tty 2>/dev/tty +} + +main() { + : >"$LOG_FILE" + log "Arch installer started" + preflight + main_menu + install_system +} + +main "$@"