#!/usr/bin/env python3 """ Arch Linux + Hyprland — Non-Interactive Standalone Installer ───────────────────────────────────────────────────────────── Run directly from the Arch Linux live ISO by exporting these environment variables before calling the script: export ARCH_DRIVE=/dev/sda # block device to wipe & install on export ARCH_USER=myuser # primary (sudo) username export ARCH_PASS=mysecretpassword # password (also used for root) export ARCH_HOSTNAME=archlinux # optional, default: archlinux export ARCH_TZ=Europe/Berlin # optional, default: UTC python install.py ──────────────────────────────────────────────────────────── Normally you would just run the one-shot bootstrap instead: bash <(curl -s https://YOURDOMAIN.COM/get) That script handles all prompts and then calls this file automatically. ───────────────────────────────────────────────────────────── What this installs (all hardcoded): • Arch Linux — Btrfs root (@, @home, @log, @cache) + FAT32 ESP /boot • systemd-boot • Hyprland (Wayland) via archinstall built-in profile • greetd + Quickshell greeter (Quickshell.Services.Greetd API, cage compositor) • seatd (seat manager) • Pipewire + WirePlumber • bluez + bluez-utils (Bluetooth) • NetworkManager • ZRAM swap (zstd) • base-devel + go (for AUR) • yay (AUR helper, built from AUR) """ 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: # ── Read config from environment ────────────────────────────────────────── 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]): print( "\nUsage:\n" " export ARCH_DRIVE=/dev/sdX\n" " export ARCH_USER=myuser\n" " export ARCH_PASS=mypassword\n" " export ARCH_HOSTNAME=archlinux # optional\n" " export ARCH_TZ=UTC # optional\n" " python install.py\n" ) sys.exit(1) 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 object total = device.device_info.total_size # Size object 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 of disk # Leave 1 MiB at the end for GPT backup header 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, # btrfs — 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 (for 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 bypassed. # 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. Primary user with sudo + relevant 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 (so 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: https://ii.clsty.link/en/ii-qs/01setup/#avoid-aur-conflict 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 Next: run dotfiles installer, then reboot.\033[0m") print(" \033[0;32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m") print() if __name__ == "__main__": main()