From 117b5e97aad0f52aef9d395b8f41606a16f2d741 Mon Sep 17 00:00:00 2001 From: alvinlollo Date: Sun, 20 Sep 2026 22:22:20 +1000 Subject: [PATCH] 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 --- airootfs/etc/motd | 3 +- .../NetworkManager.service | 31 +- airootfs/usr/local/bin/alvin-install | 563 ++++++++++++++++++ pacman.conf | 2 +- profiledef.sh | 1 + 5 files changed, 568 insertions(+), 32 deletions(-) mode change 100644 => 120000 airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service create mode 100755 airootfs/usr/local/bin/alvin-install diff --git a/airootfs/etc/motd b/airootfs/etc/motd index d4b488e..c0df390 100644 --- a/airootfs/etc/motd +++ b/airootfs/etc/motd @@ -1,4 +1,5 @@ -To install Arch Linux follow the installation guide: +To install Arch Linux, follow the installation guide, +or run alvin-install for an automated setup. https://wiki.archlinux.org/title/Installation_guide For Wi-Fi, run nmtui to configure a wireless network. diff --git a/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service b/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service deleted file mode 100644 index a794f93..0000000 --- a/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service +++ /dev/null @@ -1,30 +0,0 @@ -[Unit] -Description=Network Manager -Documentation=man:NetworkManager.conf(5) -Wants=NetworkManager-wait-online.service -After=sysinit.target network-pre.target -Wants=network-pre.target -Before=network-online.target remote-fs.target -After=network-pre.target -BindsTo=sys-subsystem-net-devices.target -After=sys-subsystem-net-devices.target - -[Service] -Type=dbus -BusName=org.freedesktop.NetworkManager -ExecCondition=/usr/bin/test -x /usr/bin/NetworkManager -ExecReload=/usr/bin/kill -HUP $MAINPID -ExecStart=/usr/bin/NetworkManager --no-daemon -Restart=on-failure -KillMode=process -CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE -LockPersonality=yes -MemoryDenyWriteExecute=yes -ProtectHome=yes -ProtectSystem=yes -RestrictNamespaces=yes -RestrictRealtime=yes -SystemCallFilter=@system-service - -[Install] -WantedBy=multi-user.target diff --git a/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service b/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service new file mode 120000 index 0000000..e874a9b --- /dev/null +++ b/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service @@ -0,0 +1 @@ +/usr/lib/systemd/system/NetworkManager.service \ No newline at end of file diff --git a/airootfs/usr/local/bin/alvin-install b/airootfs/usr/local/bin/alvin-install new file mode 100755 index 0000000..3e05028 --- /dev/null +++ b/airootfs/usr/local/bin/alvin-install @@ -0,0 +1,563 @@ +#!/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" </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" </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 "$@" diff --git a/pacman.conf b/pacman.conf index bfa19b3..680ada0 100644 --- a/pacman.conf +++ b/pacman.conf @@ -103,7 +103,7 @@ Include = /etc/pacman.d/chaotic-mirrorlist [local-aur] SigLevel = Optional TrustAll -Server = file:///home/alvin/alvinarch/localrepo +Server = file:///home/alvin/AlvinArch/localrepo # An example of a custom package repository. See the pacman manpage for # tips on creating your own repositories. diff --git a/profiledef.sh b/profiledef.sh index 7d72aac..3fdb8b5 100644 --- a/profiledef.sh +++ b/profiledef.sh @@ -20,6 +20,7 @@ file_permissions=( ["/root/.automated_script.sh"]="0:0:755" ["/root/.gnupg"]="0:0:700" ["/usr/local/bin/choose-mirror"]="0:0:755" + ["/usr/local/bin/alvin-install"]="0:0:755" ["/usr/local/bin/Installation_guide"]="0:0:755" ["/usr/local/bin/livecd-sound"]="0:0:755" )