<feed xmlns='http://www.w3.org/2005/Atom'>
<title>archangel/tests/unit, 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(test): clear the cached archzfs repo before the first scenario</title>
<updated>2026-08-07T18:41:10+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-07T18:41:10+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=5b48bef697a6c98de9b1336ad5a36ad7341a639d'/>
<id>urn:sha1:5b48bef697a6c98de9b1336ad5a36ad7341a639d</id>
<content type='text'>
archzfs re-uploads its GitHub Releases assets under the same filenames, so pacoloco can hold package bodies that no longer match what its archzfs.db advertises. Every ZFS scenario then dies at pacstrap with "invalid or corrupted package", which reads exactly like an installer regression. It went stale twice on 2026-08-06, the second time mid-run: fresh at scenario one, rotten by scenario six, and the ZFS half of the suite was lost.

The whole directory goes, not the zfs-dkms and zfs-utils globs build.sh uses. Removing the bodies while leaving a stale db trades a checksum error for "Maximum file size exceeded", which is how the second failure passed itself off as a new bug.

Removal needs root, so it degrades to a warning rather than failing the run. The path is injectable because the first version hardcoded it, and running the unit suite then invoked sudo rm -rf against the live cache. A shape check guards the override, since this is rm -rf under sudo and a mistyped path should cost a warning instead of the machine.
</content>
</entry>
<entry>
<title>fix(test): reboot the guest after a live-root rollback</title>
<updated>2026-08-07T07:40:29+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-07T07:40:29+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=14c08b4706a486ec080de606ccf1a7c2acd672b6'/>
<id>urn:sha1:14c08b4706a486ec080de606ccf1a7c2acd672b6</id>
<content type='text'>
verify_zfssnapshot_wrapper rolled back zroot/ROOT/default while it was mounted and running, then immediately read the result and destroyed the snapshot. ZFS doesn't support rolling back a mounted root, and both follow-ups were unsafe. The page and dentry caches kept serving the pre-rollback view, so the sentinel check failed on four scenarios and passed on two identical ones. The cleanup destroy was worse: it blocked in cv_wait_common behind an uninterruptible txg_quiesce and wedged the pool, taking a whole run with it.

The check reboots the guest first now. A reboot exports and reimports the pool, which clears the fragile state and makes the on-disk contents readable. I pulled boot_from_disk and config_encrypt_flag out of run_test's inline boot block, so the mid-test and end-of-install paths share one implementation.

ssh_cmd is bounded by SSH_CMD_TIMEOUT, default 120s. ConnectTimeout only bounds the connection, so that wedged guest held a healthy session open and hung the run for 40 minutes. The installer call raises the bound to INSTALL_TIMEOUT, since it legitimately runs for many minutes.

send_zfs_passphrase sends once. Embedding the pool key left a single prompt, so the second send was typing the passphrase at a login prompt.
</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(test): stop one failed scenario from failing every scenario after it</title>
<updated>2026-08-03T03:20:55+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-03T03:20:55+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=1da64394f637347414ffc4954daf9c72d180e2d0'/>
<id>urn:sha1:1da64394f637347414ffc4954daf9c72d180e2d0</id>
<content type='text'>
run_test exported INSTALLED_PASSWORD after the reboot step but cleared it only on the success path. Any failure after that point leaked the installed system's password into the next scenario, where ssh_cmd offered it to the live ISO and every call failed instantly. Six ZFS scenarios died that way behind one flaky check. I made it a local, so bash clears it on every return path.

Install logs were written after stop_vm, so the fetch always reached a stopped guest. Every *-install.log this harness produced was empty, which is why the April mirror failure went 96 days undiagnosed. I write the captured log first now, then stop the VM.

verify_rollback rolls back the mounted root underneath the running system. The wrapper check that ran next inherited the damage and failed at random. I put the fatal check first on a clean guest and left the destabilising one last, where it can only warn. Rebooting between them is the real fix. It needs the encrypted-pool passphrase re-sent through the QEMU monitor, so it stays filed.

Both installer pushes in run_install are checked now. An unreported failure left the guest with no installer. The run then died at exit 127 with no output, which reads exactly like a real regression.
</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>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>fix(build): drop sanoid from the baked AUR set</title>
<updated>2026-06-17T05:13:15+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-17T05:13:15+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=448a40651e71f8be4dd6a7b3b03bb233e6894eec'/>
<id>urn:sha1:448a40651e71f8be4dd6a7b3b03bb233e6894eec</id>
<content type='text'>
sanoid depends on perl-config-inifiles, which is AUR-only. makepkg -s can't resolve it from the official repos, so the build aborts before it produces an ISO. The 2026-06-09 dependency gate checked AUR-RPC existence rather than the official sync dbs, so it wrongly classified perl-config-inifiles as official. A full build caught it.

sanoid joins paru and mkinitcpio-firmware as AUR-of-AUR packages deferred to the vNext dependency-resolution work. The v1 baked set is now eight packages. Updated the tests and README to match.
</content>
</entry>
</feed>
