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.
14 KiB
AGENTS.md
Arch Linux ISO profile (archiso). Builds a live ISO with Hyprland desktop, end-4 dots-hyprland, and curated packages. Hosted on Gitea.
Build & Test Commands
# 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
update.sh also refreshes airootfs/opt/localrepo/ (the copy that ships on the
ISO), so run it after touching localrepo/ before building.
CI Checks (must pass before merge)
- ShellCheck: all
.shfiles, plus extensionless bash scripts inairootfs/usr/local/bin/(by shebang), 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
Package Management
Three sources contribute packages to the ISO:
packages.x86_64— main list, installed by mkarchiso. Must be in an allowed repo.localrepo/*.pkg.tar.zst— pre-built AUR packages. Thelocal-aurrepo is configured inpacman.confand picked up by mkarchiso automatically.build-end4-deps.sh— builds end-4 AUR deps at build time.
There is a fourth source: customize_airootfs.sh runs end-4's own
./setup install-deps inside the image (as a throwaway ii-builder account),
which resolves and compiles everything the dotfiles need (quickshell, MicroTeX,
theming, fonts, ...). Those packages are installed in the live system, land in
pkglist.x86_64.txt, and are harvested into /opt/localrepo so pacstrap can
install them into the target without the network.
Important: localrepo/update.sh auto-appends new package names to packages.x86_64. If you remove a package from packages.x86_64 that exists in localrepo/, it will be re-added on the next update.sh run.
Key Files
| File | Purpose |
|---|---|
packages.x86_64 |
Live ISO package list (~402 packages) |
profiledef.sh |
ISO metadata, compression, boot modes, build-stamp staging |
pacman.conf |
Repos: cachyos → core → extra → multilib → chaotic-aur → local-aur |
bootstrap_packages |
Minimal build-time packages (base, arch-install-scripts) |
airootfs/root/customize_airootfs.sh |
Runs inside chroot during build: keyrings, skel dotfiles (incl. end4-pC), qsConfig patch, end-4 install-deps, package harvest, uv/font pre-warm |
airootfs/root/.live-pacman.conf |
/etc/pacman.conf of the built image (adds [local-aur] = file:///opt/localrepo); also what pacstrap resolves the target from, so [cachyos] must stay ahead of [core] like in pacman.conf |
airootfs/usr/local/bin/alvin-install |
Installer: partitions, pacstrap, LUKS (systemd initramfs or experimental unl0kr + optional TPM2 unlock), user (fish shell), git identity + GitHub SSH keys, login manager (none/greetd/sddm/gdm) + autologin, optional sshd (default on), end-4 dotfiles (--skip-alldeps), boot |
localrepo/build-end4-deps.sh |
Non-interactive AUR builds for end-4 dependencies |
localrepo/update.sh |
Regenerates localrepo DB, syncs names to packages.x86_64, syncs airootfs/opt/localrepo/ |
localrepo/check-quickshell-pin.sh |
Tripwire: our quickshell pin and end-4's must include the Qt 6.12 fix and stay <45d old (QUICKSHELL_PIN_MAX_AGE_DAYS overrides) |
.gitea/workflows/pin-check.yaml |
Runs the pin check weekly (Mon 06:00), on dispatch, and on pushes touching pins/customize_airootfs.sh |
alvin-install (installer inside the ISO)
Source: airootfs/usr/local/bin/alvin-install (~1,880 lines, no .sh extension).
The airootfs overlay ships it as /usr/local/bin/alvin-install (mode 0755 forced in
profiledef.sh file_permissions), and /etc/motd advertises it on the live medium.
- Invoke: as root from the live session. Hard requirements:
gumand UEFI —check_prereqsexits if/sys/firmware/efiis missing. Log:/var/log/alvin-install.log(copied into the target at the end;ERRtrap prints the failing line). - Structure:
main()runs 16screen_*TUI functions (collect answers: welcome, keymap, network, disk, luks, unl0kr, filesystem, username, password, hostname, git, timezone, locale, login manager, sshd, summary) then a fixed order ofinstall_*steps (partition → LUKS → format/mount → pacstrap → fstab → repos → timezone → locale → hostname → network → sshd → user → git/SSH → end-4 dots → mkinitcpio → bootloader → TPM/unl0kr → login manager (none if unl0kr succeeded; with unl0kr selected the login screen only asks for a fallback) → autologin → update (reflector + keyrings +pacman -Syu, warn-only) → cleanup). Screens and install steps are independent — edit one without touching the other. AnEXITtrap unmounts$TARGETand closescryptrooton any exit. - Package list:
install_pacstrapbuilds the installed system's list from the medium's/run/archiso/bootmnt/arch/pkglist.x86_64.txt(i.e.pacman -Qof the live ISO), sopackages.x86_64changes flow into installed systems. It dropsPKGS_EXCLUDE(archiso mkinitcpio-archiso mkinitcpio-nfs-utils syslinux memtest86+) plus names not resolvable in the live repos, then always appends hardcoded essentials (base linux linux-firmware linux-cachyos networkmanager grub efibootmgr sudo git …). - Linting: CI's shellcheck step picks it up by its bash shebang. Locally:
shellcheck -x -s bash airootfs/usr/local/bin/alvin-install - end-4 quirks (
--skip-alldeps,yes i, end4-pC re-copy) are in Gotchas below.
Verify a single change locally (mirrors CI)
shellcheck -x -s bash <script>.sh # plus the alvin-install command above
yamllint -d "{extends: relaxed, rules: {document-start: disable, line-length: disable, new-line-at-end-of-file: disable}}" <file>.yaml
packages.x86_64 syntax: one package per line, [a-zA-Z0-9._@+\-]+ only, no trailing
whitespace.
A pre-commit hook (githooks/pre-commit) runs the same ShellCheck pass on every
commit, so CI failures surface before you push. Enable it once per clone:
git config core.hooksPath githooks
It skips linting (with a warning) when shellcheck is missing rather than blocking the
commit; CI remains the enforcer. Bypass it with git commit --no-verify.
Gotchas
- The profile root is the repo root (not a
releng/subdirectory). CI builds withmkarchiso -v -w "$PWD/work" -o "$PWD/out" -- "$PWD"(.gitea/workflows/build-iso.yaml), and the local build command above does the same — never pass arelengpath. customize_airootfs.shclones git repos (oh-my-zsh, lazy.nvim, plugins, end-4 dots) into/etc/skeland/optduring the ISO build. These require network access at build time but make the live ISO work offline.customize_airootfs.shruns./setup install-deps -f -s --skip-plasmaintgas the throwaway userii-builder(end-4's scripts refuse to run as root) and aborts the build if it fails.--skip-plasmaintgis deliberate: that optional Firefox widget pulls 410 packages / 526 MiB of KDE.- The
[local-aur]repo path inpacman.conf(file:///home/alvin/AlvinArch/localrepo) is for local builds only. The CI does not use this path. - The image ships its own
[local-aur]atfile:///opt/localrepo(see.live-pacman.conf). It holdslocalrepo/(copied by CI/update.sh) plus everythinginstall-depsbuilt. pacstrap runs without-P, so the target keeps the stock/etc/pacman.conf;alvin-installstrips[local-aur]from it anyway as a safety net, because/opt/localrepoexists only on the medium. alvin-installruns end-4 with--skip-alldeps— dependencies must come from the ISO. It feedsyes ito end-4's failure prompt: inside the installer chroot the medium's/runis bind-mounted in, so end-4'ssystemctl --machine=…@.host --user enable … --nowlines can never work (and--nowwould start services on the live medium).install_end4_dotsre-applies the enable-only part (bluetooth, ydotool, i2c module) outside the chroot and reports every step end-4 had to skip. If the end-4 step reports "incomplete", check thathyprctl/quickshell/the venv were resolvable (harvest) before re-running anything./etc/skel/.cache/uv+/etc/skel/.local/share/uv/pythonare pre-warmed by the build (~460 MB: CPython 3.12 and the pinned wheel cache) so end-4'suv venv/uv pip installat install time needs no network.- end4-pC (
pctrade/end4-pC) is the default quickshell config: cloned into/etc/skel/.config/quickshell/end4-pCat build time, selected by patchinghl.env("qsConfig", "ii")→"end4-pC"in the staged/opt/dots-hyprland/dots/.config/hypr/hyprland/variables.lua(there is nohypr/hyprland/in this repo —./setup installcopies it to the installed system).alvin-installre-copies end4-pC into the user's home after./setup install, because end-4's installerrsync --deletes all of~/.config/quickshell. airootfs/etc/skel/.config/illogical-impulse/presets/carries the user's presets (Laptop.json,Personal-Performance.json,Personal-Performance.zip). They embed/home/alvin/...paths, so wallpapers/screenshots need fixing up after install.- End-4 metapackages are built from
localrepo/end4-pkgbuilds/(quickshell-git, bibata-modern-classic-bin, microtex-git). - quickshell pin / Qt 6.12: end-4 pins quickshell to
41651d7(Sep 25), which predates upstream fix5d5d498(Oct 3, "use MOC includes instead of opaque pointer definitions — … Qt 6.12 compatibility"). Arch shipped qt6 6.12.0 on Oct 8 and the old pin died withMeta Types must be fully definedin the moc build of quickshell-hyprland-ipc (failed CI run 137).customize_airootfs.shtherefore rewrites end-4's_committo11ca60beafter cloning — but only while it still matches^_commit='41651d7, so once end-4 bumps their own pin the guard goes inert and theirs wins.localrepo/check-quickshell-pin.sh(weeklypin-check.yamljob) fails CI if either pin goes stale or loses the fix; it warns instead of failing when end-4's known-stale pin is covered by the override. - MicroTeX flags: the image's
makepkg.confis CachyOS's (-march=native -O3, LTO). GCC 16.2 hit an internal compiler error (segfault) on MicroTeX'sgtkmm_main.cppwith it (failed CI run 155; not reproducible locally), socustomize_airootfs.shpatches end-4'sillogical-impulse-microtex-gitPKGBUILD tooptions=(!lto)+-O2(ourlocalrepo/end4-pkgbuilds/microtex-gitcarries the same). - CAP_SETFCAP on the runner: without it pacstrap drops
security.capabilityxattrs (newuidmap/newgidmap, snap-confine, dumpcap, kwin_wayland, ...) from the live ISO with only a warning.build-iso.yamlfails the job early if the runner lacks it — fix the runner, don't remove the check. - yay SIGSEGV on exit: yay 13 (libalpm via purego/
dyalpm) can segfault inalpm_release()after a finished transaction (failed CI run at end-4'sillogical-impulse-kdestep), which abortedinstall-deps.customize_airootfs.shputs a temporaryyayshim first on the builder'sPATH: a failed plainyay -S <pkgs>counts as success only ifpacman -Tconfirms every target is installed (it logs[yay-shim]). Every other failure keeps yay's exit code. - gvfs / dead
~/.gvfs:install_end4_dotsreplays end-4's gsettings writes viadbus-run-sessionas the target user. A session bus D-Bus-activates gvfsd → gvfsd-fuse mounts~/.gvfsand dies with the bus, leaving a dead FUSE mount that root can't stat, so the home's catch-allchown -Rfailed (cannot access '/mnt/home/<user>/.gvfs': Permission denied) and aborted the install before the bootloader. The replay therefore runs withGIO_USE_VFS=local GVFS_DISABLE_FUSE=1, and the function unmounts anything mounted under the home before that chown — keep both. - mono's
cert-syncruns beforeca-certificates.crtexists during pacstrap, socustomize_airootfs.sh(live) andinstall_pacstrap(target) re-run it. /etc/skel/.zshrcis owned bygrml-zsh-config, so ours is staged asairootfs/root/.zshrc-skeland installed bycustomize_airootfs.shafter pacstrap — never add files underairootfs/at paths a package owns, or pacstrap aborts with "exists in filesystem".packages.x86_64is sorted alphabetically. Keep it sorted when adding/removing entries.airootfs/opt/localrepo/is a build artifact (gitignored) — never commit it.- Build stamp flow:
profiledef.sh(sourced by mkarchiso) stages the gitignoredairootfs/root/.iso-build-stampwhen a.gitdir exists orCI/GITEA_ACTIONSis set — line readsAlvinArch build <date> | commit <date> | <4-char hash>(falls back tocommit unknownifgit -c safe.directorycan't read the repo; without.git/CI no file is staged).customize_airootfs.shkeeps it as/etc/alvin-build-stampon the live medium and appends it to the live/etc/motd, so the booted ISO identifies its build. The installed system's motd is never touched by us —alvin-installhas no motd step (an earlierinstall_motdthat copied the banner + stamp into the target was removed on purpose).