The fork's colour/wallpaper/kvantum scripts still hard-code QUICKSHELL_CONFIG_NAME="ii", so they read and write ~/.config/quickshell/ii instead of end4-pC - rewrite them to end4-pC at build time (verified against the current fork HEAD: 5 scripts match the anchored pattern, re-runs are no-ops). Also replace the fork's assets/images/default_wallpaper.png - fed to switchwall.sh by FirstRunExperience.qml on first login - with the wallpaper now shipped in skel's ~/Pictures/Wallpapers, so the first login lands on the japan-street wallpaper instead of the fork's default.
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