<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-08-20T13:05:38+00:00</updated>
<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>
<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>docs: fix broken README anchors and normalize table formatting</title>
<updated>2026-07-21T12:18:54+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-21T12:18:54+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=d9594265b6706cba2b9655f1a2bd54c782f2841b'/>
<id>urn:sha1:d9594265b6706cba2b9655f1a2bd54c782f2841b</id>
<content type='text'>
</content>
</entry>
<entry>
<title>chore: extend gitignore for local task tooling</title>
<updated>2026-07-21T11:33:14+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-21T11:33:14+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=91fa90d2ea8931675742d6c59a8cce2ea310e169'/>
<id>urn:sha1:91fa90d2ea8931675742d6c59a8cce2ea310e169</id>
<content type='text'>
</content>
</entry>
</feed>
