#!/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 — greetd + Quickshell.Services.Greetd greeter (cage compositor) # • 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 " ║ Dotfiles : illogical-impulse (end-4/dots-hyprland) ║" 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 # ── 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 loop=7 and optical=11) readarray -t _PATHS < <( lsblk -d -n -o PATH --exclude 7,11 2>/dev/null \ | 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 = ( "cd ~ && rm -rf yay && " "git clone --depth=1 https://aur.archlinux.org/yay.git && " "cd yay && makepkg -s --noconfirm --skippgpcheck" ) installation.arch_chroot(build_cmd, run_as=username, peek_output=True) installation.arch_chroot( f"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([ # Network "networkmanager", # Bluetooth "bluez", "bluez-utils", # Audio — full Pipewire stack "pipewire", "pipewire-alsa", "pipewire-pulse", "pipewire-jack", "wireplumber", # Seat manager (for Hyprland) "seatd", # Login manager + minimal Wayland compositor for Quickshell greeter "greetd", "cage", # 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. # greetd handles the login screen via the Quickshell greeter. _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.") # 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", "greetd", ]) _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. greetd + Quickshell greeter # cage provides a minimal Wayland compositor so quickshell can run as greeter. # The QML uses Quickshell.Services.Greetd to authenticate and launch Hyprland. _step("Configuring greetd + Quickshell greeter...") greetd_qs = mountpoint / "etc/greetd/quickshell" greetd_qs.mkdir(parents=True, exist_ok=True) # greetd daemon config (mountpoint / "etc/greetd/config.toml").write_text( "[terminal]\n" "vt = 1\n" "\n" "[default_session]\n" 'command = "cage -s -- quickshell -p /etc/greetd/quickshell"\n' 'user = "greeter"\n' ) # Quickshell QML greeter — Catppuccin Mocha palette # PanelWindow with all anchors = fullscreen via wlr-layer-shell (cage supports it). # Using explicit id 'win' so nested handlers can write win.errorMsg. (greetd_qs / "shell.qml").write_text("""\ import Quickshell import Quickshell.Services.Greetd import QtQuick import QtQuick.Controls.Basic ShellRoot { PanelWindow { id: win property string errorMsg: "" // Fill the entire screen via layer-shell anchors anchors { top: true bottom: true left: true right: true } Rectangle { anchors.fill: parent color: "#1e1e2e" Column { anchors.centerIn: parent spacing: 16 width: 320 Text { text: "Welcome" color: "#cdd6f4" font.pixelSize: 28 font.bold: true horizontalAlignment: Text.AlignHCenter width: parent.width } TextField { id: userField width: parent.width placeholderText: "Username" color: "#cdd6f4" placeholderTextColor: "#6c7086" background: Rectangle { color: "#313244"; radius: 6 } padding: 10 } TextField { id: passField width: parent.width placeholderText: "Password" echoMode: TextInput.Password color: "#cdd6f4" placeholderTextColor: "#6c7086" background: Rectangle { color: "#313244"; radius: 6 } padding: 10 Keys.onReturnPressed: loginBtn.clicked() } Text { visible: win.errorMsg !== "" text: win.errorMsg color: "#f38ba8" width: parent.width wrapMode: Text.Wrap } Button { id: loginBtn width: parent.width text: "Login" contentItem: Text { text: parent.text color: "#1e1e2e" horizontalAlignment: Text.AlignHCenter } background: Rectangle { color: "#89b4fa"; radius: 6 } onClicked: { win.errorMsg = "" Greetd.createSession(userField.text) } } } } Connections { target: Greetd function onAuthMessage(message, error, responseRequired, echoResponse) { if (responseRequired) { Greetd.respond(passField.text) } } function onReadyToLaunch() { Greetd.launch(["Hyprland"]) } function onAuthFailure(message) { win.errorMsg = "Authentication failed. Please try again." Greetd.cancelSession() } function onError(err) { win.errorMsg = err } } } } """) # greeter user needs GPU + seat access to run quickshell under cage inst.arch_chroot("usermod -aG seat,video greeter") _ok("greetd + Quickshell greeter 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 installed successfully.\033[0m") print(" \033[0;32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m") if __name__ == "__main__": main() PYEOF python3 /tmp/_archauto.py rm -f /tmp/_archauto.py # ── Step 5: illogical-impulse dotfiles ──────────────────────────────────────── echo "" echo -e "${Y}[5/5] Installing illogical-impulse dotfiles (Quickshell + Hyprland config)${N}" echo "" echo " The dotfiles installer is interactive — it will ask which" echo " components to install. Follow its prompts below." echo "" echo " Tip: press Enter on most prompts to accept defaults for a full install." echo "" sleep 2 # Download the bootstrap script first — process substitution bash <(curl ...) # does not work reliably inside arch-chroot, so we save it to a temp file. curl -fsSL https://ii.clsty.link/get -o /mnt/tmp/_dots_install.sh chmod +x /mnt/tmp/_dots_install.sh arch-chroot /mnt su - "$USERNAME" -c "bash /tmp/_dots_install.sh" rm -f /mnt/tmp/_dots_install.sh # ── 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 ""