<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-06-28T02:08:28+00:00</updated>
<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>
<entry>
<title>refactor: extract validate_encryption_passphrase from gather_input</title>
<updated>2026-05-19T17:30:07+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-19T17:30:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=9405b1fc9984e43b0297d2bb89dea1666e1f4853'/>
<id>urn:sha1:9405b1fc9984e43b0297d2bb89dea1666e1f4853</id>
<content type='text'>
gather_input's unattended branch had two parallel if-blocks, one for ZFS and one for Btrfs, each doing the same encryption-passphrase empty check against a filesystem-specific variable (ZFS_PASSPHRASE or LUKS_PASSPHRASE). The two blocks shared the condition surface and error template. Only the variable name differed.

I lifted the check into validate_encryption_passphrase in lib/config.sh next to validate_filesystem. The helper takes the variable name and uses indirect expansion (${!var_name}) so one function covers both filesystems. gather_input now dispatches via if/elif on FILESYSTEM and calls the helper with the right variable, collapsing 14 lines to 6.

The original tests in test_archangel.bats (gather_input errors when ZFS without ZFS_PASSPHRASE / when Btrfs without LUKS_PASSPHRASE / accepts ZFS with NO_ENCRYPT=yes) still pass, exercising the helper through the dispatch. Added 4 direct unit tests in test_config.bats covering the four cases: NO_ENCRYPT=yes passes regardless, NO_ENCRYPT=no with empty fails, NO_ENCRYPT=no with value passes, and the error message names the offending variable. Bats: 177 → 181.

No behavior change. The helper preserves the original error message format and exit conditions.
</content>
</entry>
<entry>
<title>refactor: lift FILES= keyfile sed to ensure_initramfs_files helper</title>
<updated>2026-05-19T17:24:43+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-19T17:24:43+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=e49a95254d439e5e83c05756a3bc92e4575360b0'/>
<id>urn:sha1:e49a95254d439e5e83c05756a3bc92e4575360b0</id>
<content type='text'>
btrfs.sh's configure_btrfs_initramfs had a six-line inline that ensured mkinitcpio.conf's FILES= line listed the LUKS keyfile: sed-replace the existing FILES= line, then grep + append as a fallback when no FILES= line existed. The pattern is mkinitcpio-specific and self-healing rather than error-on-miss (FILES= is optional in mkinitcpio.conf, so missing means "no extra files," not a broken config).

I lifted the block into ensure_initramfs_files in lib/common.sh next to prepend_grub_cmdline_linux, then collapsed the btrfs.sh call site to a single ensure_initramfs_files line. Added three bats tests for the three cases (FILES= present and empty, FILES= present with a different value, FILES= absent entirely). Bats: 174 → 177.

No behavior change. The helper's logic matches the inline byte-for-byte: same sed pattern, same grep fallback, same final state.
</content>
</entry>
<entry>
<title>refactor: wire validate_config into the unattended install path</title>
<updated>2026-05-19T07:19:04+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-19T07:19:04+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archangel/commit/?id=b6d737fc9e3cee641afe7b855c5356122537850b'/>
<id>urn:sha1:b6d737fc9e3cee641afe7b855c5356122537850b</id>
<content type='text'>
validate_config in lib/config.sh was unreachable from main(). Its empty-field checks duplicated four lines in gather_input's unattended branch. validate_config also has two checks gather_input doesn't: that every entry in SELECTED_DISKS is a real block device, and that TIMEZONE exists under /usr/share/zoneinfo. Neither check ever ran. A config with a typo'd disk path slipped past gather_input and surfaced as an obscure sgdisk error inside the destructive partitioning step.

I wired validate_config into main() after validate_filesystem, gated on UNATTENDED so it only runs against an already-loaded config. I dropped the four duplicate empty-field checks from gather_input's unattended branch. The filesystem-specific passphrase checks stay there because they're coupled to the FILESYSTEM branch logic.

validate_config reports every missing field at once instead of dying on the first. A config with five missing fields tells you all five in one pass.

I removed the four corresponding gather_input bats tests. validate_config's existing unit tests in test_config.bats already cover their assertions. Bats: 178 → 174.
</content>
</entry>
</feed>
