unl0kr already prompts for the LUKS passphrase on screen, so a login manager on top of it would be a second prompt. The login screen now only collects a fallback (none/greetd/sddm/gdm) when unl0kr is selected; resolve_login_manager applies it only when install_unl0kr falls back to the systemd initramfs, and the summary shows both states. Also ships /etc/unl0kr.conf (breezy-dark) plus the empty /usr/share/unl0kr/unl0kr.conf the package omits so unl0kr stops warning about an unparseable config, and pins Hyprland in the user's AccountsService record so gdm autologin does not fall back to gnome.desktop.
Complete Guide to Creating a Custom Arch Linux ISO
DistroTube: https://www.youtube.com/watch?v=tSGGBbJBgvk&t=1228s
Dotfiles install file structure:
AlvinArch/
├── airootfs/
│ ├── etc/skel/
│ │ ├── .config/bat/ # bat + catppuccin theme
│ │ ├── .config/fish/ # 20 files, quickshell guard
│ │ ├── .config/nvim/ # LazyVim (14 files)
│ │ ├── .config/hypr/custom/ # 8 files (kdeconnectd + cursor only)
│ │ ├── .config/starship.toml
│ │ ├── .zshrc # adapted (no hardcoded paths)
│ │ └── .fzf.zsh
│ └── root/customize_airootfs.sh # pre-installs oh-my-zsh, lazy.nvim, plugins
├── localrepo/
│ ├── build-end4-deps.sh # non-interactive AUR build (all deps)
│ ├── end4-pkgbuilds/ # microtex-git, quickshell-git, bibata
│ ├── illogical-impulse-bibata-modern-classic-bin-*.pkg.tar.zst
│ └── update.sh # existing, regenerates repo db
└── packages.x86_64 # 420 packages (65+ end-4 deps added)
Table of Contents
- Overview
- Prerequisites
- Directory Structure
- Core Configuration Files
- User Configuration (skel/)
- System Services & Networking
- Local Repository (localrepo/)
- Boot Configuration
- Automated Installer (alvin-install)
- Build & Test
- Planned Additions
- Troubleshooting
Overview
AlvinArch is a custom Arch Linux live ISO built on the archiso framework. It ships a Hyprland desktop with end-4 dots-hyprland, a curated set of ~400 packages, and a local repository of pre-built AUR packages for packages not available in official repos. The ISO targets both real hardware and VMs (VirtualBox/Hyper-V/VMware guest services are enabled out of the box).
Key Features
- Hyprland + end-4 — Wayland compositor with illogical-impulse dotfiles (quickshell, bibata cursors, microtex)
- Automated installer (
alvin-install) — gum TUI: LUKS with systemd initramfs or experimental touchscreen unl0kr, optional TPM2 auto-unlock, GitHub SSH key import, login manager choice (none/greetd/sddm/gdm), optional sshd (on by default), autologin; the build stamp identifies the live ISO via its/etc/motd - LazyVim — Pre-configured Neovim with lazy.nvim and curated plugins
- Fish + Zsh + Starship — Dual shell setup with oh-my-zsh and a cross-shell prompt
- Local AUR repo — Pre-built packages (end-4 deps, wallust, grub-customizer, etc.) that aren't in official repos
- Curated packages — ~400 packages across ccache, networking, dev tools, and desktop utilities
- Guest VM support — VirtualBox, Hyper-V, and VMware tools auto-enabled
Prerequisites
Required Tools
# Install archiso and dependencies
sudo pacman -S archiso git base-devel
# Optional but recommended
sudo pacman -S arch-install-scripts squashfs-tools
Knowledge Requirements
- Basic Linux system administration
- Understanding of Arch Linux package management
- Shell scripting basics
- Git version control (for managing your project)
Directory Structure
Complete Project Layout
AlvinArch/
├── profiledef.sh # ISO metadata, compression, boot modes, file permissions
├── packages.x86_64 # ~400 live-environment packages (Hyprland, end-4, dev tools)
├── bootstrap_packages # Minimal build-time packages (base, arch-install-scripts)
├── pacman.conf # Repos: cachyos → core → extra → multilib → chaotic-aur → local-aur
├── airootfs/ # Root filesystem overlay (copied into live ISO)
│ ├── etc/
│ │ ├── os-release # NAME="Alvin Arch Linux", ID=arch
│ │ ├── hostname # Default hostname: archiso
│ │ ├── motd # Welcome text with install instructions and color bar
│ │ ├── locale.conf # LANG=en_AU.UTF-8
│ │ ├── locale.gen # Locale generation file
│ │ ├── localtime → ../usr/share/zoneinfo/... # Timezone symlink
│ │ ├── resolv.conf # DNS resolver config
│ │ ├── passwd # System user accounts
│ │ ├── shadow # Password hashes (mode 400)
│ │ ├── clamav/ # ClamAV antivirus config (certs, clamd, freshclam, milter)
│ │ ├── skel/ # Home directory template for new users
│ │ │ ├── .config/
│ │ │ │ ├── bat/ # bat + Catppuccin Macchiato theme
│ │ │ │ ├── fish/ # Fish shell config (~20 files, nvm, fisher, hyprland helper)
│ │ │ │ ├── nvim/ # LazyVim setup (init.lua, plugins/, config/)
│ │ │ │ ├── hypr/custom/ # Hyprland dotfiles (keybinds, rules, execs, env, general, scripts)
│ │ │ │ └── starship.toml # Starship cross-shell prompt
│ │ │ ├── .zshrc # Zsh config (oh-my-zsh compatible)
│ │ │ └── .fzf.zsh # Fzf shell integration
│ │ ├── systemd/
│ │ │ ├── system/ # Service files and enablement symlinks
│ │ │ │ ├── choose-mirror.service # Auto mirror selection
│ │ │ │ ├── pacman-init.service # Keyring initialization
│ │ │ │ ├── livecd-alsa-unmuter.service # Unmute ALSA on boot
│ │ │ │ ├── livecd-talk.service # LiveCD talk helper
│ │ │ │ ├── multi-user.target.wants/ # Enabled services (iwd, NetworkManager, sshd, vboxservice,
│ │ │ │ │ # hv_kvp/vss/fcopy, vmtoolsd, ModemManager, etc.)
│ │ │ │ ├── sysinit.target.wants/ # systemd-timesyncd, systemd-time-wait-sync
│ │ │ │ ├── sockets.target.wants/ # systemd-networkd, pcscd
│ │ │ │ ├── network-online.target.wants/ # systemd-networkd-wait-online
│ │ │ │ └── sound.target.wants/ # livecd-alsa-unmuter
│ │ │ ├── networkd.conf.d/ # IPv6 privacy extensions
│ │ │ ├── resolved.conf.d/ # systemd-resolved config
│ │ │ ├── journald.conf.d/ # Volatile journal storage
│ │ │ └── logind.conf.d/ # Do-not-suspend policy
│ │ ├── pacman.d/
│ │ │ ├── hooks/
│ │ │ │ ├── uncomment-mirrors.hook # Enable mirrors after install
│ │ │ │ └── zzzz99-remove-custom-hooks-from-airootfs.hook
│ │ │ ├── chaotic-mirrorlist # Chaotic AUR mirrors
│ │ │ └── cachyos-mirrorlist # CachyOS mirrors
│ │ ├── ssh/sshd_config.d/ # SSH live-environment config (10-archiso.conf)
│ │ ├── modprobe.d/ # broadcom-wl.conf (Broadcom WiFi driver)
│ │ └── mkinitcpio.conf.d/ # archiso initramfs config
│ ├── usr/
│ │ ├── lib/os-release # usr-merge os-release
│ │ ├── local/bin/ # alvin-install, choose-mirror, Installation_guide, livecd-sound
│ │ ├── local/share/livecd-sound/ # ALSA config (asound.conf.in)
│ │ └── share/pacman/keyrings/ # CachyOS GPG keyring + trust data
│ └── root/
│ ├── .automated_script.sh # Automated install entry point
│ ├── .zlogin # Zsh login hook
│ ├── .gnupg/scdaemon.conf # GPG smartcard config
│ ├── .live-pacman.conf # Pacman config for live session
│ └── customize_airootfs.sh # Build-time chroot script (oh-my-zsh, lazy.nvim, plugins)
├── localrepo/ # Pre-built AUR packages (indexed as local-aur repo)
│ ├── build-end4-deps.sh # Non-interactive AUR builder for end-4 dependencies
│ ├── update.sh # Regenerates repo DB + syncs names to packages.x86_64
│ ├── end4-pkgbuilds/ # PKGBUILDs: quickshell-git, microtex-git, bibata-modern-classic-bin
│ ├── *.pkg.tar.zst # Pre-built packages (illogical-impulse-*, wallust, grub-customizer,
│ │ # snapd, upscayl, eternalterminal, etc.)
│ └── local-aur.db{,.tar.gz} # Repo database files
├── efiboot/ # UEFI boot (systemd-boot)
│ └── loader/
│ ├── loader.conf
│ └── entries/
│ ├── 01-archiso-linux.conf
│ ├── 02-archiso-speech-linux.conf
│ └── 03-archiso-memtest86+x64.conf
├── grub/ # GRUB boot (BIOS + UEFI)
│ ├── grub.cfg
│ └── loopback.cfg
└── syslinux/ # BIOS boot (syslinux)
├── syslinux.cfg
├── archiso_head.cfg
├── archiso_pxe-linux.cfg
├── archiso_sys.cfg
├── archiso_sys-linux.cfg
├── archiso_tail.cfg
└── splash.png
Core Configuration Files
profiledef.sh
Defines the entire ISO build. Key settings: iso_label="AlvinArch_...", bootmodes (BIOS syslinux + UEFI systemd-boot), airootfs_image_type="squashfs" with xz compression. Edit first to change branding or compression.
packages.x86_64
The main package list (~400 packages). Packages from allowed repos (cachyos, extra, multilib, chaotic-aur) are installed directly. Packages in localrepo/*.pkg.tar.zst are pulled from the [local-aur] repo. Keep sorted alphabetically.
bootstrap_packages
Minimal packages for the mkarchiso bootstrap phase: base and arch-install-scripts.
pacman.conf
Defines the repo order: cachyos → core → extra → multilib → chaotic-aur → local-aur. The [local-aur] repo points to the local localrepo/ directory for pre-built AUR packages.
User Configuration (skel/)
Dotfiles in airootfs/etc/skel/ are copied into every new user's home directory on the live ISO.
.config/bat/
bat with a Catppuccin Macchiato color theme. Provides bat as a cat replacement.
.config/fish/
Fish shell configuration (~20 files). Includes nvm integration, fisher plugin manager, Hyprland auto-detection (auto-Hypr.fish), frozen theme/keybindings, and custom functions (edit, update, lzd).
.config/nvim/
Full LazyVim setup powered by lazy.nvim. Includes init.lua, plugin specs for Obsidian, Arduino, and Harper (grammar), plus standard LazyVim config/options/keymaps/autocmds.
.config/hypr/custom/
Hyprland dotfiles from end-4 dots-hyprland:
| File | Purpose |
|---|---|
keybinds.lua |
Window manager keybindings |
general.lua |
Appearance, gaps, border settings |
rules.lua |
Window rules (floating, size, class matching) |
execs.lua |
Autostart commands |
env.lua |
Environment variables |
variables.lua |
Shared variable definitions |
scripts/ |
Helper scripts (video wallpaper restore, paste-as-type) |
.config/starship.toml
Cross-shell prompt config for Starship. Works with both Fish and Zsh.
.zshrc / .fzf.zsh
Zsh configuration adapted for oh-my-zsh (installed by customize_airootfs.sh) and fzf shell integration. fish is the default login shell; zsh is there if you chsh to it. The .zshrc source lives at airootfs/root/.zshrc-skel, because grml-zsh-config owns /etc/skel/.zshrc and shipping it under airootfs/etc/skel/ would abort the build. zsh-autocomplete is cloned but disabled; see Troubleshooting.
System Services & Networking
Services (enabled via symlinks)
| Service | Purpose |
|---|---|
iwd |
WiFi management |
NetworkManager |
Network connections |
sshd |
SSH access to live environment |
vboxservice |
VirtualBox guest additions |
hv_kvp_daemon, hv_vss_daemon, hv_fcopy_daemon |
Hyper-V integration |
vmtoolsd, vmware-vmblock-fuse |
VMware guest tools |
ModemManager |
Mobile broadband |
pacman-init |
Initializes GPG keyring before package operations |
choose-mirror |
Auto mirror selection (triggered by mirror kernel param) |
livecd-alsa-unmuter |
Unmutes audio on live boot |
Network Config
- systemd-networkd with IPv6 privacy extensions enabled
- systemd-resolved for DNS
- DHCP auto-configured for wired (ethernet) and wireless (iwd)
Other System Config
- ClamAV — Antivirus daemon config included in
airootfs/etc/clamav/ - mkinitcpio — Archiso-specific initramfs config
- logind — No-suspend policy for live session
- journald — Volatile (in-memory) log storage
Local Repository (localrepo/)
Pre-built AUR packages that aren't available in any allowed repo. The local-aur repo is configured in pacman.conf and picked up automatically by mkarchiso.
Scripts
build-end4-deps.sh— Non-interactive AUR build for all end-4 (illogical-impulse) dependencies. Run--quickto skip large packages.update.sh— Regenerates the repo database and syncs new package names topackages.x86_64.
end4-pkgbuilds/
Custom PKGBUILDs for end-4 metapackages:
quickshell-git— Quickshell (Qt6/QML shell for Wayland)microtex-git— LaTeX previewerbibata-modern-classic-bin— Bibata cursor theme
Pre-built Packages
Packages like illogical-impulse-bibata-modern-classic-bin, wallust, grub-customizer, snapd, upscayl-bin, xwaylandvideobridge, eternalterminal, etc.
Boot Configuration
UEFI — systemd-boot (efiboot/)
Three boot entries: standard Arch Linux, speech/accessibility, and MemTest86+.
BIOS — GRUB (grub/)
grub.cfg for BIOS/CSM boot, loopback.cfg for booting the ISO from a mounted image.
BIOS — syslinux (syslinux/)
Legacy BIOS boot with custom splash screen. Includes PXE network boot config.
Automated Installer (alvin-install)
The ISO ships an interactive installer at /usr/local/bin/alvin-install (source:
airootfs/usr/local/bin/alvin-install, ~1,880 lines of bash; the live /etc/motd
advertises it). It partitions, encrypts, installs, and fully configures a system —
including the end-4 dotfiles — without network access to this repo.
Running it
alvin-install # as root on the live medium (root autologins on tty1)
Requirements: UEFI boot (it exits if /sys/firmware/efi is missing — BIOS/CSM
is not supported by the installer) and gum. A network connection is required and
verified by pinging archlinux.org before any disk is touched; the network screen
can open nmtui for WiFi. Every step is logged to /var/log/alvin-install.log,
which is copied into the installed system.
What it asks (gum TUI screens)
Keymap → network → target disk (the live USB is hidden from the list) → LUKS
encryption (optional; the LUKS passphrase is the same as your login password)
→ experimental unl0kr touchscreen unlock → filesystem (ext4/btrfs) →
username/password/hostname → git identity + GitHub username (both optional) →
timezone/locale → login manager → SSH server (enabled by default) → summary.
What it does
- GPT partitioning (1 GiB ESP) on the chosen disk, optional LUKS2 on the rest,
format + mount at
/mnt pacstrapwith the package list derived from the medium'spkglist.x86_64.txt(pacman -Qof the live ISO) —archiso,syslinux,memtest86+etc. are dropped, hardcoded essentials (kernel, GRUB, NetworkManager, cryptsetup, microcode) always appended- User account, sudo, git config, GitHub SSH keys (
ssh-import-id, falling back togithub.com/<user>.keys) - end-4 dotfiles from the staged
/opt/dots-hyprlandvia./setup install -f -s --skip-alldeps - Initramfs in one of two modes: Mode A — systemd initramfs +
sd-encrypt(default); Mode B — busybox hooks + unl0kr touchscreen unlock (experimental, mutually exclusive with TPM2; falls back to Mode A on failure) - Optional TPM2 auto-unlock (
systemd-cryptenroll, PCR 0+7; passphrase slot kept as fallback; warns if Secure Boot is off) - GRUB (UEFI, boot entry
ALVIN), login manager —none(plain tty, Hyprland autostarts),greetd+tuigreet,sddm, orgdm— falls back tononeonly if the package can't be resolved/installed; a failedsystemctl enablekeepsgetty@tty1as the login path (the summary still reports the chosen manager). Optional autologin (forced off when TPM2 is enrolled), and sshd enabled at boot when selected (default: on) — Arch's stock config allows password login with the same password as LUKS, so only accept the default on networks you trust (or hardensshd_configafterwards). The final banner reports whether the enable actually succeeded, not just what was selected
On failure an EXIT trap unmounts /mnt and closes the cryptroot mapping, so
re-running is safe. If the end-4 step reports "incomplete", see the repository's
AGENTS.md Gotchas for the chroot/systemd limitations that cause it.
Build & Test
Building the ISO
# Build the ISO (repo root IS the profile directory)
sudo mkarchiso -v -w work -o out/ .
# Build AUR deps for end-4 (run locally before ISO build if localrepo/ is stale)
./localrepo/build-end4-deps.sh # build all
./localrepo/build-end4-deps.sh --quick # skip large AUR pkgs
# Refresh localrepo DB after adding/moving .pkg.tar.zst files
./localrepo/update.sh # re-indexes existing packages
./localrepo/update.sh pkg1.pkg.tar.zst # add files, then re-index
CI Checks (must pass before merge)
- ShellCheck: all
.shfiles, plus extensionless bash scripts inairootfs/usr/local/bin/, checked withshellcheck -x -s bash - yamllint: all
.yml/.yamlfiles (relaxed config) - packages.x86_64 syntax: one package per line, no trailing whitespace,
[a-zA-Z0-9._@+\-]+only - Package repo check: every package in
packages.x86_64must be resolvable viapacman -Siin allowed repos (cachyos,extra,multilib,chaotic-aur), OR have a pre-built.pkg.tar.zstinlocalrepo/, OR appear inlocalrepo/build-end4-deps.sh
Git Hooks
A pre-commit hook (githooks/pre-commit) runs the ShellCheck pass on every commit, so lint failures surface before you push instead of in CI.
# Enable once per clone
git config core.hooksPath githooks
# Bypass the hook for a single commit
git commit --no-verify
If shellcheck is not installed the hook warns and skips rather than blocking the commit; CI remains the enforcer.
Recommended VirtualBox Settings (for testing)
- RAM: 4GB minimum, 8GB recommended
- Video Memory: 128MB
- 3D Acceleration: Enabled
- Storage: 40GB minimum
Guest additions (vboxservice), Hyper-V (hv_*), and VMware (vmtoolsd) tools are all auto-enabled in the live environment.
Planned Additions
The following dotfiles are not yet included but are planned for future versions:
| Component | Status | Notes |
|---|---|---|
| Alacritty terminal config | 🚧 Planned | Catppuccin Macchiato theme to match bat |
| GTK 3.0/4.0 theme config | 🚧 Planned | Consistent theming across GTK apps |
| Fontconfig custom settings | 🚧 Planned | Font rendering and fallback rules |
| Dunst notification daemon | 🚧 Planned | Notification appearance and behavior |
Troubleshooting
ISO won't boot
- Verify the ISO was built with
sudo mkarchiso(not as a regular user) - Check that UEFI or BIOS/CSM is correctly set in VM firmware settings
- For Ventoy: use the
.isodirectly, don't extract
No network in live environment
iwdhandles WiFi — runiwctlto connect manually if NetworkManager doesn't detect networks- Ethernet via systemd-networkd with DHCP should work automatically
Package installation fails during build
- Run
./localrepo/update.shto refresh the localrepo database - Ensure
localrepo/*.pkg.tar.zstfiles are valid and not corrupted - Check that the package name in
packages.x86_64matches exactly
LazyVim / oh-my-zsh not installed
customize_airootfs.shclones repos at build time — requires network access duringmkarchiso- Check build logs for git clone failures
zsh-autocomplete partial-load error
Symptom:
_autocomplete__history_lines: function not defined
_autocomplete__unambiguous: function not defined
Cause: zsh-autocomplete was loaded after compinit (e.g. via oh-my-zsh
plugins=(...)), a stale completion dump, or a conflict with
zsh-autosuggestions / zsh-history-substring-search. On this ISO compinit
always runs first: grml-zsh-config's global /etc/zsh/zshrc calls it
unconditionally before ~/.zshrc is read.
The shipped ~/.zshrc leaves zsh-autocomplete out of plugins=(...). The
plugin stays cloned at ~/.oh-my-zsh/custom/plugins/zsh-autocomplete/
either way, just disabled by default.
Fix: drop autocomplete (recommended)
# 1. Edit ~/.zshrc, remove this line from plugins=(...):
# zsh-autocomplete
# 2. Clear stale dumps (bash avoids zsh's nomatch abort on the glob):
bash -c 'rm -f ~/.zcompdump* ~/.cache/zsh/zcompdump*'
# 3. Restart the shell and verify:
exec zsh
typeset -f _autocomplete__history_lines >/dev/null && echo "still loaded" || echo "clean"
Fix: keep autocomplete instead
# 1. Remove zsh-autocomplete, zsh-autosuggestions and
# zsh-history-substring-search from plugins=(...) in ~/.zshrc.
# 2. Stop the global compinit. `skip_global_compinit=1` in ~/.zshenv does
# NOT work here (that is a Debian/Ubuntu /etc/zsh/zshrc convention; grml
# ignores it), so remove the package that ships the global zshrc:
sudo pacman -R grml-zsh-config
# 3. At the very top of ~/.zshrc, before oh-my-zsh is sourced:
source ~/.oh-my-zsh/custom/plugins/zsh-autocomplete/zsh-autocomplete.plugin.zsh
# 4. Clear dumps as above, then: exec zsh
Acknowledgements
- Arch Linux and the archiso project
- end-4 dots-hyprland for the illogical-impulse Hyprland config
- LazyVim for the Neovim starter config
- DistroTube for the archiso walkthrough that inspired this project