Repository navigation
Conversation
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 SummaryThe 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.
Confidence Score: 5/5The 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.
|
| 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
Reviews (47): Last reviewed commit: "Cover the two things the release's verdi..." | Re-trigger Greptile
|
absolutely mogs #108 |
|
I did not know about #108, I am curious about the differences |
|
Pacman keyring — please decide. This PR ships one |
|
Reviewed. Read the full diff against First, what I went looking for and did not find. The competing squashfs PR (#108) clobbers High — the disk is destroyed before the image is validated. 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. Medium — the image can be built from a downgraded package. Medium — a fresh install has no man pages. The image is pacstrapped with Medium — leftover benchmarking knob. Low:
On tests. 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 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. |
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
2b7c014 to
c994369
Compare
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
|
Thanks for the thorough review. Everything below is pushed; the branch is now at f58d9e2 (12 commits). FindingsHigh — disk destroyed before validation → 6406d49. All pre-flight checks now run in Medium — image built from a downgraded package → 418c840. The resolve → prune → Medium — no man pages → NoExtract dropped entirely (the commit is gone from the branch). Your open question on Medium — Low → 145b786 (package-count range check is now a hard failure; Not changed, noted
Two things beyond the review
TestingBuilt the ISO from f58d9e2 and ran
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 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. |
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
|
Re-reviewed the 11 commits since The four open findingsHigh — format before validate: fixed for full-disk installs. Traced at this head: Medium — downgraded package via unpruned Medium — NoExtract dropping man pages: moot. The commit is gone from the branch; Medium — New — the free-space (protected) path still formats before validationThe 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: The cost is bounded and it is not the data-loss case: Not pushing a fix: gating Coverage of the ordering, measured rather than asserted
The fast suite does not guard it, and I checked that by mutation rather than by reading: swapping the two phase entries in One caveat on the scenario itself: Correction to the #108 comparison in my last commentThat comparison is stale and I should not have left it standing. The three defects I named there — 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 Also checked, nothing foundThe per-machine keyring is fail-closed end to end: a non-zero unit status raises at 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
|
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.
The install failed on real hardware in Fix: pin Boot medium on BFQ so the verify hash yields (10e2073)
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 Verified in a throttled QEMU boot (ISO booted as a USB 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>
|
Round 10 at 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 Medium — the Pushed 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.
Not yours, but you will hit 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 Tests. What the second opinion added. Codex reached the merge finding independently, citing the same Waiting on the maintainer. |
|
Ran Built an ISO from this head (
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 ( |
|
End user confirms that omacom/omarchy#7515 is fixed by this PR. Source: https://x.com/julian_le_roux/status/2096304775637630991 |
|
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. |
|
Thanks much, I think an optimal approach can be synthesized from every variant. Clearly I didnt do my homework. |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.


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-datastream. 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 receivestores 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:
quattroISO8329635, 2026-08-28)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 asomarchy-root.btrfs.zst(zstd -15 --long=27 -T0over 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=27is exactly what a stockzstd -daccepts; 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-spacemode (pre-existing ESP + data partition verified intact from inside the installed system).How it works
Build (
builder/build-root-image.sh, called frombuild-iso.sh)builder/image.packages, minus the per-machinehardware_packages) into a btrfs loop image mountedcompress-force=zstd:15, with the boot-image pacman hooks masked andNoExtractfor docs, man pages and non-English locales, thenbtrfs send --compressed-datait into the airootfs (stored uncompressed in the squashfs like the mirror).configs/airootfs/root/customize_airootfs.shthen 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 soomarchy-pkg-add'spacman -S --neededover mixed package lists still resolves every name.Install (
orchestrator/phases_impl.py,archinstall_adapter.py)mount_ordered_layout+sanity_check: mount the filesystem top level,btrfs receive(publishingphase_progressfor the dashboard), snapshot writable, unmount the layout, replace archinstall's empty@, replay the mount table fromfindmnt(so LUKS is never unlocked twice and the configurator-mounted protected layout works unchanged), copy the image'spacman.loginto@log,systemd-machine-id-setup.install_base_deltamirrors archinstall 4.4'sminimal_installationstep for step with the bulk pacstrap reduced to what the image lacks (kernel, microcode).install_applicationsstraps only missing packages (the image carries PipeWire; the mirror no longer does).Review notes
mknodsince Docker fills/devonce). Not yet exercised in the nightly workflow.customize_airootfs.shis deprecated in archiso but supported by the pinned submodule (v87).pacman-key --initkeyring per build, like the officialarchlinuxDocker image; re-initialising per machine would cost several seconds of--populate.NoExtractandcompress-forceapply to the image only. The installed system runs Omarchy's ownpacman.confand mountscompress=zstd, so upgrades bring docs/man/translations back and new data follows the heuristic — ISO size was the goal, the system may regrow.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-spacebuilds its Windows-style fixture without root; unlikeomarchy-iso-test-windows-diskit has noEFI/Microsoftdirectory, so the configurator reports no Windows ESP (same free-space mechanics either way).Test plan
test/allbin/omarchy-iso-test release/*-image.iso --install-onlybin/omarchy-iso-test release/*-image.iso --encrypt --install-onlybin/omarchy-iso-test release/*-image.iso --free-space --install-only8329635ISO:test/all(the receive tests now drive the realzstdon real compressed fixtures, mutation-checked), unattended cidata install through the new receive pipeline,corrupt-image8/8 (its fixture now flips a digit of the recorded sha256), and a hands-on interactive install in QEMU booted through to the installed systembin/omarchy-iso-testwithout--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.