#!/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 "$@"