fixes
This commit is contained in:
Executable
+703
@@ -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 >/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 "$@" </dev/tty 2>&1 >/dev/tty; }
|
||||
msg() { dialog --title "Arch Linux Installer" --msgbox "$1" "${2:-12}" "${3:-70}" </dev/tty >/dev/tty 2>/dev/tty; }
|
||||
yesno() { dialog --title "Arch Linux Installer" --yesno "$1" "${2:-12}" "${3:-70}" </dev/tty >/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 >/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 >/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 >/dev/tty 2>/dev/tty
|
||||
}
|
||||
|
||||
main() {
|
||||
: >"$LOG_FILE"
|
||||
log "Arch installer started"
|
||||
preflight
|
||||
main_menu
|
||||
install_system
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user