<feed xmlns='http://www.w3.org/2005/Atom'>
<title>archangel/installer, branch main</title>
<subtitle>Arch Linux installer ISO — ZFS-on-root or BTRFS, doubles as rescue disk
</subtitle>
<id>https://git.cjennings.net/archangel/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/archangel/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/'/>
<updated>2026-08-14T16:51:27+00:00</updated>
<entry>
<title>fix(install): refuse out-of-range passphrases before the disk is wiped</title>
<updated>2026-08-14T16:51:27+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-14T16:51:27+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=e9f82d19c75ac8602f5840794d6bd62e30576c30'/>
<id>urn:sha1:e9f82d19c75ac8602f5840794d6bd62e30576c30</id>
<content type='text'>
The unattended path only checked that a passphrase was non-empty, while zpool create enforces 8-512 characters, so a short passphrase failed after partitioning had already destroyed the old pool. The velox reinstall hit exactly that: its profile shipped a 7-char placeholder, and run 1 died post-wipe. validate_encryption_passphrase now takes min/max bounds. ZFS gets 8-512 pre-flight, and LUKS gets the same 8 minimum the interactive prompt enforces.

Two adjacent gaps close with it: SWAP_SIZE now rejects zero sizes, which previously passed validation and died at sgdisk after the wipe. validate_config warns when the swap partition lands next to an encrypted root, since a hibernate image is a full RAM dump with keys included. The tracked example profiles' 7-char placeholders are now 8 characters.
</content>
</entry>
<entry>
<title>feat(install): optional SWAP_SIZE carves a swap partition for hibernate</title>
<updated>2026-08-14T14:59:55+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-14T14:59:55+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=1a900e50433b0fcd3a192eed1c7989233a6928cf'/>
<id>urn:sha1:1a900e50433b0fcd3a192eed1c7989233a6928cf</id>
<content type='text'>
Hibernate needs a resume target outside the pool (swap-on-zvol deadlocks, a long-standing OpenZFS issue), and that's only decidable at partition time. SWAP_SIZE=100G in a config file carves partition 3 (type 8200) physically between EFI and root. Root still takes the remainder and the EFI=1/ROOT=2 numbering contract holds. validate_config rejects malformed sizes and multi-disk layouts. get_swap_partition handles nvme vs sata naming.

This only partitions: formatting, encryption, and the resume chain stay manual for now. The change ran the real velox reinstall on 2026-08-13 (EFI=512M, SWAP=100G, ROOT=remainder).
</content>
</entry>
<entry>
<title>fix(install): stop encrypted ZFS boots asking for the passphrase twice</title>
<updated>2026-08-03T04:12:39+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-03T04:12:39+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=72dc6c49636aefd12f707ffc3eeb244744133cae'/>
<id>urn:sha1:72dc6c49636aefd12f707ffc3eeb244744133cae</id>
<content type='text'>
ZFSBootMenu unlocks the pool to read the kernel and initramfs, then kexecs into it. The loaded key doesn't survive kexec. The booted initramfs re-imports the pool, finds keylocation=prompt, and asks for the same passphrase again.

I write the passphrase to /etc/zfs/zroot.key inside the encrypted root, point the encryption root at it, and bake it into the initramfs. ZFSBootMenu can't read a file in a dataset it hasn't unlocked, so it overrides the file:// URI and prompts once. The booted initramfs then loads the key silently.

Nothing weakens at rest. Both the keyfile and the initramfs live inside the encrypted dataset, which only holds because ZFSBootMenu keeps the initramfs inside the boot environment rather than on the ESP.

keyformat stays passphrase, since that's what lets ZFSBootMenu accept the typed value. keylocation alone is settable with zfs set, so this never reaches for zfs change-key and never rekeys the pool mid-install.

The Btrfs path already did this for LUKS, so I reused its ensure_initramfs_files helper.
</content>
</entry>
<entry>
<title>fix(installer): scope AUR list to filesystem, keep pacman.conf 0644</title>
<updated>2026-06-28T02:08:28+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-28T02:08:28+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=2ead67458add6accda226cca3e21592162d05bfd'/>
<id>urn:sha1:2ead67458add6accda226cca3e21592162d05bfd</id>
<content type='text'>
The baked AUR set installed unconditionally, so zfs-auto-snapshot reached every target. On a btrfs install there's no zfs to satisfy its dependency, and pacstrap aborted the whole transaction. The ISO still bakes the full set. install_base now filters the manifest names through filter_aur_for_fs, dropping zfs-only tooling (zfs-auto-snapshot, zrepl) on a non-zfs target.

strip_repo_stanza mv'd a 0600 mktemp file onto the target, so a clean install shipped /etc/pacman.conf root-only and every user-level makepkg/yay failed to read it. It now truncate-writes through the existing file, preserving the pristine 0644.

Tested in test_common.bats.
</content>
</entry>
<entry>
<title>fix(installer): refresh package databases before pacstrap</title>
<updated>2026-06-25T17:05:56+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-25T17:05:56+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=89691a0dc9eb7dcbf8583797d7cdc34aecec0d56'/>
<id>urn:sha1:89691a0dc9eb7dcbf8583797d7cdc34aecec0d56</id>
<content type='text'>
A ZFS-root install from an aged ISO died in install_base: dkms couldn't build zfs/2.3.3 against a current linux-lts. The ISO bakes the archzfs sync db at build time, so as the ISO ages that db keeps pinning an older zfs-dkms while linux-lts is pulled current from the live mirror. The DKMS module then fails to compile against the newer kernel.

Run pacman -Syy after the archzfs and baked-AUR repos are added to the live pacman.conf and before pacstrap -K, so pacstrap resolves the current zfs-dkms instead of the stale baked one. A plain -y can be skipped by pacman's freshness check against the GitHub-served archzfs db (no reliable timestamps), so -yy forces the refresh.
</content>
</entry>
<entry>
<title>refactor(installer): extract parse_btrfs_subvol_opts helper</title>
<updated>2026-06-24T01:05:17+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-24T01:05:17+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=73e66b703258270a3d688a51dd90ed2f24401568'/>
<id>urn:sha1:73e66b703258270a3d688a51dd90ed2f24401568</id>
<content type='text'>
mount_btrfs_subvolumes and generate_btrfs_fstab each carried an identical block that composed a subvolume's mount options from BTRFS_OPTS plus the per-subvol extra flags. The two could drift out of sync. Extracted the logic into parse_btrfs_subvol_opts (pure string transform), preserving the exact behavior, and called it from both. Added bats cases covering the default, compress=no, nodatacow, nosuid, and combined paths.
</content>
</entry>
<entry>
<title>fix(installer): RAID validation, set -e fix, drop dead shadow branch</title>
<updated>2026-06-24T00:55:07+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-24T00:55:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=edb5016809f3bc657283d8c2402970dbbab3c5cf'/>
<id>urn:sha1:edb5016809f3bc657283d8c2402970dbbab3c5cf</id>
<content type='text'>
Two installer cleanups from the todo backlog.

validate_config now rejects a RAID_LEVEL the selected disk count can't support, guarding the unattended path (the interactive path already constrains the choice). While adding it I found a latent bug: the error loop's ((errors++)) returned 0 on the first error and tripped set -e in the monolith's `[[ UNATTENDED == true ]] &amp;&amp; validate_config` call, aborting after one warning instead of listing every problem. Switched to pre-increment so the count accumulates as designed. Added four bats cases, including one that runs validate_config under set -e outside bats' run shield.

build.sh dropped the dead shadow-file rebuild else-branch. The profile is always copied fresh from releng (which ships /etc/shadow), so the branch never ran, and its hardcoded account list had drifted from what releng provides. Replaced with an assertion that fails the build loudly if the file is ever missing.
</content>
</entry>
<entry>
<title>feat(install): install baked AUR packages and clean the target config</title>
<updated>2026-06-10T04:45:00+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-10T04:45:00+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=4e6f4cc66206f02e92d4a2ca2f414fad5a3439a1'/>
<id>urn:sha1:4e6f4cc66206f02e92d4a2ca2f414fad5a3439a1</id>
<content type='text'>
Wire the baked AUR repo into the installer. Before pacstrap, install_base checks whether the ISO shipped the repo and, if so, exposes [aur] in the live /etc/pacman.conf and reads the package names from the manifest, adding them to the pacstrap set so they install into the target offline. This mirrors the existing [archzfs] handling. pacstrap resolves repos from the live system, not $MNTPOINT.

The live config already carries [aur] from the shipped ISO config, so the append is idempotent by design. A --skip-aur ISO ships no repo, and aur_repo_available gates the whole path, so the installer still works there.

configure_system strips any [aur] stanza from the target /etc/pacman.conf. pacstrap installs a stock target config with no [aur], so this is defensive, but it guarantees the installed system never references /usr/share/aur-packages, which exists only on the live ISO.

Four new common.sh helpers carry the logic: aur_repo_available, append_aur_repo (idempotent), aur_manifest_names (the manifest is the source of what to install, so the list never drifts), and strip_repo_stanza. All four covered across Normal, Boundary, and Error.
</content>
</entry>
<entry>
<title>refactor: drop the dead duplicate disk_in_use from common.sh</title>
<updated>2026-05-23T14:40:30+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-23T14:40:30+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=c82761fe3352294ef644a9b614cc2677f8f2e339'/>
<id>urn:sha1:c82761fe3352294ef644a9b614cc2677f8f2e339</id>
<content type='text'>
common.sh and disk.sh both defined disk_in_use. archangel sources common.sh first, then disk.sh, so disk.sh's thorough version (mount, active swap, imported zpool, md array) won at runtime everywhere — including list_available_disks, the common.sh function that calls it. common.sh's older mount-and-holders version was dead.

I deleted it. list_available_disks now resolves disk_in_use to disk.sh's, which is what already happened at runtime. The disk.sh unit tests cover the surviving version. Suite stays at 245, lint clean.
</content>
</entry>
<entry>
<title>feat(install): add pre-flight environment and disk-target validation</title>
<updated>2026-05-22T23:03:40+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-22T23:03:40+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=b6525a50fabf3aedf41eee70c164519b00d27704'/>
<id>urn:sha1:b6525a50fabf3aedf41eee70c164519b00d27704</id>
<content type='text'>
archangel went straight from filesystem selection into a destructive install behind only a root check and a ZFS module load. A missing tool, a BIOS boot, a too-small or in-use disk, or a dead network surfaced as a confusing abort partway through, sometimes after partitioning had already run.

Two gates now fail fast. validate_environment runs after filesystem selection, before any disk is touched: it confirms UEFI boot mode and that every required command is present, with the list coming from a new required_commands helper built like pacstrap_packages. validate_install_targets runs after disk selection, before the first wipe: it refuses a target that's mounted, holds active swap, or belongs to an imported pool or md array, rejects disks under 20 GB, and confirms a mirror is reachable via DNS plus a TCP probe (no ICMP, since some networks drop it).

I folded the install_failure_cleanup hardening into the same change. It now falls back to lazy unmounts, so a pacstrap-interrupted target with busy bind mounts still releases the pool and unmounts the EFI partition. Without that, the disk-in-use guard would block the very retry the cleanup exists to enable. "Re-run to retry" only holds if the disk is genuinely freed first.

The 20 GB floor is decimal on purpose. It reads as the natural minimum and clears a 20 GiB disk image with headroom instead of sitting on the boundary.
</content>
</entry>
</feed>
