Skip to content

Install the target from a btrfs root image instead of pacstrapping it - #113

Open
hegjon wants to merge 69 commits into
omacom:quattrofrom
hegjon:btrfs-image-install
Open

hegjon wants to merge 69 commits into
omacom:quattrofrom
hegjon:btrfs-image-install

Conversation

@hegjon

@hegjon hegjon commented Aug 21, 2026 •

Copy link
Copy Markdown

Summary

The target install is the same ~940 packages for every machine; only the kernel, CPU microcode, audio firmware and Tailscale differ per host. pacman extracts that set single-threaded at ~40 packages/s — about 34 s of the install on any machine with four or more cores, and nothing parallelises it (a CPU sweep from 1 to 32 vCPUs is flat from 4 upward; merging the seven pacstrap calls into three changed nothing).

This PR builds that invariant set once, at ISO build time, into a btrfs subvolume and ships it as a btrfs send --compressed-data stream. The installer receives it onto the target right after archinstall mounts the layout, swaps it in as @, and finishes with a small per-machine pacstrap. btrfs receive stores the pre-compressed extents as they arrive, so the unpack costs ~17 s regardless of CPU count or compression level.

Results

Same 16-vCPU QEMU VM, same harness, back-to-back:

quattro ISO inital branch + outer zstd (8329635, 2026-08-28)
Install (dashboard time) 0m 43s 0m 24s 0m 24s
Package phase 35.7 s 18.3 s 18.3 s
Packages installed 942 942 (identical set) 942
Installed system on disk 5.73 GiB 3.98 GiB 3.98 GiB
Root image stream — 3.75 GB 3.34 GB (−11.0%)
ISO 6.2 GB 6.96 GB 6.84

On a Ryzen 9 9950X workstation the full interactive install went from 53 s to 36 s.

Update 2026-08-28 (8329635 — outer zstd layer on the stream): the send stream now ships as omarchy-root.btrfs.zst (zstd -15 --long=27 -T0 over the whole stream, ~28 s of build time). The per-extent zstd:15 inside the stream can't see past a 128 KiB extent, so a whole-stream pass still reclaims 11% — mostly send framing and redundancy that only exists across extents. The install path keeps its shape: the live system decompresses in the receive pipe (zstd -dc | btrfs receive, far faster than any install medium — on real USB the install reads 415 MB less and gets faster) and the extents land on disk unchanged, so the installed system is byte-identical. The boot-time verify hashes the smaller file and its size-based timeout follows automatically. Measured before shipping: the compression level is nearly irrelevant (−10.3% at --fast=3, −11.5% at --ultra -22) — the 128 MiB long-range window is the knob, and --long=27 is exactly what a stock zstd -d accepts; a trained dictionary is useless on one large stream; duperemove found 44.9 KB of duplicate extents in the whole 8.4 GB image, so dedupe is a dead end.

All three install modes pass in omarchy-iso-test: full-disk, --encrypt (LUKS unlock at first boot), and the new --free-space mode (pre-existing ESP + data partition verified intact from inside the installed system).

How it works

Build (builder/build-root-image.sh, called from build-iso.sh)

  • pacstraps the image set (package lists + builder/image.packages, minus the per-machine hardware_packages) into a btrfs loop image mounted compress-force=zstd:15, with the boot-image pacman hooks masked and NoExtract for docs, man pages and non-English locales, then btrfs send --compressed-data it into the airootfs (stored uncompressed in the squashfs like the mirror).
  • The per-machine delta resolves against the image's own pacman db; the dashboard's expected-package count comes from the same resolution.
  • The build cache keeps the full download closure. configs/airootfs/root/customize_airootfs.sh then removes, from the live root's copy of the mirror, every package file the image already provides — 941 of 1250 — while leaving the repo db complete so omarchy-pkg-add's pacman -S --needed over mixed package lists still resolves every name.

Install (orchestrator/phases_impl.py, archinstall_adapter.py)

  • After mount_ordered_layout + sanity_check: mount the filesystem top level, btrfs receive (publishing phase_progress for the dashboard), snapshot writable, unmount the layout, replace archinstall's empty @, replay the mount table from findmnt (so LUKS is never unlocked twice and the configurator-mounted protected layout works unchanged), copy the image's pacman.log into @log, systemd-machine-id-setup.
  • install_base_delta mirrors archinstall 4.4's minimal_installation step for step with the bulk pacstrap reduced to what the image lacks (kernel, microcode). install_applications straps only missing packages (the image carries PipeWire; the mirror no longer does).

Review notes

  • The build container needs loop devices and a btrfs mount (privileged Docker has both; loop nodes are created with mknod since Docker fills /dev once). Not yet exercised in the nightly workflow.
  • customize_airootfs.sh is deprecated in archiso but supported by the pinned submodule (v87).
  • The image ships one pacman-key --init keyring per build, like the official archlinux Docker image; re-initialising per machine would cost several seconds of --populate.
  • NoExtract and compress-force apply to the image only. The installed system runs Omarchy's own pacman.conf and mounts compress=zstd, so upgrades bring docs/man/translations back and new data follows the heuristic — ISO size was the goal, the system may regrow.
  • ISO is 0.76 GB larger than quattro. The remaining 1.9 GB shipped mirror is nvidia (833 MB), three kernels + headers (660 MB) and lib32; the image could shrink another ~0.5–0.8 GB as a squashfs (1 MiB solid blocks, full zstd range) at the same unpack speed — left for a follow-up.
  • omarchy-iso-test --free-space builds its Windows-style fixture without root; unlike omarchy-iso-test-windows-disk it has no EFI/Microsoft directory, so the configurator reports no Windows ESP (same free-space mechanics either way).

Test plan

  • test/all
  • bin/omarchy-iso-test release/*-image.iso --install-only
  • bin/omarchy-iso-test release/*-image.iso --encrypt --install-only
  • bin/omarchy-iso-test release/*-image.iso --free-space --install-only
  • 2026-08-28, on the 8329635 ISO: test/all (the receive tests now drive the real zstd on real compressed fixtures, mutation-checked), unattended cidata install through the new receive pipeline, corrupt-image 8/8 (its fixture now flips a digit of the recorded sha256), and a hands-on interactive install in QEMU booted through to the installed system
  • nightly build in CI
  • full acceptance suite (bin/omarchy-iso-test without --install-only)

🤖 Generated with Claude Code

https://claude.ai/code/session_01P9b7oP8j2GA6sZZ9e8aJYv

Fixes #125 — the finish screen now releases the install target before offering the medium's removal, and reboots without paging from the medium.

hegjon and others added 4 commits August 21, 2026 20:17
The target install is the same ~940 packages for every machine; only the
kernel, CPU microcode, audio firmware and Tailscale differ. pacman extracts
that set single-threaded at ~40 packages/s, about 34s on any machine with
four or more cores, and no amount of parallelism reaches it.

So build-root-image.sh pacstraps the invariant set once at ISO build time
into a btrfs subvolume mounted compress=zstd:3 (the level the installer
mounts the target with) and ships it as a `btrfs send --compressed-data`
stream. The orchestrator receives it at the target filesystem's top level
right after archinstall mounts the layout, snapshots it writable in place
of the empty @ subvolume, replays the mount table, and then lets
archinstall finish with the per-machine delta (install_base_delta mirrors
minimal_installation minus the bulk pacstrap), users and fstab. The
application installers strap only what the target lacks, since the image
carries their package sets and the mirror no longer does.

Measured in the same 16-vCPU VM: the package phase drops from 35.7s to
24.5s and the whole install from 41.8s to 30.6s, with an identical set of
942 packages installed; the installed system boots. The receive itself is
~17s and independent of CPU count.

The offline mirror keeps only what is still pacstrapped at install time:
the live ISO's own packages, the per-machine packages, and the
omarchy-other.packages extras omarchy-apply-system may pull in, resolved
against the offline repo itself so the keep-set can only name files the
mirror holds. The ISO grows from 6.2GB to 9.1GB, the live closure and the
extras now sitting beside the 5.2GB image.

Build details: the container needs loop device nodes made by hand (Docker
fills /dev once, at start), pacman-key's gpg-agent must be stopped before
the image unmounts, and stale copies of locally rebuilt omarchy packages
are evicted from the shared pacman cache so mkarchiso's pacstrap does not
hit a checksum mismatch.

The dashboard gets a phase_progress signal from the unpack so the bar
moves while the local pacman db is still empty, and the live ISO
prefetches the leading bytes of the stream during the wizard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9b7oP8j2GA6sZZ9e8aJYv
btrfs's incompressibility heuristic skips a lot of data in this tree that
zstd compresses fine, and the level only costs build time: btrfs receive
stores the extents as they arrive. Against plain compress=zstd:3 the send
stream goes from 5.2GB to 3.3GB and the installed root from 5.4GB to
4.0GB, at the same ~17s receive time; level 15 adds under two minutes to
the ISO build. btrfs clamps anything above 15.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9b7oP8j2GA6sZZ9e8aJYv
--free-space installs into the unallocated tail of a synthetic
Windows-style disk: a FAT32 ESP and an ext4 data partition with a marker
file, then ~76GiB of free space. The fixture is built without root (parted
on the raw file, mkfs at the partition offsets), so unlike
omarchy-iso-test-windows-disk it carries no EFI/Microsoft directory; the
configurator's free-space mechanics are the same either way. The wizard is
driven through the mode picker and the free-space confirm, and once the
installed system is up the harness checks from inside it that both
pre-existing partitions and the marker survived.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9b7oP8j2GA6sZZ9e8aJYv
mkarchiso pacstraps the live root from the complete mirror at build time,
but at install time only packages the root image lacks can ever be
downloaded from it. The live root's customize_airootfs.sh removes every
package file the image already holds at the same version from its copy of
the mirror, keeping the repo db complete so `pacman -S --needed` over the
hardware scripts' mixed package lists still resolves every name.

The build cache goes back to keeping the whole download closure, so a
rebuild downloads nothing; the shipped selection is decided per build from
the image's local db.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9b7oP8j2GA6sZZ9e8aJYv
@greptile-apps

greptile-apps Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Greptile Summary

The PR replaces bulk target pacstrapping with a prebuilt Btrfs root-image stream and adds the build, verification, installation, release, boot, and test infrastructure needed for that workflow.

  • Builds the invariant package set into a compressed Btrfs stream during ISO construction.
  • Receives and activates that image before installing machine-specific package deltas.
  • Verifies the shipped image before disk modification and expands integration coverage for installation modes, media failures, release handling, and PXE boot.
  • Updates target release handling so failed encrypted-mapper cleanup prevents the removable-medium prompt and forced reboot.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported mapper-close path now returns failure when the mapping remains active or cannot be queried, and the dashboard consequently withholds medium-removal guidance and forced reboot.

Important Files Changed

Filename Overview
builder/build-root-image.sh Builds the reusable Btrfs target image from the invariant package set and emits the stream consumed by installation.
builder/build-iso.sh Integrates root-image construction, package-delta resolution, checksumming, and ISO assembly.
configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py Adds image verification, receive-and-swap installation, mount replay, and per-machine package completion to the target lifecycle.
configs/airootfs/usr/share/omarchy-iso/orchestrator/archinstall_adapter.py Adapts Archinstall’s base-install behavior to configure the received image and install only host-specific deltas.
configs/airootfs/usr/local/bin/omarchy-release-install-target Releases target mounts and crypt mappings, returning failure when a mapper remains active or its state cannot be established.
configs/airootfs/usr/local/bin/omarchy-install-dashboard Gates removable-medium guidance and forced reboot on successful target release.
configs/syslinux/archiso_pxe-linux.cfg Updates PXE entries for direct NBD/NFS-backed operation with the new ISO payload.
test/unit/release-install-target-test.sh Exercises target-release success, busy-mapper, missing-mapper, and indeterminate device-mapper outcomes.
test/unit/dashboard-release-gate-test.sh Confirms failed target release retains the medium warning and selects graceful reboot.

Sequence Diagram

sequenceDiagram
  participant Build as ISO build
  participant ISO as Live ISO
  participant Verify as Image verifier
  participant Installer as Orchestrator
  participant Target as Btrfs target
  participant Dashboard as Dashboard
  Build->>Build: Pacstrap invariant package set
  Build->>ISO: Store Btrfs send stream and checksum
  ISO->>Verify: Verify root-image stream
  Verify-->>Installer: Publish verification result
  Installer->>Target: Prepare and mount layout
  Installer->>Target: "Receive stream and replace empty @"
  Installer->>Target: Install machine-specific package delta
  Installer->>Target: Configure and validate boot
  Dashboard->>Target: Release mounts and encrypted mappings
  Target-->>Dashboard: Report successful release
  Dashboard->>Dashboard: Offer medium removal and reboot
Loading

Reviews (47): Last reviewed commit: "Cover the two things the release's verdi..." | Re-trigger Greptile

@hegjon

hegjon commented Aug 21, 2026 •

Copy link
Copy Markdown
Author

bin/omarchy-iso-test on AMD Ryzen 9 9950X 16-Core Processor

Baseline:

success-installer-15-reboot

@hegjon

hegjon commented Aug 21, 2026

Copy link
Copy Markdown
Author

This branch:

success-installer-15-reboot

@nlgripto

Copy link
Copy Markdown

absolutely mogs #108

@hegjon

hegjon commented Aug 21, 2026

Copy link
Copy Markdown
Author

I did not know about #108, I am curious about the differences

@hegjon

hegjon commented Aug 22, 2026

Copy link
Copy Markdown
Author

Pacman keyring — please decide. This PR ships one pacman-key --init keyring per ISO build (as EndeavourOS offline installs and the archlinux Docker image do); #108 generates one per machine, as Manjaro does. The consequence, verified on an installed system: pacman accepts a package signed with the image's master key under SigLevel = Required, and that key is inside the ISO — so anyone with the ISO can sign packages every install from that build will trust, given a compromised mirror to deliver them. (Fedora isn't comparable: RPM holds public keys only, no machine-side secret.) Per-machine generation costs ~2.4 s and can run in the background. If you want it, it's a contained change to this PR — -G plus dropping etc/pacman.d/gnupg at build, and a backgrounded pacman-key --init && --populate after the mount replay in _install_root_image — which I'll make on request.

@omarchybot

Copy link
Copy Markdown
Collaborator

Reviewed. Read the full diff against quattro, the surrounding orchestrator and configurator code, archinstall 4.4's installer.py, and omarchy's package lists and mkinitcpio drop-in. Ran ./test/all on a disposable VM: all shell unit tests pass, 58 Python tests OK. I did not build an ISO or run an install, so every build-time and install-time claim below comes from reading the source.

First, what I went looking for and did not find. The competing squashfs PR (#108) clobbers /etc/crypttab by unpacking over generated key files, misses LVM in its config guard, and drops brltty/espeakup for a user who boots the accessibility entry. This branch avoids all three. The image replaces the root subvolume rather than unpacking over it, and _install_root_image is ordered before generate_key_files() and before the pre-mounted crypttab writer, so nothing pacman marks backup= can be overwritten. The LVM guard is there. And accessibility survives because archinstall appends brltty/espeakup/alsa-utils to _base_packages in Installer.__init__, which install_base_delta straps for anything the image lacks — alsa-utils is in the image, the other two come from the releng live-ISO list and so survive the mirror prune. The ACL claim is also true here where it is not for squashfs: btrfs send carries xattrs, so system.posix_acl_access, security.capability and user.* all round-trip.

High — the disk is destroyed before the image is validated. arch_install_system calls arch.perform_filesystem_operations(config) (phases_impl.py:216-217), which partitions, formats and LUKS-encrypts, before _install_root_image checks the stream exists (:347), before the btrfs/@ guards (:350-357), and before the LVM guard (archinstall_adapter.py:183-184). Any of those failing leaves the user with a wiped disk and no system — and on the encrypted path, a disk encrypted with a passphrase and no system. All three checks are pure predicates and could run in prepare_install_target, which is already a separate earlier phase that runs before anything destructive.

Worth deciding how far to take it: an existence check does not cover a truncated stream, which for a 7 GB ISO on a badly flashed USB is the likelier failure. btrfs receive does catch that via the stream's per-command CRC32C, but only after the disk is gone. A checksum written at build time and verified in prepare_install_target would close both cases at once.

Medium — the image can be built from a downgraded package. rebuild_offline_repo_db now runs at build-iso.sh:248, over the unpruned mirror, where previously repo-add ran only after prune-offline-mirror.sh (:335) over the resolved file set. That cache is persistent across builds (bin/omarchy-iso-make:152-154 bind-mounts ~/.cache/omarchy/iso_$MIRROR/…), so it holds several versions of the same package — which is precisely what the prune exists to clean up. repo-add replaces a db entry by name with whatever it processes last and only warns on a downgrade (/usr/bin/repo-add:253-260; PREVENT_DOWNGRADE is off by default, and -q hides the warning). The glob at :246 expands lexicographically rather than by version, so foo-1.9-1 sorts after foo-1.10-1 and wins. build-root-image.sh then pacstraps the image from that db at :289. The mirror db is rebuilt correctly after the prune (:339), so the net effect is an image carrying an older package than the mirror beside it advertises. Building the image from the pruned mirror, or feeding repo-add a newest-per-name selection, would settle it.

Medium — a fresh install has no man pages. The image is pacstrapped with NoExtract for usr/share/man/*, usr/share/doc/*, usr/share/info/*, usr/share/gtk-doc/*, usr/share/help/* and all of usr/share/locale/* except en* (build-iso.sh:253-259). man-db is in omarchy-base.packages, so man is installed and man ls finds nothing until coreutils is next upgraded. Same for non-English message catalogs. The PR notes the mechanism as an ISO-size measure but not this consequence, and it reads as the kind of thing that should be the maintainer's call rather than a side effect of a performance change. (/usr/share/i18n is kept, so locale-gen still works, and the configurator hardcodes en_US.UTF-8, so the catalogs only matter to users who change locale later.)

Medium — leftover benchmarking knob. bin/omarchy-iso-boot:155 replaces -smp "$(($(nproc) < 8 ? $(nproc) : 8))" with a hardcoded -smp 16. It landed in 0cefda3, whose message is entirely about zstd level 15 and never mentions it. It oversubscribes the VM on any machine with fewer than 16 cores.

Low:

  • build-iso.sh:414-422 — the resolved-package-count check is only a WARNING, but under this PR the number is image_count + kernel closure + 1, so it is now a direct measure of how many packages the image actually holds — the one signal that would catch a short image. The shipped-mirror selection immediately above it hard-exit 1s on a bad count. Worth the same treatment now that the full runtime package list is no longer installed as a backstop.
  • build-iso.sh:322 — shipped_count=$(grep -c . "$shipped_list") under set -e exits silently on an empty list, so the "the shipped-mirror selection looks wrong: 0 of N" message at :325 can never print for the zero case.
  • build-root-image.sh:114-122 — the hook masking has no "already masked" guard, unlike the orchestrator's _is_devnull_symlink at phases_impl.py:832. If the script dies untrappably, the next run moves the /dev/null symlink over the real backup and the host's hook is permanently masked. Inert for the ISO build (fresh container each time), but the header documents the script as runnable standalone.
  • build-root-image.sh:132-135 — losetup --find and the following losetup "$loop" "$backing" are not atomic; if two builds share a kernel and the attach loses the race, the EXIT trap at :106-108 detaches the other build's device. losetup --find --show makes it atomic.
  • App selections that mean "install nothing" can no longer be honoured: the image carries the PipeWire stack and the strap_missing wrapper (archinstall_adapter.py:294-308) only ever adds. Reachable through the autoinstall user_configuration.json path. Inherent to the image approach and true of Install from a prebuilt rootfs image instead of pacstrapping 925 packages #108 as well, so more a thing to document than to fix.
  • /usr/share/omarchy-iso/omarchy-base.packages is still shipped onto the ISO (build-iso.sh:178) but nothing on the ISO reads it now that _runtime_package_list is gone.

On tests. ./test/all is green, but it exercises none of the new code — nothing covers _install_root_image, install_base_delta, _receive_root_image or customize_airootfs.sh. test/unit/test_provisioning_state.py already unit-tests the near-identical create_factory_snapshot subvolume sequence by asserting on the subprocess calls, so there is a pattern in-repo to follow for the destructive delete/snapshot/rename dance, which is the part of this change with the least margin for error.

Second opinion. codex at xhigh reviewed this independently. The loop-device race, the un-guarded hook masking and the audio-selection gap are its findings, not mine. It reached the same conclusions I had on the format-before-validate ordering and on xattr fidelity — agreement rather than confirmation, since its independence is not currently guaranteed. I rejected its highest-severity claim, that encrypted installs are unbootable because nothing calls installer.mkinitcpio() to persist archinstall's _hooks: the encrypt hook does not come from archinstall at all, but from omarchy-settings' /etc/mkinitcpio.conf.d/omarchy_hooks.conf, which sets the full HOOKS=(… block encrypt filesystems fsck btrfs-overlayfs) array and ships inside the root image, with _root_image_required_packages asserting the package is there before the UKI is built. It also asserted that pacman's local db still lists NoExtracted files as owned, so -Qkk would flag them missing; I could not settle that from the documentation and ran no round-trip, so I am leaving it unverified either way.

What happens next: this is waiting on the maintainer, not on you. #108 implements the same idea with a prebuilt squashfs and only one of the two can land, so the choice between them is his — I have written up an honest comparison for that decision and have deliberately not pushed anything to this branch, since the two substantive findings above are judgement calls about where pre-flight validation belongs and how to make the image build deterministic against the persistent cache. Worth flagging for that decision: the nightly has never built this path, and the loop device plus btrfs mount inside a GitHub Actions container is the least-tested surface here.

hegjon and others added 5 commits August 22, 2026 16:09
arch_install_system partitions, formats and encrypts as its first step,
and only then did _install_root_image check that the stream exists and
that the layout puts the root on a btrfs @ subvolume, with the LVM guard
later still. Any of those failing left a wiped disk (encrypted, on that
path) with no system on it.

All three are predicates on the ISO and the configurator JSON, so run
them in prepare_install_target, the phase before anything destructive.
The protected path checks the real mounts, which exist already.

Existence does not cover a truncated stream, which on a badly flashed
USB is the likelier failure: btrfs receive's per-command checksums catch
that too, but after the disk is gone. build-iso.sh now writes a sha256
next to the stream and the same pre-flight verifies it, which also warms
the page cache for the unpack.

Unit tests cover the pre-flight checks and the subvolume swap in
_install_root_image, asserted on the subprocess sequence the way
create_factory_snapshot already is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdQdQ7YZmBdbvaZNQ2xjKN
The root image was pacstrapped from a repo db built over the unpruned
cache. That cache persists across builds and can hold several versions
of the same package; repo-add keeps whichever file it processes last
(warning on downgrade, hidden by -q) and the glob orders by name, so
foo-1.9 beats foo-1.10. The mirror db was rebuilt correctly after the
prune, leaving an image that could carry an older package than the
mirror beside it advertises.

The resolve/prune/repo-add block does not depend on the image, so run
it first and drop the early repo-add: the image now resolves against
exactly the files this build ships. image.packages joins the download
list so the pruned mirror always holds the image's own packages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdQdQ7YZmBdbvaZNQ2xjKN
0cefda3 hardcoded -smp 16 while benchmarking the image receive and
never mentioned it; it oversubscribes the VM on anything with fewer
cores.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdQdQ7YZmBdbvaZNQ2xjKN
The resolved count is the image's package count plus the kernel closure,
so since the image replaced the full pacstrap it is the one build-time
signal that would catch a short root image; a WARNING that ships no
denominator and lets the build continue is not enough for that. The
shipped-mirror selection right above already exits on a bad count.

Also let the zero case of that selection reach its error message:
grep -c exits 1 on no match, which under set -e killed the script
before the "looks wrong: 0 of N" line could print.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdQdQ7YZmBdbvaZNQ2xjKN
The hook masking moved whatever was at the hook path to .omarchy-backup
and put a /dev/null symlink in its place, with no check for a mask left
behind by a run that died before its cleanup. A second run would then
move the symlink over the real backup and mask the host's hook for
good. Inert for the ISO build (fresh container every time), but the
script documents itself as runnable standalone; skip such hooks the way
the orchestrator's _is_devnull_symlink does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdQdQ7YZmBdbvaZNQ2xjKN
@hegjon
hegjon force-pushed the btrfs-image-install branch from 2b7c014 to c994369 Compare August 22, 2026 14:14
hegjon and others added 3 commits August 22, 2026 16:30
Seed the send stream into mkarchiso's work/iso tree next to airootfs.sfs
instead of the live squashfs: mkarchiso packs that directory as is, with
the boot records intact, and the live system reads the stream straight off
the boot medium at /run/archiso/bootmnt/arch/x86_64/omarchy-root.btrfs.

Measured against the squashfs location, same build cache, back-to-back:
the ISO is byte-for-byte the same size, mkarchiso is 5s quicker (no 3GB
copy into the squashfs), and the install's package phase drops from
18.7s to 16.2s — reading through squashfs costs a copy per 1MiB block even
with no decompression. airootfs.sfs shrinks to the live root, and the
image can be pulled out of the ISO with any ISO9660 tool.

The orchestrator and the wizard-time prefetch look at the ISO path first
and fall back to the squashfs path, so mixed old/new pieces still work.
Builds before this left the stream in the persistent build cache, where it
would ship a second time; the build removes it. The build also logs
timestamps around the image step and mkarchiso.

The pre-flight checksum follows the stream: build-iso.sh writes the
sha256 next to it in the ISO tree, and the orchestrator derives the
checksum path from whichever stream location it finds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdQdQ7YZmBdbvaZNQ2xjKN
The image was pacstrapped with -K, so it carried /etc/pacman.d/gnupg
with a master key (secring.gpg) that every install would share. A
signing key must never be distributed; pacstrap the image with -G and
remove anything a scriptlet might have seeded regardless, as omacom#108 does.

On a target pacstrapped directly, as on quattro, pacstrap -K initialised
a per-machine keyring and the keyring packages' scriptlets populated it
in the same run. Here those packages come from the image, where their
scriptlets ran with no keyring to populate, so the orchestrator does it:
after the last pacstrap (each one runs its own pacman-key --init on the
target), pacman-key --init, idempotent for the key the delta pacstrap
already generated, then --populate archlinux omarchy from the target's
own keyring files. Chroot-free via --gpgdir and --populate-from, and the
gpg daemons are killed on every path so the target can be unmounted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdQdQ7YZmBdbvaZNQ2xjKN
pacman-key --init/--populate took the critical path synchronously and
left a gpg-agent and dirmngr to be killed by name afterwards. Start it
with systemd-run --wait --pipe right after the last pacstrap instead,
and join it in create_factory_snapshot: nothing in between reads the
keyring (the offline repo is SigLevel = Never) or writes it, so the
Limine, user and finalizer phases hide its few seconds, and the snapshot
waits so @factory never captures it half-written.

A unit rather than a detached child: systemd kills the gpg daemons with
the rest of the cgroup the moment pacman-key exits, so no sockets under
the target's gnupg dir survive to block the unmount; the dashboard's
process-group kill does not reach it while systemctl stop still does,
which main() runs on every exit path; and its output lands in the
journal whatever happens to the orchestrator. --wait --pipe give a child
to join with the unit's exit status and output; --collect releases the
name after a failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdQdQ7YZmBdbvaZNQ2xjKN
@hegjon

hegjon commented Aug 22, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough review. Everything below is pushed; the branch is now at f58d9e2 (12 commits).

Findings

High — disk destroyed before validation → 6406d49. All pre-flight checks now run in prepare_install_target, the phase before anything touches the disk: stream present, stream sha256 (written by build-iso.sh next to the stream, verified before partitioning — this also catches the truncated-USB case you raised, and warms the page cache for the unpack), and a layout the image can land on (root on a btrfs @ subvolume, no LVM) checked from the configurator JSON. Protected installs check the real mounts instead. Unit tests cover the checks and the delete/snapshot/rename sequence in _install_root_image, following the create_factory_snapshot pattern you pointed at.

Medium — image built from a downgraded package → 418c840. The resolve → prune → repo-add block now runs before the image build and the early repo-add over the unpruned cache is gone, so the image resolves against exactly the files the ISO ships. image.packages joins the download list so the pruned mirror always holds the image's own packages.

Medium — no man pages → NoExtract dropped entirely (the commit is gone from the branch). Your open question on -Qkk settled in codex's favour: pacman's local db does record NoExtract'd files as owned, so pacman -Qk on the installed system reports them missing. Beyond man pages it also broke the /usr/share/licenses/{xz,systemd} symlinks into /usr/share/doc and dropped GNOME's English help under usr/share/help/C. Whether any of it is worth ~300 MB of stream is a separate discussion for a later PR.

Medium — -smp 16 → 01ea166, reverted to the nproc-capped form.

Low → 145b786 (package-count range check is now a hard failure; grep -c zero case reaches its error message), c994369 (already-masked hooks are skipped, like _is_devnull_symlink).

Not changed, noted

  • losetup --find race: real, but --find --show isn't a drop-in here because of the mknod dance for device nodes missing inside the container. Needs a try-then-mknod-retry shape; left for a follow-up since it only bites with two builds on one kernel.
  • "Install nothing" audio selection can't be honoured by the image path — inherent, documenting rather than fixing.
  • omarchy-base.packages is still copied onto the ISO though nothing there reads it now; harmless, left alone.

Two things beyond the review

  • 5e99a67 merges the root-image-as-plain-file change (stream sits next to airootfs.sfs in the ISO9660 tree; read straight off the medium, ~10% off the unpack, and extractable with any ISO tool). The checksum follows it.
  • 99e0a2e / f58d9e2: the image was pacstrapped with -K, so it shipped /etc/pacman.d/gnupg with a master key every install would share. The image is now built with -G (and the dir removed regardless, as Install from a prebuilt rootfs image instead of pacstrapping 925 packages #108 does), and the installer creates a per-machine keyring: pacman-key --init + --populate archlinux omarchy against the target, chroot-free via --gpgdir/--populate-from, run as a transient systemd unit (systemd-run --wait --pipe) started after the last pacstrap and joined in create_factory_snapshot. systemd kills the gpg daemons with the unit's cgroup, the dashboard's process-group kill doesn't reach it while systemctl stop does, and its output lands in the journal. On quattro this is what pacstrap -K plus the keyring packages' scriptlets did in one go. A static unit file (target path via an environment file) would be the better shape if more of the install moves under systemd: the delta pacstrap and the arch-chroot finalizers leave the same kind of stray daemons behind and would gain the same cgroup cleanup, kill handle and journal capture. That's a decision to make for all of them together rather than special-case the keyring, so it stays a transient unit here.

Testing

Built the ISO from f58d9e2 and ran ./test/integration (unattended cidata install in QEMU):

  • Orchestrator run: 29 s end to end; Preparing install target 1.6 s (checksum of the 3.6 GB stream, page-cached by the wizard-time prefetch), Installing Arch + Omarchy 20.7 s including the linux delta pacstrap.
  • Verification logged before partitioning; image found at the ISO path; @factory present; installed 938 / expected 939 (the VM gets no microcode).
  • Keyring on the installed system: master key at ultimate trust, Omarchy <pkgs@omarchy.org> at full trust, 183 public keys; the image stream on the ISO carries no etc/pacman.d/gnupg.
  • ISO-side: arch/x86_64/omarchy-root.btrfs.sha256 matches the stream; the shipped mirror (310 files) equals offline-mirror.shipped, and none of its packages is in the image at any version.

Two things seen during testing that are not from this branch, for the record: (1) the factory-reset scenario fails 3 of 8 staging assertions (Windows/foreign entries dropped) — identically on an ISO without the keyring change and in an earlier run; the scenario from #109 expects more than the packaged omarchy 4.0.0 reset tool does. (2) One boot in twelve of the installed system hung: plymouth-start.service: start-post operation timed out at 92 s, after which plymouth update-root-fs --read-write blocked sysinit.target indefinitely. Plymouth flake under virtio-vga, same config as any quattro install; the harness might want plymouth.enable=0 on test boots.

As you said, the #108-vs-#113 call is the maintainer's; the nightly has still never built this path, and the loop device plus btrfs mount inside the GitHub Actions container remains the least-tested surface.

hegjon and others added 4 commits August 23, 2026 05:27
The pre-flight sha256 of the root image ran inline in the orchestrator's
prepare_install_target phase: 1.6s on an NVMe-backed VM, tens of seconds
from a USB stick, all of it after the user had pressed Install. The medium
sits idle while the user works through the configurator, so move the read
there: omarchy-root-image-verify.service runs `sha256sum -c` on the ISO
copy of the stream as a oneshot at boot, niced and at idle I/O class, with
RemainAfterExit so the verdict persists. prepare_install_target now only
collects it: done → go on, failed → the corrupt-medium error with the
unit's journal tail, still running → wait with progress read from the
hasher's /proc/PID/io, never started → start it and wait. Measured in the
install harness, the phase drops from 1.6s to 0.0s.

The unit is the only verifier: the inline hashlib loop goes, and with it
the squashfs fallback location for the stream, which only existed so a
live root and an orchestrator from either side of the move to the plain
ISO file could still pair up. Every ISO now ships the stream and its
checksum at /run/archiso/bootmnt/arch/x86_64, which both conditions of
the unit require; an orchestrator that finds no unit to ask fails the
install instead of hashing quietly.

The wizard-time prefetch in .automated_script.sh waits for the unit before
reading the image: two sequential readers on one USB stick seek against
each other, and the unit's pass is the warm-up anyway. Its head read
afterwards is a cache hit where the image fit, and re-warms the leading
bytes where a small budget let the kernel drop them behind the hash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvydT2sYFgBvji2nk9QbDp
A corrupt install medium failed the install with the right error and the
disk untouched, but the dashboard centres each line of the failure in ~80
columns and clipped the one sentence that mattered: "root image stream is
corrupt: omarchy-ro…" — the advice to re-flash never reached the screen,
and four of the five "last log lines" were systemd's exit/failed/consumed
boilerplate, which had pushed sha256sum's own "FAILED" line out of the
tail. Seen on a throttled-cdrom run with one digit of the recorded sha256
flipped.

Lead with the action on its own short line ("install medium is corrupt:
re-flash it"), put the detail on the next, and append only what sha256sum
wrote (journalctl -u <unit> _COMM=sha256sum) instead of the last five
journal lines. The dashboard folds the failed phase's lines at word
boundaries rather than truncating them.

The corrupt-image integration scenario keeps it that way: copy the ISO,
flip one hex digit of the checksum in place (ISO9660 has no per-file
integrity data; reflink makes the copy free), autoinstall from it, and
assert the verify unit failed, the install halted in the pre-flight phase
with the re-flash advice and sha256sum's verdict, nothing later ran, the
target disk still has no partition table, and both the stop screen and the
advice are visible. It boots the ISO itself rather than the installed base
and reaches the live root over SSH through a tty3 console login, now a
base-test.sh helper (bootstrap_live_root_ssh / ssh_live_root).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvydT2sYFgBvji2nk9QbDp
The scenario flipped a digit of the recorded checksum, which exercises the
same failure but is not what a bad medium looks like: on a badly flashed
stick the checksum file is fine and the bytes under it are not. Damage the
image instead: find the stream on the ISO copy by the NUL-terminated magic
every btrfs send stream starts with, and overwrite 16 bytes a third of the
way in, deep in extent data. The checksum file is untouched.

The fixture no longer re-hashes the damaged stream to check its own work;
the assertions on the install's behaviour are the test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvydT2sYFgBvji2nk9QbDp
The archiso hook defaults to copytoram=auto, which copies the airootfs
to RAM and then unmounts /run/archiso/bootmnt when the boot device is
not an optical drive, the airootfs is under 4 GiB (ours is 3.18 GB)
and MemAvailable exceeds the image size plus 2 GiB. Every USB-booted
laptop with 6 GB or more trips it. The installer then fails in
prepare_install_target with "root image stream missing", because the
root image is deliberately kept out of the airootfs and read straight
off the medium.

The QEMU integration tests attach the ISO as an IDE CD-ROM, which is
the one case the auto rule excludes, so this only showed up on real
hardware (ThinkPad X200s, 8 GB; X200, 6 GB).

Add copytoram=n to every live-boot entry (BIOS syslinux, GRUB,
systemd-boot), guard that with a unit test, and make the missing-stream
error say when the medium was released by copytoram so a hand-edited
cmdline fails with a useful message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJj2oLbxWnJn3Cvqdmz1HS
@omarchybot

Copy link
Copy Markdown
Collaborator

Re-reviewed the 11 commits since 2b7c0144. Reviewed by Claude Opus 5 in Claude Code and by Codex at xhigh reasoning, independently prompted and pinned to 07fc5f08; codex's line citations were checked back against the tree before anything below was written. Ran ./test/all on a disposable VM at this head: all shell unit tests pass, 89 Python tests OK. I did not build an ISO, so test/integration.d/corrupt-image-test.sh was not executed here.

The four open findings

High — format before validate: fixed for full-disk installs. Traced at this head: prepare_install_target (main.py:51) is phase 2 and returns only after verify_root_image_stream collects the unit's verdict (phases_impl.py:227, returning at :283); the first destructive operation is arch.perform_filesystem_operations at phases_impl.py:373 in phase 3, reaching archinstall at archinstall_adapter.py:122. Phase 1's omarchy-iso-cleanup-disk unmounts, deactivates VGs and closes mappings — nothing destructive. The systemd handoff is fail-closed on every state I could construct: deactivating and any unexpected ActiveState raise at :292; _systemctl_show returning {} (OSError or non-zero systemctl show) fails the LoadState == loaded check at :269; a ConditionPathExists skip leaves the unit inactive and aborts, and the missing-file cases are caught earlier at :263-265 with their own message. The checksum is not vacuous: build-iso.sh:361 hashes the basename from inside the image directory, which resolves against the unit's matching WorkingDirectory. Codex traced the same states and agreed; its independence is not currently guaranteed, since codex exec -s read-only restricts writes but not reads.

Medium — downgraded package via unpruned repo-add: fixed. rebuild_offline_repo_db is defined at build-iso.sh:248 and called once, at :305, after the transaction-resolved keep-set (:259) and the prune (:296). That restores the base branch's prune-then-index order, and the rm -f offline.db* makes it a from-scratch index rather than an overlay. Codex checked this independently and found nothing.

Medium — NoExtract dropping man pages: moot. The commit is gone from the branch; NoExtract appears nowhere at this head, and the image pacman.conf at build-iso.sh:327-329 adds only CacheDir.

Medium — -smp 16: fixed. bin/omarchy-iso-boot is now byte-identical to quattro.

New — the free-space (protected) path still formats before validation

The fix covers the orchestrator, and the orchestrator is not the only thing that writes to the disk. On a free-space install the configurator partitions and formats before the orchestrator starts at all: create_partition at configurator:704 and :708 (which runs parted mkpart at disk-partitioning.sh:93), wipefs -af on the new partitions at :728-729, cryptsetup luksFormat at :737, mkfs.btrfs at :751 and mkfs.fat at :772 — all inside run_partition_execute, which .automated_script.sh:123 runs to completion before the orchestrator is launched at :145 and reaches validation at phases_impl.py:227. A corrupt medium on that path therefore halts after two partitions have been created, LUKS-formatted with the user's passphrase and filesystems made.

The cost is bounded and it is not the data-loss case: needs_mklabel is only true when the disk has no partition table at all (configurator:554-564), so an existing table is never re-labelled, and wipefs is scoped to the two partitions this run created. What is left behind is debris rather than destruction — but it is not cleaned up either. cleanup_protected_state (phases_impl.py:2285-2301) only unmounts and closes the LUKS mapper; the configurator's own rollback_created_parts is reachable from disk_abort_hook but not from a failure the orchestrator raises, so a user aborting here on a disk shared with Windows keeps two orphan partitions. Codex found this path independently and named the same missing rollback, which is the part I had not traced.

Not pushing a fix: gating run_partition_execute on the verify unit means deciding what the wizard does when the hash is still activating on a slow stick, and wiring the partition rollback into the orchestrator's failure path crosses a boundary the two halves currently keep. Both are your calls, not a defect fix.

Coverage of the ordering, measured rather than asserted

corrupt-image-test.sh is real coverage of the regression it names, not a test that merely detects corruption somewhere. Its cidata config is "mode": "full_disk" (base-test.sh:470), and it asserts list equality rather than membership: the failed phases are exactly ["Preparing install target"] (:97) and the ok phases are exactly ["Preparing live environment"] (:103), plus no partition table and no blkid signature on /dev/vda (:104-105). Moving the format back before validation fails all three.

The fast suite does not guard it, and I checked that by mutation rather than by reading: swapping the two phase entries in main.py:51-52 on a copy leaves all 89 Python unit tests passing. PrepareInstallTargetTest asserts ordering within prepare_install_target and never relates it to arch_install_system. So the only thing standing between this and a silent regression is a scenario that needs a built ISO and QEMU, and .github/workflows/nightly-build.yml runs omarchy-iso-make alone — neither ./test/all nor ./test/integration. Wiring integration into CI is the better fix than a unit-level guard, which is why I have not added one.

One caveat on the scenario itself: 8ef3a32 reports an end-to-end run with a flipped checksum digit, and 07fc5f0 then changed the fixture to damage the stream bytes instead. The rewritten fixture has no reported run. Its failure mode is safe — if grep -Pboa -m1 'btrfs-stream\x00' matched something other than the stream, the verify unit would pass and assert_refused would fail loudly rather than pass vacuously — but it is untested at this head, by you on the record and by me.

Correction to the #108 comparison in my last comment

That comparison is stale and I should not have left it standing. The three defects I named there — /etc/crypttab clobbered by the restore, no LVM guard, brltty/espeakup dropped for the accessibility entry — were all fixed on #108 in d110966f, 33ffad90 and 67275cdc, pushed 2026-08-21T20:09-20:16, before my comment went out. At its current head 142f683a #108 also rejects xfs and f2fs.

What still separates the two on the point this review has been about: #108 checks that the image is present and that the config is supported before formatting, and has no integrity check on the image at all — a truncated or corrupt squashfs is discovered by unsquashfs after the disk is formatted, which is the failure this branch now closes with the boot-time sha256. It also withdrew its ACL claim in fd30ab53, where btrfs send carries xattrs, ACLs and capabilities natively. On the free-space path neither branch validates before the configurator formats.

Also checked, nothing found

The per-machine keyring is fail-closed end to end: a non-zero unit status raises at phases_impl.py:636 and create_factory_snapshot joins it before doing anything else (:2154), so an install cannot report success with a broken keyring. --gpgdir, --populate-from and both archlinux.gpg/omarchy.gpg exist on a real Omarchy system, and archlinux-keyring reaches the image as a dependency of base, so --populate archlinux omarchy has what it needs. The pre-flight layout check reads user_configuration, which is the same dict written to the archinstall config at context.py:77,101, so the JSON proxy cannot diverge from what archinstall acts on. The dashboard folds rather than truncates the failure text. Codex found nothing further in customize_airootfs.sh, the builder shell, or the mirror indexing.

Waiting on the maintainer: the #108-vs-#113 choice, and whether the free-space path should validate before the configurator formats.

omarchy-root-image-verify.service hashes the multi-GB root image from the
boot medium at boot with IOSchedulingClass=idle, while the live system
pages its airootfs in lazily from the same medium (copytoram is off). The
idle class only means anything under BFQ; the default mq-deadline ignores
I/O priority, so on a slow USB stick the hash competes as an equal with
every squashfs page-in and the boot crawls.

A throttled QEMU boot (usb-storage capped at 33 MB/s, 6 GB RAM, the ISO
booted as a real USB stick under SeaBIOS) confirms it: with a buffered
sha256sum hog running idle-class, interactive random reads complete at
~50/s under mq-deadline versus ~130/s under BFQ -- roughly 2.5-3x more of
the device handed to the live system. The configurator is interactive by
~45s either way while the hash runs to ~120s in the background.

Add a udev rule that puts USB disks, SD cards and optical drives on BFQ
(internal SATA/NVMe install targets keep their default), a boot-medium
helper the verify unit runs as ExecStartPre to log the device and its
scheduler next to the verify result, and a unit test over the shipped
udev rules.

The elevator= kernel parameter cannot do this: it was tied to the legacy
single-queue block layer and became a no-op when blk-mq landed in 5.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJj2oLbxWnJn3Cvqdmz1HS
@hegjon

hegjon commented Aug 23, 2026

Copy link
Copy Markdown
Author

Tested this branch on two real ThinkPads (X200s and X200), both booting the ISO from a USB stick — throughput bottlenecked by the machines' USB 2.0 ports (~30–40 MB/s), not the stick itself. This is a boot path the QEMU integration suite doesn't exercise, since it attaches the ISO as an IDE CD-ROM. Found and fixed two issues; pushed as the two commits at the top.

copytoram=n — the root image is streamed from the boot medium (8def994)

The install failed on real hardware in prepare_install_target with root image stream missing: /run/archiso/bootmnt/arch/x86_64/omarchy-root.btrfs. Cause: archiso's hook defaults to copytoram=auto, which copies the airootfs to RAM and then unmounts /run/archiso/bootmnt when the boot device isn't optical, the airootfs is under 4 GiB (ours is ~3.2 GB), and there's enough RAM. Since this branch deliberately keeps the root image out of the airootfs and reads it straight off the medium, the stream vanishes right when the installer needs it. Every USB-booted machine with ≥6 GB RAM trips it (seen on an X200s/8 GB and an X200/6 GB); the CD-ROM-based tests never hit it because optical is the one case the auto rule excludes.

Fix: pin copytoram=n on every live-boot entry (BIOS syslinux, GRUB, systemd-boot), guard it with a unit test, and make the missing-stream error explain the copytoram case for anyone hand-editing the cmdline.

Boot medium on BFQ so the verify hash yields (10e2073)

omarchy-root-image-verify.service hashes the multi-GB image at boot with IOSchedulingClass=idle, while the live system pages its airootfs in from the same medium. That idle class only means anything under BFQ — the default mq-deadline ignores I/O priority — so on a slow link the hash competes as an equal with every page-in.

Fix: a udev rule putting USB disks / SD cards / optical drives on BFQ (internal SATA/NVMe install targets keep their default), plus a small helper the verify unit runs as ExecStartPre to log the boot device and its scheduler next to the verify result.

Verified in a throttled QEMU boot (ISO booted as a USB usb-storage device capped at 33 MB/s — matching what the test ThinkPads' USB 2.0 ports deliver, which is where slow media makes this contention visible — 6 GB RAM, SeaBIOS): the rule applies (/dev/sda … [bfq], ID_BUS=usb), verify succeeds, and the configurator is interactive by ~45 s while the hash runs to ~120 s in the background. A contention probe (buffered idle-class sha256sum vs parallel normal-priority reads) showed ~50 interactive reads/s under mq-deadline vs ~130/s under BFQ — roughly 2.5–3× more of the device handed to the live system.

Both fixes are exercised on real BIOS hardware end to end: USB → install (~3 min) → Limine (MBR) → LUKS → first boot. (Two post-install failures I hit are out of scope for this branch — the X200s's coreboot GRUB payload can't chainload an MBR bootloader, and the X200's Gen4 GPU lacks the GLES 3 that Hyprland needs.)

…helper

The full-disk path refuses a corrupt medium before archinstall formats
(prepare_install_target runs the verify before phase 3). The free-space
path formats in the configurator — parted, wipefs, luksFormat, mkfs — before
the orchestrator ever starts, so a corrupt medium there halted only after two
partitions had been created and LUKS-formatted with the user's passphrase,
with no rollback from the orchestrator's failure path. Found by an automated
review of omacom#113; the cidata-based corrupt-image test never reached it because
autoinstall skips the configurator.

Fold the verdict collection and the boot-medium/scheduler logging into one
script, omarchy-wait-root-image-verify (replacing omarchy-iso-boot-medium):
it logs the boot device and its scheduler, then collects the boot-time
hasher's verdict, waiting for the unit if it is still running and starting it
if it never did. The configurator runs it before run_partition_execute on the
free-space path; the orchestrator's verify_root_image_stream now shells out to
the same script instead of reimplementing the systemd handoff in Python. One
source of truth for both disk-touching paths; whoever reaches it first pays
the wait.

Drops the unit's ExecStartPre (the script logs the medium now) and the Python
_systemctl_show/_process_read_bytes/_journal_tail helpers (the per-byte verify
progress bar goes with them; the hash is almost always done before either
caller reaches the gate). The copytoram-released-medium message moves into the
script. New wait-root-image-verify-test.sh drives the gate with stubbed
systemctl/findmnt; the Python verify tests now cover the shell-out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJj2oLbxWnJn3Cvqdmz1HS
The quattro merge brought in 9a7325e, which removed the two
`wait_for_screen "without encryption"` waits because tesseract cannot read
gum's highlighted "Yes, install without encryption" button under any
preprocessing. The free-space block this branch adds carries a third copy of
that wait, in a place git had no reason to conflict on, so the merge left it
behind: `omarchy-iso-test --free-space` would spend 30s waiting for text that
never resolves, then die under `set -e` before the install ever starts. Same
treatment as the two sibling sites -- capture the screen and continue; a
Ctrl+C toggle that failed to apply still surfaces as an encrypted install
stalling at the LUKS prompt with SSH never coming up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@omarchybot

Copy link
Copy Markdown
Collaborator

Round 10 at 0dcc9ba. Reviewed by Claude Opus 5 and by Codex at xhigh reasoning, both over the 826a9bc..0dcc9ba delta — the two new commits and the quattro merge. All three of round 9's items are closed, and each is now proved rather than asserted. One new defect, from the merge rather than from either commit, and it is pushed.

Round 9's three items, checked rather than taken on trust. The pristine-digest preamble works: I built a synthetic ISO with the stream at arch/x86_64/omarchy-root.btrfs.zst and its .sha256 beside it, ran the fixture's exact commands against xorriso 1.5.8.pl02, and the recorded digest matched the dd-extracted extent byte for byte; pointing the same preamble at a copy with the byte already flipped refused with the two digests named. The extent-failure diagnostic is reachable now — a bogus stream name prints "xorriso could not report the extent of …" and returns 1 instead of dying bare under set -e, verified in a scratch shell on both sides. And except BaseException is mutation-proved: reverting the one word fails test_an_interrupt_during_spawn_reaps_the_decompressor_too 10 runs out of 10 with AssertionError: unexpectedly None — zstd still alive — and nothing else in the suite; restored, it passes 10 out of 10.

Medium — the quattro merge quietly restored an OCR wait that quattro had just deleted. bin/omarchy-iso-test:716, inside the --free-space block this branch adds, still does wait_for_screen "without encryption" 30 after the Ctrl+C toggle. quattro's 9a7325e removed exactly that call from the two other confirm sites, and its message is unambiguous about why: "Tesseract cannot read the highlighted 'Yes, install without encryption' button under any preprocessing, so waiting for that text always timed out and killed unencrypted installs at the disk warning." The free-space confirm is the same widget — configurator:630 and configurator:962 both set affirmative="Yes, install without encryption" on the same gum confirm — so the text is equally unreadable. Git had no reason to conflict: the branch's copy sits in a block that did not exist on quattro. The consequence is deterministic rather than flaky: bin/omarchy-iso-test --free-space (unencrypted, the default) burns 30 s, wait_for_screen returns 1 at line 269, drive_configurator is called plainly at line 1080 under set -euo pipefail, and the run dies before the install starts. This branch's own new install mode, and the one thing the merge could break without a conflict marker.

Pushed f684071, applying the same treatment as the two sibling sites — capture the screen and continue on a bounded sleep, with the reasoning left to the comment three lines below rather than repeated. Nothing else touched.

The six questions about the whole change, answered at this head. These are older than the delta, but they are the ones worth having written down before this lands.

  1. Does a truncated or corrupt stream fail the install, or land a partial subvolume as @? It fails. There is no shell pipeline and so no pipefail exposure — _receive_root_image spawns both children itself and reads both statuses at phases_impl.py:735-749, raising when either is nonzero. Proved on real filesystems rather than read: a real btrfs send --compressed-data | zstd -15 --long=27 stream over a 1 GiB loop-backed btrfs, truncated at 10/50/90/99%, gives zstd_rc=1 recv_rc=1 every time, against 0/0 for the pristine control. btrfs receive does leave a partial subvolume behind on the top level, but the RuntimeError fires before the snapshot at :542 and the rename at :556, so nothing partial can become @, and the next attempt deletes it at :533.
  2. The verify timeout, against the smaller file and against real USB. floor(bytes / 2 MiB) + 600 s at build-iso.sh:377, over the compressed file, which is the file sha256sum --check actually reads — so the arithmetic and the read measure the same bytes and the fixed ten minutes is proportionally more slack on the smaller artifact. It cannot degrade into a skipped verification: active is the only path out of omarchy-wait-root-image-verify that exits 0, failed and deactivating both fail, and every other state — including the inactive a failed ConditionPathExists leaves behind — falls through to the wildcard and fails. The residual risk is a spurious failure on a genuinely awful stick, and 2 MiB/s is a floor well under what even a bad USB 2.0 port sustains.
  3. --long=27 at decompression. phases_impl.py:142 is the only production decompressor of this stream (.automated_script.sh:75 only warms the page cache) and it passes --long=27, matching build-root-image.sh:68. The body's claim that a stock zstd -d accepts a 27 window is true, and I checked it rather than trusting it: a 400 MB stream compressed -15 --long=27 reports Window Size: 128 MiB and decompresses to a byte-identical sha256 under plain zstd -dc, with no --long. Codex established the other half — archiso's releng list pulls btrfs-progs, which hard-depends on the zstd package, so the CLI is on the live root by construction.
  4. The subvolume swap. Not atomic: btrfs subvolume delete top/@ at :555 and staged.rename(top/"@") at :556 are two ioctls. An interruption between them leaves no @ and the complete image still present as @.image, recoverable by mounting subvolid=5 and renaming it back. Worth knowing, but the @ being deleted is archinstall's empty one and the install has already failed by then, on a disk that was going to be repartitioned anyway. The free-space path cannot reach a pre-existing ESP or data partition: everything here resolves off the btrfs device backing /mnt on subvol=@ (_root_image_target_mounts, :504-520), and the existing partitions are on devices that are never mounted under the target.
  5. Can the image and the per-machine pacstrap disagree about a version? No. The live root's /etc/pacman.conf is pacman-offline.conf (build-iso.sh:467), whose only repository is file:///var/cache/omarchy/mirror/offline/ — there is no network repository section for a moved upstream to be reached through. The image and the delta resolve against the same build-time snapshot, so a partial upgrade on a fresh install is not reachable.
  6. Is "byte-identical" tested? The load-bearing half is: test_root_image.py:529 drives the real zstd over a real compressed fixture and asserts the bytes arriving at the receive's stdin equal the original, so the outer layer is proved lossless. Nothing compares physical extents or two installed systems, so the body's stronger sentence is asserted rather than covered. Not something to fix — just worth not reading as tested.

Not yours, but you will hit it. test/unit/install-media-diagnosis-test.sh is flaky: 1 failure in 8 runs here, on a screen-render race in the diagnosis assertions. It fails at the same rate on quattro itself with the file unchanged, so it is a base-branch flake rather than anything this branch did — but it will occasionally redden ./test/all on this PR and get blamed on it.

One stale line left. The PR body's test plan still says the corrupt-image fixture "now flips a digit of the recorded sha256" — the same claim 7577f67 corrected in README.md, and 826a9bc is what stopped it being true. Only the body now.

Tests. ./test/all green on a disposable worker VM before and after the push (96 Python tests plus every shell suite). All mutation runs, the real btrfs truncation matrix, the zstd window round-trip and the xorriso extent work above were on that worker too. No ISO was built and no integration scenario was booted here — your 0dcc9ba run and the corrupt-image 8/8 are read, not reproduced. In particular --free-space has not been run since the merge, by either of us, which is why the finding above was only visible by reading.

What the second opinion added. Codex reached the merge finding independently, citing the same 9a7325e and the same three line ranges, and its account of the free-space partitioning path (the configurator formats only the new EFI and root partitions it created in the selected free extent, then hands them over pre-mounted) is a chain I had not traced. That agreement is worth less than it looks: codex exec -s read-only restricts writes but not reads, so its independence from this review is not currently guaranteed. It also contributed the btrfs-progs → zstd dependency argument in item 3 and the recovery procedure in item 4. One finding of its own I rejected: that the digest extraction is not rerunnable, because xorriso would refuse to overwrite an existing $BASE_DIR/stream.sha256 on a second --reuse-base or standalone run. Tested directly against xorriso 1.5.8.pl02 — the second extract into an existing file returns 0 and overwrites, and the fixture's guard passes with the correct digest. It also matters that the chatter goes to stderr: with 2>/dev/null the command substitution captures only awk's output, so the anchored 64-hex guard sees a bare digest.

Waiting on the maintainer.

@hegjon

hegjon commented Sep 5, 2026

Copy link
Copy Markdown
Author

Ran --free-space at f684071 — the one thing round 10 fixed by reading rather than running, so it was worth actually booting. Green end to end.

Built an ISO from this head (omarchy-2026.09.05-x86_64-quattro.iso, 6.85 GB) and drove bin/omarchy-iso-test --free-space unencrypted, the exact path the merge had broken:

  • The free-space confirm is reached, ctrl-c toggles to the unencrypted flow, and the run continues straight into the install — no 30 s wait, no death before install_phase. Screenshots success-installer-11-install-mode through success-installer-14-install-started are all present in order.
  • Install completes, the VM reboots into the installed system, and verify_preserved_partitions confirms WINDOWS_ESP and WINDOWS_DATA (marker file included) survived intact — the pre-existing ESP and data partition the free-space mode exists to preserve.
  • Shortcut smoke: 26/26.
  • Full graphical acceptance suite against the installed system (--sync-omarchy an omarchy v4.0.2 checkout, matching the installed package): 144/144, 0 failures, passed in 80 s.

So the whole scenario is green: the merge regression is gone, the free-space install works, the existing partitions are preserved, and the installed desktop passes acceptance.

Host: Ryzen 9 9950X, QEMU/KVM headless. Also reproduced the free-space install and the 26/26 smoke on an earlier branch ISO (0dcc9ba content) before rebuilding, same result.

@hegjon

hegjon commented Sep 5, 2026

Copy link
Copy Markdown
Author

End user confirms that omacom/omarchy#7515 is fixed by this PR.

Source: https://x.com/julian_le_roux/status/2096304775637630991

@emestee

emestee commented Sep 28, 2026

Copy link
Copy Markdown

Hello,

I've started a similar project without realizing this existed. In my experiments, direct dd beats btrfs send and with other optimizations allows about 30% shave on wall clock install time. A problem with this approach is the replication of UUIDs which is solvable, but requires a custom tree for btrfs-tools. I will review this PR to see if anything in my project is worth porting over. I probably have useful findings which you can integrate. Otherwise I see no reason to maintain a competing lineage.

I'll post my findings later.

@hegjon

hegjon commented Sep 28, 2026

Copy link
Copy Markdown
Author

Yes, btrfs receive have some overhead. I think #145 that uses qemu-img convert is the most performant option at the moment. Issue #151 have an overview, feel free to include your experiment .

@emestee

emestee commented Sep 28, 2026

Copy link
Copy Markdown

Thanks much, I think an optimal approach can be synthesized from every variant. Clearly I didnt do my homework.

emirb added a commit that referenced this pull request Oct 6, 2026
The ISO has shipped only omarchy-root.img.zst since the F1 write replaced the
send stream, and build-root-image.sh no longer produces a stream (its send
block was commented out). So the receive path could not run: _install_root_image
always took the write. Gone with it:

- phases_impl: ROOT_IMAGE_STREAM, ROOT_IMAGE_DECOMPRESS, the plain
  uncompressed omarchy-root.img fallback (ROOT_IMAGE_RAW and its dd branch),
  _receive_root_image and _close_receive, and the receive branch of
  _install_root_image. _root_image() keeps the copytoram message for an image
  that is missing because the medium was released. throwaway_root_fs is True:
  the image always overwrites the btrfs archinstall makes.
  verify_root_image_stream is verify_install_medium (it checks the mirror too).
- build-root-image.sh: STREAM_COMPRESS, the commented-out send block, the
  output-name munging notes; the header describes the packed image.
- build-iso.sh: root_image_stream is root_image; comments say image.
- Tests: the receive pump's and the swap's (the write and the reshape are
  covered by test_install_root_image_dd.py); the required-package gate keeps
  its test as FinishRootImageTest. pxe checks for the one image name.

The zstdcat | dd pipe stays: it is the write's fallback when
omarchy-image-write refuses an image.

This is hegjon's receive path from #113, hardened over ten
review rounds; the raw write superseded it, and its verify gate, keyring,
release and copytoram work all stay.
emirb added a commit that referenced this pull request Oct 6, 2026
install-media-diagnosis-test.sh failed about one run in eight on CI (omarchybot
measured 1 in 8 on unmodified quattro in #113; CI run
36818971497 here), each time on a different screen assertion, with the text
it looked for on the screen it printed. The checks were
`visible_screen | grep -qF TEXT` under set -o pipefail: GNU grep -q exits at
its first match, the sed still writing the screen gets SIGPIPE, and pipefail
reports the pipeline as failed. The screen is about 4.5 KB, two writes of
sed's 4 KiB buffer, so it only lost when grep finished between them: 2 in
1000 locally with grep on another core, likelier on a loaded 2-vCPU runner,
and the test makes several such checks. Forced (sed -u, writing line by line)
it fails 100 times in 100; the here-string form 0 in 100. A desktop with
ugrep as grep never shows it: ugrep reads its whole input before exiting.

The checks take the screen as a here-string, and the one pipeline that ended
in head -n 1 ends in sed -n 1p. boot-cmdline-test.sh had the same shape in
an if, where a SIGPIPE would have made the cms_verify guard pass on a
regression; it is one grep -rq now, and still fails on an injected
cms_verify=y.
emirb added a commit that referenced this pull request Oct 6, 2026
The ISO has shipped only omarchy-root.img.zst since the F1 write replaced the
send stream, and build-root-image.sh no longer produces a stream (its send
block was commented out). So the receive path could not run: _install_root_image
always took the write. Gone with it:

- phases_impl: ROOT_IMAGE_STREAM, ROOT_IMAGE_DECOMPRESS, the plain
  uncompressed omarchy-root.img fallback (ROOT_IMAGE_RAW and its dd branch),
  _receive_root_image and _close_receive, and the receive branch of
  _install_root_image. _root_image() keeps the copytoram message for an image
  that is missing because the medium was released. throwaway_root_fs is True:
  the image always overwrites the btrfs archinstall makes.
  verify_root_image_stream is verify_install_medium (it checks the mirror too).
- build-root-image.sh: STREAM_COMPRESS, the commented-out send block, the
  output-name munging notes; the header describes the packed image.
- build-iso.sh: root_image_stream is root_image; comments say image.
- Tests: the receive pump's and the swap's (the write and the reshape are
  covered by test_install_root_image_dd.py); the required-package gate keeps
  its test as FinishRootImageTest. pxe checks for the one image name.

The zstdcat | dd pipe stays: it is the write's fallback when
omarchy-image-write refuses an image.

This is hegjon's receive path from #113, hardened over ten
review rounds; the raw write superseded it, and its verify gate, keyring,
release and copytoram work all stay.
emirb added a commit that referenced this pull request Oct 6, 2026
install-media-diagnosis-test.sh failed about one run in eight on CI (omarchybot
measured 1 in 8 on unmodified quattro in #113; CI run
36818971497 here), each time on a different screen assertion, with the text
it looked for on the screen it printed. The checks were
`visible_screen | grep -qF TEXT` under set -o pipefail: GNU grep -q exits at
its first match, the sed still writing the screen gets SIGPIPE, and pipefail
reports the pipeline as failed. The screen is about 4.5 KB, two writes of
sed's 4 KiB buffer, so it only lost when grep finished between them: 2 in
1000 locally with grep on another core, likelier on a loaded 2-vCPU runner,
and the test makes several such checks. Forced (sed -u, writing line by line)
it fails 100 times in 100; the here-string form 0 in 100. A desktop with
ugrep as grep never shows it: ugrep reads its whole input before exiting.

The checks take the screen as a here-string, and the one pipeline that ended
in head -n 1 ends in sed -n 1p. boot-cmdline-test.sh had the same shape in
an if, where a SIGPIPE would have made the cms_verify guard pass on a
regression; it is one grep -rq now, and still fails on an injected
cms_verify=y.
emirb added a commit that referenced this pull request Oct 6, 2026
The ISO has shipped only omarchy-root.img.zst since the F1 write replaced the
send stream, and build-root-image.sh no longer produces a stream (its send
block was commented out). So the receive path could not run: _install_root_image
always took the write. Gone with it:

- phases_impl: ROOT_IMAGE_STREAM, ROOT_IMAGE_DECOMPRESS, the plain
  uncompressed omarchy-root.img fallback (ROOT_IMAGE_RAW and its dd branch),
  _receive_root_image and _close_receive, and the receive branch of
  _install_root_image. _root_image() keeps the copytoram message for an image
  that is missing because the medium was released. throwaway_root_fs is True:
  the image always overwrites the btrfs archinstall makes.
  verify_root_image_stream is verify_install_medium (it checks the mirror too).
- build-root-image.sh: STREAM_COMPRESS, the commented-out send block, the
  output-name munging notes; the header describes the packed image.
- build-iso.sh: root_image_stream is root_image; comments say image.
- Tests: the receive pump's and the swap's (the write and the reshape are
  covered by test_install_root_image_dd.py); the required-package gate keeps
  its test as FinishRootImageTest. pxe checks for the one image name.

The zstdcat | dd pipe stays: it is the write's fallback when
omarchy-image-write refuses an image.

This is hegjon's receive path from #113, hardened over ten
review rounds; the raw write superseded it, and its verify gate, keyring,
release and copytoram work all stay.
emirb added a commit that referenced this pull request Oct 6, 2026
install-media-diagnosis-test.sh failed about one run in eight on CI (omarchybot
measured 1 in 8 on unmodified quattro in #113; CI run
36818971497 here), each time on a different screen assertion, with the text
it looked for on the screen it printed. The checks were
`visible_screen | grep -qF TEXT` under set -o pipefail: GNU grep -q exits at
its first match, the sed still writing the screen gets SIGPIPE, and pipefail
reports the pipeline as failed. The screen is about 4.5 KB, two writes of
sed's 4 KiB buffer, so it only lost when grep finished between them: 2 in
1000 locally with grep on another core, likelier on a loaded 2-vCPU runner,
and the test makes several such checks. Forced (sed -u, writing line by line)
it fails 100 times in 100; the here-string form 0 in 100. A desktop with
ugrep as grep never shows it: ugrep reads its whole input before exiting.

The checks take the screen as a here-string, and the one pipeline that ended
in head -n 1 ends in sed -n 1p. boot-cmdline-test.sh had the same shape in
an if, where a SIGPIPE would have made the cms_verify guard pass on a
regression; it is one grep -rq now, and still fails on an injected
cms_verify=y.
emirb added a commit that referenced this pull request Oct 6, 2026
The ISO ships only omarchy-root.img.zst and build-root-image.sh produces no
send stream, so the receive path could not run: _install_root_image always
took the write. Gone with it:

- phases_impl: ROOT_IMAGE_STREAM, ROOT_IMAGE_DECOMPRESS, the plain
  uncompressed omarchy-root.img fallback (ROOT_IMAGE_RAW and its dd branch),
  _receive_root_image and _close_receive, and the receive branch of
  _install_root_image. _root_image() keeps the copytoram message for an image
  that is missing because the medium was released. throwaway_root_fs is True:
  the image always overwrites the btrfs archinstall makes.
  verify_root_image_stream is verify_install_medium (it checks the mirror too).
- build-root-image.sh: STREAM_COMPRESS and the fallback that appended .zst to
  the output name; the header describes the packed image.
- build-iso.sh: root_image_stream is root_image; comments say image.
- Tests: the receive pump's and the swap's (the write and the reshape are
  covered by test_install_root_image_dd.py); the required-package gate keeps
  its test as FinishRootImageTest. pxe checks for the one image name.

The zstdcat | dd pipe stays: it is the write's fallback when
omarchy-image-write refuses an image.

The receive path is hegjon's, from #113; the image write
supersedes it, and that work's verify gate, keyring, release and copytoram
handling all stay.
emirb added a commit that referenced this pull request Oct 6, 2026
install-media-diagnosis-test.sh failed about one run in eight on CI (the same
rate was measured on the unmodified base branch in #113),
each time on a different screen assertion, with the text it looked for on
the screen it printed. The checks were
`visible_screen | grep -qF TEXT` under set -o pipefail: GNU grep -q exits at
its first match, the sed still writing the screen gets SIGPIPE, and pipefail
reports the pipeline as failed. The screen is about 4.5 KB, two writes of
sed's 4 KiB buffer, so it only lost when grep finished between them: 2 in
1000 locally with grep on another core, likelier on a loaded 2-vCPU runner,
and the test makes several such checks. Forced (sed -u, writing line by line)
it fails 100 times in 100; the here-string form 0 in 100. A desktop with
ugrep as grep never shows it: ugrep reads its whole input before exiting.

The checks take the screen as a here-string, and the one pipeline that ended
in head -n 1 ends in sed -n 1p. boot-cmdline-test.sh had the same shape in
an if, where a SIGPIPE would have made the cms_verify guard pass on a
regression; it is one grep -rq now, and still fails on an injected
cms_verify=y.
emirb added a commit that referenced this pull request Oct 6, 2026
The ISO ships only omarchy-root.img.zst and build-root-image.sh produces no
send stream, so the receive path could not run: _install_root_image always
took the write. Gone with it:

- phases_impl: ROOT_IMAGE_STREAM, ROOT_IMAGE_DECOMPRESS, the plain
  uncompressed omarchy-root.img fallback (ROOT_IMAGE_RAW and its dd branch),
  _receive_root_image and _close_receive, and the receive branch of
  _install_root_image. _root_image() keeps the copytoram message for an image
  that is missing because the medium was released. throwaway_root_fs is True:
  the image always overwrites the btrfs archinstall makes.
  verify_root_image_stream is verify_install_medium (it checks the mirror too).
- build-root-image.sh: STREAM_COMPRESS and the fallback that appended .zst to
  the output name; the header describes the packed image.
- build-iso.sh: root_image_stream is root_image; comments say image.
- Tests: the receive pump's and the swap's (the write and the reshape are
  covered by test_install_root_image_dd.py); the required-package gate keeps
  its test as FinishRootImageTest. pxe checks for the one image name.

The zstdcat | dd pipe stays: it is the write's fallback when
omarchy-image-write refuses an image.

The receive path is hegjon's, from #113; the image write
supersedes it, and that work's verify gate, keyring, release and copytoram
handling all stay.
emirb added a commit that referenced this pull request Oct 6, 2026
install-media-diagnosis-test.sh failed about one run in eight on CI (the same
rate was measured on the unmodified base branch in #113),
each time on a different screen assertion, with the text it looked for on
the screen it printed. The checks were
`visible_screen | grep -qF TEXT` under set -o pipefail: GNU grep -q exits at
its first match, the sed still writing the screen gets SIGPIPE, and pipefail
reports the pipeline as failed. The screen is about 4.5 KB, two writes of
sed's 4 KiB buffer, so it only lost when grep finished between them: 2 in
1000 locally with grep on another core, likelier on a loaded 2-vCPU runner,
and the test makes several such checks. Forced (sed -u, writing line by line)
it fails 100 times in 100; the here-string form 0 in 100. A desktop with
ugrep as grep never shows it: ugrep reads its whole input before exiting.

The checks take the screen as a here-string, and the one pipeline that ended
in head -n 1 ends in sed -n 1p. boot-cmdline-test.sh had the same shape in
an if, where a SIGPIPE would have made the cms_verify guard pass on a
regression; it is one grep -rq now, and still fails on an injected
cms_verify=y.
emirb added a commit that referenced this pull request Oct 6, 2026
The ISO ships only omarchy-root.img.zst and build-root-image.sh produces no
send stream, so the receive path could not run: _install_root_image always
took the write. Gone with it:

- phases_impl: ROOT_IMAGE_STREAM, ROOT_IMAGE_DECOMPRESS, the plain
  uncompressed omarchy-root.img fallback (ROOT_IMAGE_RAW and its dd branch),
  _receive_root_image and _close_receive, and the receive branch of
  _install_root_image. _root_image() keeps the copytoram message for an image
  that is missing because the medium was released. throwaway_root_fs is True:
  the image always overwrites the btrfs archinstall makes.
  verify_root_image_stream is verify_install_medium (it checks the mirror too).
- build-root-image.sh: STREAM_COMPRESS and the fallback that appended .zst to
  the output name; the header describes the packed image.
- build-iso.sh: root_image_stream is root_image; comments say image.
- Tests: the receive pump's and the swap's (the write and the reshape are
  covered by test_install_root_image_dd.py); the required-package gate keeps
  its test as FinishRootImageTest. pxe checks for the one image name.

The zstdcat | dd pipe stays: it is the write's fallback when
omarchy-image-write refuses an image.

The receive path is hegjon's, from #113; the image write
supersedes it, and that work's verify gate, keyring, release and copytoram
handling all stay.
emirb added a commit that referenced this pull request Oct 6, 2026
install-media-diagnosis-test.sh failed about one run in eight on CI (the same
rate was measured on the unmodified base branch in #113),
each time on a different screen assertion, with the text it looked for on
the screen it printed. The checks were
`visible_screen | grep -qF TEXT` under set -o pipefail: GNU grep -q exits at
its first match, the sed still writing the screen gets SIGPIPE, and pipefail
reports the pipeline as failed. The screen is about 4.5 KB, two writes of
sed's 4 KiB buffer, so it only lost when grep finished between them: 2 in
1000 locally with grep on another core, likelier on a loaded 2-vCPU runner,
and the test makes several such checks. Forced (sed -u, writing line by line)
it fails 100 times in 100; the here-string form 0 in 100. A desktop with
ugrep as grep never shows it: ugrep reads its whole input before exiting.

The checks take the screen as a here-string, and the one pipeline that ended
in head -n 1 ends in sed -n 1p. boot-cmdline-test.sh had the same shape in
an if, where a SIGPIPE would have made the cms_verify guard pass on a
regression; it is one grep -rq now, and still fails on an injected
cms_verify=y.
emirb added a commit that referenced this pull request Oct 6, 2026
The ISO ships only omarchy-root.img.zst and build-root-image.sh produces no
send stream, so the receive path could not run: _install_root_image always
took the write. Gone with it:

- phases_impl: ROOT_IMAGE_STREAM, ROOT_IMAGE_DECOMPRESS, the plain
  uncompressed omarchy-root.img fallback (ROOT_IMAGE_RAW and its dd branch),
  _receive_root_image and _close_receive, and the receive branch of
  _install_root_image. _root_image() keeps the copytoram message for an image
  that is missing because the medium was released. throwaway_root_fs is True:
  the image always overwrites the btrfs archinstall makes.
  verify_root_image_stream is verify_install_medium (it checks the mirror too).
- build-root-image.sh: STREAM_COMPRESS and the fallback that appended .zst to
  the output name; the header describes the packed image.
- build-iso.sh: root_image_stream is root_image; comments say image.
- Tests: the receive pump's and the swap's (the write and the reshape are
  covered by test_install_root_image_dd.py); the required-package gate keeps
  its test as FinishRootImageTest. pxe checks for the one image name.

The zstdcat | dd pipe stays: it is the write's fallback when
omarchy-image-write refuses an image.

The receive path is hegjon's, from #113; the image write
supersedes it, and that work's verify gate, keyring, release and copytoram
handling all stay.
emirb added a commit that referenced this pull request Oct 6, 2026
install-media-diagnosis-test.sh failed about one run in eight on CI (the same
rate was measured on the unmodified base branch in #113),
each time on a different screen assertion, with the text it looked for on
the screen it printed. The checks were
`visible_screen | grep -qF TEXT` under set -o pipefail: GNU grep -q exits at
its first match, the sed still writing the screen gets SIGPIPE, and pipefail
reports the pipeline as failed. The screen is about 4.5 KB, two writes of
sed's 4 KiB buffer, so it only lost when grep finished between them: 2 in
1000 locally with grep on another core, likelier on a loaded 2-vCPU runner,
and the test makes several such checks. Forced (sed -u, writing line by line)
it fails 100 times in 100; the here-string form 0 in 100. A desktop with
ugrep as grep never shows it: ugrep reads its whole input before exiting.

The checks take the screen as a here-string, and the one pipeline that ended
in head -n 1 ends in sed -n 1p. boot-cmdline-test.sh had the same shape in
an if, where a SIGPIPE would have made the cms_verify guard pass on a
regression; it is one grep -rq now, and still fails on an injected
cms_verify=y.
emirb added a commit that referenced this pull request Oct 7, 2026
The ISO ships only omarchy-root.img.zst and build-root-image.sh produces no
send stream, so the receive path could not run: _install_root_image always
took the write. Gone with it:

- phases_impl: ROOT_IMAGE_STREAM, ROOT_IMAGE_DECOMPRESS, the plain
  uncompressed omarchy-root.img fallback (ROOT_IMAGE_RAW and its dd branch),
  _receive_root_image and _close_receive, and the receive branch of
  _install_root_image. _root_image() keeps the copytoram message for an image
  that is missing because the medium was released. throwaway_root_fs is True:
  the image always overwrites the btrfs archinstall makes.
  verify_root_image_stream is verify_install_medium (it checks the mirror too).
- build-root-image.sh: STREAM_COMPRESS and the fallback that appended .zst to
  the output name; the header describes the packed image.
- build-iso.sh: root_image_stream is root_image; comments say image.
- Tests: the receive pump's and the swap's (the write and the reshape are
  covered by test_install_root_image_dd.py); the required-package gate keeps
  its test as FinishRootImageTest. pxe checks for the one image name.

The zstdcat | dd pipe stays: it is the write's fallback when
omarchy-image-write refuses an image.

The receive path is hegjon's, from #113; the image write
supersedes it, and that work's verify gate, keyring, release and copytoram
handling all stay.
emirb added a commit that referenced this pull request Oct 7, 2026
install-media-diagnosis-test.sh failed about one run in eight on CI (the same
rate was measured on the unmodified base branch in #113),
each time on a different screen assertion, with the text it looked for on
the screen it printed. The checks were
`visible_screen | grep -qF TEXT` under set -o pipefail: GNU grep -q exits at
its first match, the sed still writing the screen gets SIGPIPE, and pipefail
reports the pipeline as failed. The screen is about 4.5 KB, two writes of
sed's 4 KiB buffer, so it only lost when grep finished between them: 2 in
1000 locally with grep on another core, likelier on a loaded 2-vCPU runner,
and the test makes several such checks. Forced (sed -u, writing line by line)
it fails 100 times in 100; the here-string form 0 in 100. A desktop with
ugrep as grep never shows it: ugrep reads its whole input before exiting.

The checks take the screen as a here-string, and the one pipeline that ended
in head -n 1 ends in sed -n 1p. boot-cmdline-test.sh had the same shape in
an if, where a SIGPIPE would have made the cms_verify guard pass on a
regression; it is one grep -rq now, and still fails on an injected
cms_verify=y.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Removing the install medium at the Reboot Now screen wedges the machine in an I/O error storm

4 participants