#!/usr/bin/env bash # ╔══════════════════════════════════════════════════════════════╗ # ║ Arch Linux + Hyprland · One-Shot Automated Installer ║ # ║ Usage: bash <(curl -s https://arch-script.pages.dev/get) ║ # ╚══════════════════════════════════════════════════════════════╝ # # What gets installed (all hardcoded — no component selection): # • Arch Linux — Btrfs root (@, @home, @log, @cache) + FAT32 ESP # • Kernel — linux + auto-detected microcode (intel/amd) # • Boot — systemd-boot # • Desktop — Hyprland (Wayland) via archinstall profile # • Seat — seatd # • Login — GDM (GNOME Display Manager) # • Audio — Pipewire + WirePlumber # • Bluetooth — bluez + bluez-utils # • Network — NetworkManager # • Swap — ZRAM (zstd) # • AUR helper — yay # • Dotfiles — illogical-impulse (end-4/dots-hyprland) # # Prompts (the only user input required): # 1. Drive to install to # 2. Username # 3. Password (also used for root) # 4. Hostname [default: archlinux] # 5. Timezone [default: UTC] # ────────────────────────────────────────────────────────────────── set -euo pipefail trap 'echo -e "\n\033[0;31m[ERROR] Installer failed at line ${LINENO}.\033[0m" >&2' ERR # Colour shortcuts R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'; C='\033[0;36m'; N='\033[0m' # ── Defaults (edit before hosting) ──────────────────────────────────────────── DEFAULT_HOSTNAME="archlinux" DEFAULT_TIMEZONE="UTC" # ────────────────────────────────────────────────────────────────────────────── banner() { echo -e "${C}" echo " ╔══════════════════════════════════════════════════════════╗" echo " ║ Arch Linux + Hyprland · One-Shot Installer ║" echo " ║ Login : GDM (GNOME Display Manager) ║" echo " ╚══════════════════════════════════════════════════════════╝" echo -e "${N}" } # ── Sanity checks ───────────────────────────────────────────────────────────── [[ $EUID -ne 0 ]] && { echo -e "${R}[ERROR] Must be run as root. Boot the Arch Linux live ISO first.${N}" exit 1 } command -v pacman &>/dev/null || { echo -e "${R}[ERROR] pacman not found — boot the Arch Linux live ISO.${N}" exit 1 } ping -c1 -W5 archlinux.org &>/dev/null || { echo -e "${R}[ERROR] No internet connection. Connect first (e.g. iwctl).${N}" exit 1 } banner # ── Pre-configure mirror (geo.mirror.pkgbuild.com = Arch's own global CDN) ─── echo 'Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch' > /etc/pacman.d/mirrorlist # ── Step 1: Update archinstall ──────────────────────────────────────────────── echo -e "${Y}[1/5] Bootstrapping — updating archinstall and dependencies...${N}" pacman -Sy --noconfirm --needed archinstall python git curl 2>&1 | grep -E '(upgraded|installed|error)' || true echo -e " ${G}Done.${N}" # ── Step 2: Drive selection ─────────────────────────────────────────────────── echo -e "\n${Y}[2/5] Drive selection${N}\n" # Build drive list: exclude floppy(2), loop(7), optical(11); writable only (RO=0) readarray -t _PATHS < <( lsblk -d -n -o PATH,RO --exclude 2,7,11 2>/dev/null \ | awk '$2=="0"{print $1}' \ | grep -v '^$' ) if [[ ${#_PATHS[@]} -eq 0 ]]; then echo -e "${R}[ERROR] No usable block devices found.${N}"; exit 1 fi printf " %-6s %-18s %-10s %s\n" "No." "Device" "Size" "Model" echo " ──────────────────────────────────────────────────────────" for i in "${!_PATHS[@]}"; do DEV="${_PATHS[$i]}" SZ=$(lsblk -d -n -o SIZE "$DEV" 2>/dev/null || echo "?") MD=$(lsblk -d -n -o MODEL "$DEV" 2>/dev/null || echo "") printf " [%-3d] %-18s %-10s %s\n" "$((i+1))" "$DEV" "$SZ" "$MD" done echo " ──────────────────────────────────────────────────────────" if [[ ${#_PATHS[@]} -eq 1 ]]; then DRIVE="${_PATHS[0]}" echo -e "\n Auto-selected: ${G}${DRIVE}${N} (only drive present)" else while true; do read -rp $'\n Enter drive number: ' _C if [[ "$_C" =~ ^[0-9]+$ ]] && (( _C >= 1 && _C <= ${#_PATHS[@]} )); then DRIVE="${_PATHS[$((_C-1))]}" break fi echo " Invalid — try again." done fi echo -e "\n ${R}[WARNING]${N} ALL data on ${C}${DRIVE}${N} will be permanently destroyed." read -rp " Type 'YES' to confirm: " _CHK [[ "$_CHK" == "YES" ]] || { echo -e "\n Aborted."; exit 0; } # ── Step 3: User credentials ────────────────────────────────────────────────── echo -e "\n${Y}[3/5] User account${N}\n" while true; do read -rp " Username : " USERNAME [[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]{0,30}$ ]] && break echo " [!] Lowercase letters / digits / hyphens only, starting with a letter." done while true; do read -rsp " Password : " _P1; echo [[ -n "$_P1" ]] || { echo " [!] Password cannot be empty."; continue; } read -rsp " Confirm : " _P2; echo [[ "$_P1" == "$_P2" ]] && USERPASS="$_P1" && break echo " [!] Passwords do not match — try again." done unset _P1 _P2 echo "" read -rp " Hostname [${DEFAULT_HOSTNAME}]: " HOSTNAME HOSTNAME="${HOSTNAME:-$DEFAULT_HOSTNAME}" read -rp " Timezone [${DEFAULT_TIMEZONE}]: " TIMEZONE TIMEZONE="${TIMEZONE:-$DEFAULT_TIMEZONE}" echo "" echo -e " ${G}Summary${N}" echo " ───────────────────────────────────────────" echo " Drive : $DRIVE" echo " Hostname : $HOSTNAME" echo " Timezone : $TIMEZONE" echo " Username : $USERNAME" echo " ───────────────────────────────────────────" read -rp $'\n Press Enter to start installation (Ctrl+C to abort): ' _ # ── Step 4: Python installer ────────────────────────────────────────────────── echo -e "\n${Y}[4/5] Installing Arch Linux + Hyprland (10 – 30 min)...${N}\n" export ARCH_DRIVE="$DRIVE" export ARCH_USER="$USERNAME" export ARCH_PASS="$USERPASS" export ARCH_HOSTNAME="$HOSTNAME" export ARCH_TZ="$TIMEZONE" # Write the Python installer to a temp file. # Single-quoted heredoc → no bash substitution inside; # Python reads all values via os.environ. cat > /tmp/_archauto.py << 'PYEOF' """ Non-interactive Arch Linux + Hyprland installer. All configuration is read from environment variables: ARCH_DRIVE — block device path e.g. /dev/sda ARCH_USER — primary username ARCH_PASS — password (plaintext; hashed internally by archinstall) ARCH_HOSTNAME — machine hostname [default: archlinux] ARCH_TZ — timezone [default: UTC] """ import os import sys from pathlib import Path from archinstall.default_profiles.desktops import SeatAccess from archinstall.default_profiles.desktops.hyprland import HyprlandProfile from archinstall.default_profiles.profile import CustomSetting from archinstall.lib.disk.device_handler import device_handler from archinstall.lib.disk.filesystem import FilesystemHandler from archinstall.lib.installer import Installer from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.device import ( DeviceModification, DiskLayoutConfiguration, DiskLayoutType, FilesystemType, ModificationStatus, PartitionFlag, PartitionModification, PartitionType, Size, SubvolumeModification, Unit, ) from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.users import Password, User from archinstall.lib.profile.profiles_handler import profile_handler # ─── Helpers ────────────────────────────────────────────────────────────────── def _step(msg: str) -> None: print(f"\n \033[1;33m>>>\033[0m {msg}") def _ok(msg: str) -> None: print(f" \033[0;32m[+]\033[0m {msg}") def _install_yay(installation: Installer, username: str) -> None: """Build yay from the AUR as the regular user, install system-wide.""" _step("Building yay AUR helper...") build_cmd = ( f"bash -c '" f"mkdir -p /home/{username}/tmp /home/{username}/.cache/go && " f"export TMPDIR=/home/{username}/tmp GOCACHE=/home/{username}/.cache/go && " f"cd /home/{username} && rm -rf yay && " f"git clone --depth=1 https://aur.archlinux.org/yay.git && " f"cd yay && makepkg -s --noconfirm --skippgpcheck'" ) installation.arch_chroot(build_cmd, run_as=username, peek_output=True) installation.arch_chroot( f"bash -c 'pacman -U --noconfirm /home/{username}/yay/yay-*.pkg.tar.zst'", peek_output=True, ) _ok("yay installed.") # ─── Main ───────────────────────────────────────────────────────────────────── def main() -> None: drive_str = os.environ.get("ARCH_DRIVE", "") username = os.environ.get("ARCH_USER", "") pass_str = os.environ.get("ARCH_PASS", "") hostname = os.environ.get("ARCH_HOSTNAME", "archlinux") timezone = os.environ.get("ARCH_TZ", "UTC") if not all([drive_str, username, pass_str]): sys.exit("[FATAL] env vars ARCH_DRIVE, ARCH_USER and ARCH_PASS are required.") device_path = Path(drive_str) password = Password(plaintext=pass_str) # ── Disk layout ─────────────────────────────────────────────────────────── _step(f"Preparing disk layout on {device_path}...") device = device_handler.get_device(device_path) if not device: sys.exit(f"[FATAL] Device {device_path} not found by archinstall.") ss = device.device_info.sector_size # SectorSize total = device.device_info.total_size # Size dev_mod = DeviceModification(device, wipe=True) # Partition 1 — /boot 512 MiB FAT32 (EFI System Partition) boot_part = PartitionModification( status = ModificationStatus.CREATE, type = PartitionType.PRIMARY, start = Size(1, Unit.MiB, ss), length = Size(512, Unit.MiB, ss), mountpoint = Path("/boot"), fs_type = FilesystemType.FAT32, flags = [PartitionFlag.BOOT, PartitionFlag.ESP], ) dev_mod.add_partition(boot_part) # Partition 2 — / Btrfs remainder (leave 1 MiB for GPT backup table) root_start = Size(513, Unit.MiB, ss) root_length = total - root_start - Size(1, Unit.MiB, ss) root_part = PartitionModification( status = ModificationStatus.CREATE, type = PartitionType.PRIMARY, start = root_start, length = root_length, mountpoint = None, # mountpoints are on subvolumes fs_type = FilesystemType.BTRFS, mount_options = ["compress=zstd", "noatime"], btrfs_subvols = [ SubvolumeModification("@", Path("/")), SubvolumeModification("@home", Path("/home")), SubvolumeModification("@log", Path("/var/log")), SubvolumeModification("@cache", Path("/var/cache/pacman/pkg")), ], ) dev_mod.add_partition(root_part) disk_config = DiskLayoutConfiguration( config_type = DiskLayoutType.Default, device_modifications = [dev_mod], ) # ── Format ──────────────────────────────────────────────────────────────── _step("Formatting partitions...") FilesystemHandler(disk_config).perform_filesystem_operations() _ok("Partitions formatted.") # ── Install ─────────────────────────────────────────────────────────────── mountpoint = Path("/mnt") with Installer(mountpoint, disk_config, kernels=["linux"]) as inst: inst.mount_ordered_layout() inst.sanity_check() # 1. Base system _step("Bootstrapping base system (pacstrap)...") inst.minimal_installation(hostname=hostname) _ok("Base system installed.") # 2. Extra packages _step("Installing extra packages...") inst.add_additional_packages([ # Explicitly choose iptables-nft to avoid pacman interactive conflict prompt "iptables-nft", # Network "networkmanager", # Bluetooth "bluez", "bluez-utils", # Audio — full Pipewire stack "pipewire", "pipewire-alsa", "pipewire-pulse", "pipewire-jack", "wireplumber", # Seat manager (for Hyprland) "seatd", # Display manager "gdm", # Build toolchain (required by yay and dotfiles) "base-devel", "go", # Download tools "git", "curl", "wget", # XDG "xdg-user-dirs", "xdg-utils", ]) _ok("Extra packages installed.") # 3. ZRAM swap (zstd) _step("Configuring ZRAM swap...") inst.setup_swap() _ok("ZRAM swap configured.") # 4. Hyprland desktop profile # Pre-select seatd so the TUI seat-access prompt is skipped. _step("Installing Hyprland desktop profile...") hypr = HyprlandProfile() hypr.custom_settings[CustomSetting.SeatAccess] = SeatAccess.seatd.value profile_handler.install_profile_config(inst, ProfileConfiguration(hypr)) _ok("Hyprland profile installed.") # Remove conflicting display managers the profile may have pulled in. _step("Removing conflicting display managers...") inst.arch_chroot( "bash -c '" "pacman -Rns --noconfirm sddm lightdm lxdm 2>/dev/null || true; " "systemctl disable sddm lightdm display-manager 2>/dev/null || true; " "rm -f /etc/systemd/system/display-manager.service; " "echo done'", peek_output=True, ) _ok("Conflicting display managers removed.") # 5. Bootloader _step("Installing systemd-boot...") inst.add_bootloader(Bootloader.Systemd) _ok("systemd-boot installed.") # 6. Enable services _step("Enabling services...") inst.enable_service([ "NetworkManager", "bluetooth", "seatd", "gdm", ]) _ok("Services enabled.") # 7. Timezone inst.set_timezone(timezone) # 8. User account + groups _step(f"Creating user '{username}'...") user = User( username = username, password = password, sudo = True, groups = ["seat", "video", "input", "audio"], ) inst.create_users(user) _ok(f"User '{username}' created with sudo access.") # 9. Root password — same as user password (no separate root cred to remember) inst.set_user_password(User("root", password, sudo=False)) _ok("Root password set.") # 10. Ensure Hyprland session file exists for GDM _step("Configuring GDM session entry for Hyprland...") sessions_dir = mountpoint / "usr/share/wayland-sessions" sessions_dir.mkdir(parents=True, exist_ok=True) session_file = sessions_dir / "hyprland.desktop" if not session_file.exists(): session_file.write_text( "[Desktop Entry]\n" "Name=Hyprland\n" "Comment=An intelligent dynamic tiling Wayland compositor\n" "Exec=Hyprland\n" "Type=Application\n" ) _ok("GDM / Hyprland session configured.") # 11. pacman.conf tweaks _step("Patching pacman.conf...") pcfg = mountpoint / "etc/pacman.conf" txt = pcfg.read_text() # Enable parallel downloads if "#ParallelDownloads" in txt: txt = txt.replace("#ParallelDownloads = 5", "ParallelDownloads = 5") # Prevent illogical-impulse AUR group from being upgraded via pacman # (required by the dotfiles project: https://ii.clsty.link/en/ii-qs/01setup/) if "IgnoreGroup=illogical-impulse" not in txt: txt += ( "\n# Prevent AUR conflict with illogical-impulse dotfiles\n" "IgnoreGroup=illogical-impulse\n" ) pcfg.write_text(txt) _ok("pacman.conf patched.") # 12. yay AUR helper _install_yay(inst, username) print() print(" \033[0;32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m") print(" \033[0;32m Arch Linux + Hyprland + GDM installed successfully.\033[0m") print(" \033[0;32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m") if __name__ == "__main__": main() PYEOF # Write multiple fallback mirrors before pacstrap so large packages have # alternative sources if geo.mirror.pkgbuild.com throttles mid-download. { echo 'Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch' echo 'Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch' echo 'Server = https://mirrors.mit.edu/archlinux/$repo/os/$arch' echo 'Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch' echo 'Server = https://mirror.leaseweb.net/archlinux/$repo/os/$arch' } > /etc/pacman.d/mirrorlist python3 /tmp/_archauto.py 2>&1 | tee /tmp/_py_install.log; _PYRC=${PIPESTATUS[0]} [[ $_PYRC -eq 0 ]] || { echo -e "\n${R}[ERROR] Python installer failed. Last output:${N}"; tail -20 /tmp/_py_install.log; exit $_PYRC; } # ── Step 5: illogical-impulse dotfiles ──────────────────────────────────────── echo "" echo -e "${Y}[5/5] Installing illogical-impulse dotfiles (Quickshell + Hyprland config)${N}" echo "" echo -e " ${C}→${N} Cloning end-4/dots-hyprland and running automated setup..." echo "" # Clone directly from GitHub (more reliable than ii.clsty.link/get). # Flags: # --skip-allgreeting skip the greeting/pause screens # --force ask=false — no "Execute? [y/e/s]" prompts from v() # --skip-sysupdate skip pacman -Syu (already up to date from our install) # yes 'i' is piped to stdin so that x() error-retry loops auto-choose "ignore" # and the install continues rather than hanging on a retryable failure. # Temporary NOPASSWD rule so dotfiles setup can run sudo without a password prompt. echo "$USERNAME ALL=(ALL) NOPASSWD: ALL" > /mnt/etc/sudoers.d/99-dotfiles-tmp chmod 440 /mnt/etc/sudoers.d/99-dotfiles-tmp arch-chroot /mnt su - "$USERNAME" -c " git clone --filter=blob:none --recurse-submodules \ https://github.com/end-4/dots-hyprland.git ~/.cache/dots-hyprland && cd ~/.cache/dots-hyprland && yes 'i' | ./setup install --skip-allgreeting --force --skip-sysupdate " || { echo -e "\n${Y}[!] Dotfiles installer had issues — re-run after reboot:${N}" echo -e " ${C}cd ~/.cache/dots-hyprland && ./setup install${N}" } # Remove temporary NOPASSWD rule (user still has sudo via %wheel in /etc/sudoers) rm -f /mnt/etc/sudoers.d/99-dotfiles-tmp # ── Done ────────────────────────────────────────────────────────────────────── echo "" echo -e "${G}" echo " ╔══════════════════════════════════════════════════════════╗" echo " ║ Installation Complete! ║" echo " ╚══════════════════════════════════════════════════════════╝" echo -e "${N}" echo " Quick-start:" echo " Ctrl+Super+T — pick a wallpaper" echo " Super+/ — keybind cheatsheet" echo "" echo " To update dotfiles later:" echo " cd ~/.cache/dots-hyprland && git stash && git pull && ./setup install" echo "" echo -e " Type '${C}reboot${N}' when ready." echo ""