Files
AlvinArch/airootfs/usr/local/bin/alvin-install
T
alvinlollo 117b5e97aa
Tests / tests (push) Successful in 2m23s
Add alvin-install TUI installer, fix NetworkManager boot
- New gum-based installer script (alvin-install) with LUKS encryption,
  nmtui networking, disk selector, user/git config, and CachyOS repo setup
- Replace NetworkManager full unit file with symlink for consistency
- Add alvin-install reference to MOTD
- Add file_permissions entry in profiledef.sh
- Fix localrepo path case in pacman.conf
2026-09-20 22:22:20 +10:00

564 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
# alvin-install — Gum TUI Arch Linux installer with LUKS encryption
# shellcheck disable=SC2034
set -Eeuo pipefail
readonly LOG=/var/log/alvin-install.log
readonly PASS_MIN=8
readonly ESP_SIZE="1GiB"
readonly TARGET="/mnt"
readonly -a PKGS_EXCLUDE=(archiso mkinitcpio-archiso mkinitcpio-nfs-utils syslinux memtest86+)
skip() { local x; for x in "${PKGS_EXCLUDE[@]}"; do [[ "$x" == "$1" ]] && return 0; done; return 1; }
DISK=""
KEYMAP=""
USERNAME=""
PASS=""
HOSTNAME=""
GIT_NAME=""
GIT_EMAIL=""
TIMEZONE=""
LOCALE=""
LUKS="true"
FILESYSTEM="ext4"
log() { echo "[$(date '+%H:%M:%S')] $*" >> "$LOG"; }
cleanup() {
set +e
umount -R "$TARGET" 2>/dev/null
cryptsetup close cryptroot 2>/dev/null
true
}
trap cleanup EXIT
trap 'gum style --foreground 9 --bold "Failed at line $LINENO. See $LOG"' ERR
(( EUID == 0 )) || { echo "Run as root" >&2; exit 1; }
die() { gum style --foreground 9 --bold "ERROR: $*"; exit 1; }
info() { gum style --foreground 12 --bold "$*"; }
ok() { gum style --foreground 10 --bold "$*"; }
header() {
gum style \
--foreground "#FAFAFA" --background "#6C91BF" \
--padding "1 2" --bold --align center --width 70 \
"$1"
}
divider() {
gum style --foreground 8 "──────────────────────────────────────────────────────────"
}
part() { [[ "$DISK" =~ [0-9]$ ]] && echo "${DISK}p$1" || echo "${DISK}$1"; }
valid_username() { [[ "$1" =~ ^[a-z_][a-z0-9_-]*$ ]] && (( ${#1} <= 32 )); }
check_prereqs() {
command -v gum >/dev/null 2>&1 || die "gum is not installed"
[[ -d /sys/firmware/efi ]] || die "UEFI not detected. This installer requires UEFI boot."
}
# ── Screens ───────────────────────────────────────────────────────────────────
screen_welcome() {
clear
header "Alvin Arch Linux Installer"
echo ""
gum style --foreground 15 --align center \
"This will install Arch Linux with:" \
" * LUKS disk encryption (default)" \
" * GRUB bootloader" \
" * NetworkManager" \
" * Your user + git config" \
""
divider
echo ""
gum confirm --affirmative "Begin installation" --negative "Abort" \
"Ready to begin?" || die "Installation aborted by user."
}
screen_keymap() {
clear
header "Keyboard Layout"
echo ""
gum style --foreground 15 \
"Select your console keymap." \
"This affects passwords and the LUKS passphrase at boot."
echo ""
KEYMAP=$(localectl list-keymaps | gum filter --header "+ Keymap" --placeholder "Search keymaps...")
[[ -z "$KEYMAP" ]] && die "No keymap selected."
loadkeys "$KEYMAP" 2>/dev/null || true
log "Keymap: $KEYMAP"
}
screen_network() {
clear
header "Network Setup"
echo ""
gum style --foreground 15 --align center \
"A network connection is required for installation." \
"" \
"When nmtui opens:" \
" 1. Select 'Activate a connection'" \
" 2. Use arrow keys to pick your WiFi network" \
" 3. Enter the password and press Enter" \
" 4. Press Esc to exit nmtui"
echo ""
divider
echo ""
gum confirm --affirmative "Open nmtui" --negative "Skip (already connected)" \
"Open NetworkManager TUI?" && nmtui || true
echo ""
info "Verifying network connection..."
local try=0
while ! ping -c1 -W3 archlinux.org &>/dev/null; do
try=$((try + 1))
if (( try >= 5 )); then
gum confirm --affirmative "Retry" --negative "Abort" \
"Network check failed. Retry?" || die "No network connection."
try=0
fi
sleep 2
done
ok "Network verified."
log "Network: connected"
}
screen_disk() {
clear
header "Select Installation Disk"
echo ""
local live_dev=""
if [[ -d /run/archiso/bootmnt ]]; then
local src
src=$(findmnt -no SOURCE /run/archiso/bootmnt 2>/dev/null || true)
[[ -n "$src" ]] && live_dev="/dev/$(lsblk -no PKNAME "$src" 2>/dev/null | head -n1)"
fi
local disks=()
while IFS= read -r line; do
local name size model
name=$(echo "$line" | awk '{print $1}')
size=$(echo "$line" | awk '{print $2}')
model=$(echo "$line" | awk '{$1=$2=""; sub(/^[ \t]+/,""); print}')
[[ "$name" == "$live_dev" ]] && continue
disks+=("${name} ${size} ${model}")
done < <(lsblk -dpno NAME,SIZE,MODEL,TYPE | awk '$NF=="disk" {print $1,$2,$3}')
(( ${#disks[@]} > 0 )) || die "No disks found."
while true; do
local choice
choice=$(gum choose --header "+ Target disk (live USB hidden)" "${disks[@]}") \
|| die "No disk selected."
DISK=$(echo "$choice" | awk '{print $1}')
gum confirm --affirmative "Wipe ${DISK}" --negative "Go back" \
"WARNING: ALL data on ${DISK} will be destroyed!" && break
done
log "Disk: $DISK"
}
screen_luks() {
clear
header "Disk Encryption"
echo ""
gum style --foreground 15 --align center \
"LUKS encrypts your root filesystem." \
"The passphrase is the same as your login password." \
"Your data stays protected if the device is lost or stolen."
echo ""
echo ""
if gum confirm --affirmative "Enable LUKS encryption" --negative "No encryption" \
"Enable LUKS disk encryption? (default: yes)"; then
LUKS="true"
else
LUKS="false"
fi
log "LUKS: $LUKS"
}
screen_filesystem() {
clear
header "Filesystem"
echo ""
FILESYSTEM=$(gum choose --header "+ Root filesystem" "ext4" "btrfs") \
|| die "No filesystem selected."
log "Filesystem: $FILESYSTEM"
}
screen_username() {
clear
header "Create User"
echo ""
local input
input=$(gum input --placeholder "username" --char-limit 32 --header "+ Username") \
|| die "No username entered."
[[ -z "$input" ]] && die "Username cannot be empty."
valid_username "$input" || die "Invalid username. Use lowercase letters, digits, _ or -."
USERNAME="$input"
log "Username: $USERNAME"
}
screen_password() {
clear
header "Set Password"
echo ""
gum style --foreground 15 \
"This password will be used for login and LUKS disk encryption."
echo ""
local pass1 pass2
pass1=$(gum input --password --header "+ Password") || die "No password entered."
(( ${#pass1} >= PASS_MIN )) || die "Password must be at least ${PASS_MIN} characters."
pass2=$(gum input --password --header "+ Confirm password") || die "No confirmation entered."
[[ "$pass1" == "$pass2" ]] || die "Passwords do not match."
PASS="$pass1"
log "Password: set (${#PASS} chars)"
}
screen_hostname() {
clear
header "Hostname"
echo ""
HOSTNAME=$(gum input --placeholder "archlinux" --char-limit 64 --header "+ Hostname") \
|| die "No hostname entered."
[[ -z "$HOSTNAME" ]] && HOSTNAME="archlinux"
log "Hostname: $HOSTNAME"
}
screen_git() {
clear
header "Git Configuration"
echo ""
GIT_NAME=$(gum input --value "$USERNAME" --header "+ Git name") \
|| die "No git name entered."
GIT_EMAIL=$(gum input --placeholder "user@example.com" --header "+ Git email") \
|| die "No git email entered."
[[ -z "$GIT_EMAIL" ]] && die "Git email cannot be empty."
log "Git: $GIT_NAME <$GIT_EMAIL>"
}
screen_timezone() {
clear
header "Timezone"
echo ""
TIMEZONE=$(timedatectl list-timezones | gum filter --header "+ Timezone" --placeholder "Search...") \
|| die "No timezone selected."
log "Timezone: $TIMEZONE"
}
screen_locale() {
clear
header "Locale"
echo ""
local locales
locales=$(sed -n 's/^#\?\([a-z][a-z]*_[A-Z][A-Z]\.UTF-8\) UTF-8.*/\1/p' /etc/locale.gen | sort -u)
[[ -z "$locales" ]] && locales="en_US.UTF-8"
LOCALE=$(echo "$locales" | gum filter --header "+ Locale" --placeholder "Search...") \
|| die "No locale selected."
log "Locale: $LOCALE"
}
screen_summary() {
clear
header "Installation Summary"
echo ""
local enc_status="No"
[[ "$LUKS" == "true" ]] && enc_status="Yes (uses login password)"
gum style --foreground 15 --border normal --border-foreground 8 \
--padding "0 2" --width 60 \
"Disk: ${DISK}" \
"Encryption: ${enc_status}" \
"Filesystem: ${FILESYSTEM}" \
"Username: ${USERNAME}" \
"Hostname: ${HOSTNAME}" \
"Keymap: ${KEYMAP}" \
"Locale: ${LOCALE}" \
"Timezone: ${TIMEZONE}" \
"Git: ${GIT_NAME} <${GIT_EMAIL}>"
echo ""
gum confirm --affirmative "Install" --negative "Abort" \
"Proceed with installation?" || die "Installation aborted by user."
}
# ── Install steps ─────────────────────────────────────────────────────────────
install_partition() {
info "Partitioning ${DISK}..."
log "Partitioning $DISK"
wipefs -af "$DISK" 2>>"$LOG"
sgdisk --zap-all "$DISK" 2>>"$LOG"
parted -s "$DISK" \
mklabel gpt \
mkpart ESP fat32 1MiB "${ESP_SIZE}" \
set 1 esp on \
mkpart root "${ESP_SIZE}" 100%
partprobe "$DISK"
udevadm settle --timeout=30
wipefs -af "$(part 1)" "$(part 2)"
ok "Partitioned."
}
install_luks() {
[[ "$LUKS" != "true" ]] && return
info "Setting up LUKS encryption on $(part 2)..."
log "LUKS format $(part 2)"
printf '%s' "$PASS" | cryptsetup luksFormat \
--batch-mode --type luks2 --key-file=- "$(part 2)"
printf '%s' "$PASS" | cryptsetup open \
--key-file=- "$(part 2)" cryptroot
ok "LUKS unlocked."
}
install_format_mount() {
info "Formatting..."
mkfs.fat -F32 -n ESP "$(part 1)" 2>>"$LOG"
local root_dev
if [[ "$LUKS" == "true" ]]; then
root_dev="/dev/mapper/cryptroot"
else
root_dev="$(part 2)"
fi
if [[ "$FILESYSTEM" == "btrfs" ]]; then
mkfs.btrfs -f -L root "$root_dev" 2>>"$LOG"
else
mkfs.ext4 -F -L root "$root_dev" 2>>"$LOG"
fi
info "Mounting..."
mount "$root_dev" "$TARGET"
mkdir -p "${TARGET}/boot"
mount "$(part 1)" "${TARGET}/boot"
ok "Formatted and mounted."
}
install_pacstrap() {
info "Building package list..."
local pkglist="/run/archiso/bootmnt/arch/pkglist.x86_64.txt"
local pkgs=()
if [[ -f "$pkglist" ]]; then
while IFS= read -r pkg; do
[[ -z "$pkg" ]] && continue
skip "$pkg" && continue
pkgs+=("$pkg")
done < "$pkglist"
fi
for p in base linux linux-firmware linux-cachyos linux-cachyos-headers \
networkmanager grub efibootmgr sudo git intel-ucode amd-ucode \
mkinitcpio cryptsetup dosfstools e2fsprogs btrfs-progs \
cachyos-keyring cachyos-mirrorlist; do
local found=false
for existing in "${pkgs[@]+"${pkgs[@]}"}"; do
[[ "$existing" == "$p" ]] && found=true && break
done
$found || pkgs+=("$p")
done
info "Installing ${#pkgs[@]} packages (this may take a while)..."
log "Pacstrap: ${#pkgs[@]} packages"
pacstrap "$TARGET" "${pkgs[@]}" --noconfirm 2>>"$LOG"
ok "Packages installed."
}
install_fstab() {
info "Generating fstab..."
genfstab -U "$TARGET" >> "${TARGET}/etc/fstab"
ok "fstab generated."
}
install_cachyos_repo() {
info "Configuring CachyOS repository..."
if ! grep -q '\[cachyos\]' "${TARGET}/etc/pacman.conf" 2>/dev/null; then
cat >> "${TARGET}/etc/pacman.conf" <<'REPO'
[cachyos]
Include = /etc/pacman.d/cachyos-mirrorlist
REPO
fi
sed -i '/\[local-aur\]/,/Include/d' "${TARGET}/etc/pacman.conf" 2>/dev/null || true
ok "CachyOS repo configured."
}
install_timezone() {
info "Setting timezone..."
arch-chroot "$TARGET" ln -sf "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime
arch-chroot "$TARGET" hwclock --systohc || true
ok "Timezone set."
}
install_locale() {
info "Setting locale..."
arch-chroot "$TARGET" sed -i "s/^#${LOCALE}/${LOCALE}/" /etc/locale.gen
arch-chroot "$TARGET" locale-gen
echo "LANG=${LOCALE}" > "${TARGET}/etc/locale.conf"
echo "KEYMAP=${KEYMAP}" > "${TARGET}/etc/vconsole.conf"
ok "Locale and keymap set."
}
install_hostname() {
info "Setting hostname..."
echo "$HOSTNAME" > "${TARGET}/etc/hostname"
cat > "${TARGET}/etc/hosts" <<EOF
127.0.0.1 localhost
::1 localhost
127.0.1.1 ${HOSTNAME}.localdomain ${HOSTNAME}
EOF
ok "Hostname set."
}
install_network() {
info "Configuring network..."
arch-chroot "$TARGET" systemctl enable NetworkManager
local nm_dir="/etc/NetworkManager/system-connections"
if [[ -d "$nm_dir" ]] && ls "$nm_dir"/* &>/dev/null; then
mkdir -p "${TARGET}/etc/NetworkManager/system-connections"
cp -a "$nm_dir"/* "${TARGET}/etc/NetworkManager/system-connections/"
chmod 600 "${TARGET}/etc/NetworkManager/system-connections"/*
log "WiFi profiles copied."
fi
ok "NetworkManager enabled."
}
install_user() {
info "Creating user..."
cp -rT /etc/skel "${TARGET}/etc/skel"
arch-chroot "$TARGET" useradd -m -G wheel -s /bin/bash "$USERNAME"
printf '%s:%s\n' "$USERNAME" "$PASS" | arch-chroot "$TARGET" chpasswd
arch-chroot "$TARGET" passwd -l root
echo '%wheel ALL=(ALL:ALL) ALL' > "${TARGET}/etc/sudoers.d/10-wheel"
chmod 440 "${TARGET}/etc/sudoers.d/10-wheel"
ok "User ${USERNAME} created (wheel group, sudo enabled). Root locked."
}
install_git() {
info "Configuring git..."
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
git config --global user.name "$GIT_NAME"
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
git config --global user.email "$GIT_EMAIL"
ok "Git configured for ${USERNAME}."
}
install_mkinitcpio() {
info "Generating initramfs..."
if [[ "$LUKS" == "true" ]]; then
mkdir -p "${TARGET}/etc/mkinitcpio.conf.d"
cat > "${TARGET}/etc/mkinitcpio.conf.d/alvin.conf" <<HOOKS
HOOKS=(base udev autodetect modconf kms keyboard keymap consolefont block encrypt filesystems fsck)
HOOKS
fi
arch-chroot "$TARGET" mkinitcpio -P
ok "initramfs generated."
}
install_bootloader() {
info "Installing GRUB bootloader..."
if [[ "$LUKS" == "true" ]]; then
local luks_uuid
luks_uuid=$(blkid -s UUID -o value "$(part 2)")
sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\"cryptdevice=UUID=${luks_uuid}:cryptroot\"|" \
"${TARGET}/etc/default/grub"
fi
arch-chroot "$TARGET" grub-install \
--target=x86_64-efi --efi-directory=/boot --bootloader-id=ALVIN
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg
ok "Bootloader installed."
}
install_cleanup() {
info "Cleaning up..."
cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true
unset PASS
log "Installation complete."
ok "Installation complete!"
echo ""
gum style --foreground 10 --align center \
"Arch Linux installed successfully." \
"" \
"Run 'reboot' to boot into your new system."
echo ""
}
# ── Main ──────────────────────────────────────────────────────────────────────
main() {
check_prereqs
screen_welcome
screen_keymap
screen_network
screen_disk
screen_luks
screen_filesystem
screen_username
screen_password
screen_hostname
screen_git
screen_timezone
screen_locale
screen_summary
install_partition
install_luks
install_format_mount
install_pacstrap
install_fstab
install_cachyos_repo
install_timezone
install_locale
install_hostname
install_network
install_user
install_git
install_mkinitcpio
install_bootloader
install_cleanup
}
main "$@"