<feed xmlns='http://www.w3.org/2005/Atom'>
<title>archangel, 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-09-13T15:59:31+00:00</updated>
<entry>
<title>fix(test): keep each failed install attempt's log</title>
<updated>2026-09-13T15:59:31+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-09-13T15:59:31+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=0adcfaa5c7e479e9c433b92836a5548a19168e05'/>
<id>urn:sha1:0adcfaa5c7e479e9c433b92836a5548a19168e05</id>
<content type='text'>
The retry loop refetched the guest's newest install log after every failed attempt, so `test-logs/` only ever held the last one. When a retry followed a pacstrap stall, the saved log showed the retry's "Disk in use" and the stall behind it was gone. Each failed attempt's log now lands in `test-logs/&lt;config&gt;-install-attempt&lt;N&gt;.log`.
</content>
</entry>
<entry>
<title>fix(install): run the failure cleanup when an install step fails</title>
<updated>2026-09-13T15:59:31+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-09-13T15:59:31+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=6e99a7fbae089009e7948cb3853b68364f2f527b'/>
<id>urn:sha1:6e99a7fbae089009e7948cb3853b68364f2f527b</id>
<content type='text'>
Every install step runs inside a function, and bash never delivers an ERR trap to a command failing inside a called function. With `set -e` the installer exited without `install_failure_cleanup` ever running. That left /mnt mounted and the pool imported, so a re-run or the test harness's retry died at "Disk in use". The `|| error` shape had the same problem, because `error()` calls a plain `exit 1` that no ERR trap sees.

The failure trap now also fires on EXIT, and `arm_failure_trap` and `disarm_failure_trap` set and clear it in one place. I left errtrace off on purpose. It hands the trap to command substitutions, so a masked failing `$(...)` would run the cleanup in a subshell and unmount /mnt while the install carried on.

Once the cleanup runs, three more things could stop it from finishing. A SIGTERM to the whole process group also kills the logging tee, and the cleanup then died on SIGPIPE at its first message. It now ignores SIGPIPE and turns errexit off for itself with `local -`. It also ignores INT and TERM, so a second Ctrl-C can't cut it short. The pool export could race pacman's still-exiting chroot processes and fail as busy, so the export and the Btrfs LUKS close now retry for up to 15 seconds. The Btrfs branch also gets the recursive unmount with a lazy fallback that the ZFS branch already had.
</content>
</entry>
<entry>
<title>docs: check Secure Boot before blaming a lost EFI boot entry</title>
<updated>2026-09-13T12:00:06+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-09-13T12:00:06+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=3bab9f1ff287c61e85504ad5ab0c5a0d69bf0a61'/>
<id>urn:sha1:3bab9f1ff287c61e85504ad5ab0c5a0d69bf0a61</id>
<content type='text'>
A firmware update can re-enable Secure Boot without clearing NVRAM. The firmware then rejects the unsigned ZFSBootMenu or GRUB loader as "no bootable drive" rather than a security violation, which looks like a missing boot entry. A Framework BIOS update did this to an installed machine this week. I put a check-Secure-Boot-first bullet at the top of the troubleshooting section, with shim's MOK screen on the Ventoy stick as the tell and the firmware fix.
</content>
</entry>
<entry>
<title>fix(build): say so when the ISO ships without archsetup</title>
<updated>2026-08-20T13:05:38+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-20T13:05:38+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=6ed8697ae38e30bd7990553d26ea6bb4ef4d3e46'/>
<id>urn:sha1:6ed8697ae38e30bd7990553d26ea6bb4ef4d3e46</id>
<content type='text'>
The 2026-08-02 build ran without ARCHSETUP_DIR set and produced a bare ISO with no complaint. A recovery image missing its setup tooling only reveals that on the machine you're recovering, which is the worst place to find out.

The build now warns when ARCHSETUP_DIR is unset or points at a non-directory, and the closing summary reports baked or not baked either way. Both go to the build log, since mkarchiso's output buries anything printed mid-run.

The remediation hint uses sudo's VAR=value form because build.sh needs root and env_reset drops an exported variable. It uses $HOME rather than a tilde, which zsh leaves unexpanded in an assignment.
</content>
</entry>
<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(build): keep private files out of the ISO, ship machine profiles</title>
<updated>2026-08-14T11:22:12+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-14T11:22:12+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=4186fcf3f75831b28333dbfb0814318ec88dd0d0'/>
<id>urn:sha1:4186fcf3f75831b28333dbfb0814318ec88dd0d0</id>
<content type='text'>
rsync -a copies by disk presence, so untracked personal-tooling state in the archsetup checkout rode into a portable USB image. The archsetup copy now excludes it.

The gitignored machine profiles (installer/*.conf) now ship to /root/ on the live ISO, so an unattended bare-metal reinstall has its profile without a repo checkout. The copy lists what shipped and warns when no profile exists. It also warns when a profile carries a non-placeholder credential, since the ISO is an unencrypted portable image.
</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>
</feed>
