Skip to content

Install with archinstall-bash: moving towards systemd as the orchestrator - #132

Open
hegjon wants to merge 177 commits into
omacom:quattrofrom
hegjon:archinstall-bash
Open

Install with archinstall-bash: moving towards systemd as the orchestrator#132
hegjon wants to merge 177 commits into
omacom:quattrofrom
hegjon:archinstall-bash

Conversation

@hegjon

@hegjon hegjon commented Aug 28, 2026

Copy link
Copy Markdown

This PR is here for early feedback, not for merging yet: it waits on the conclusion of #113 (whose content it carries),

(hegjon:archinstall-bash 1→ quattro, ~95 commits, 107 files, +10,249/−3,893)

This branch replaces Python archinstall with archinstall-bash, a bash port of the archinstall surface the Omarchy installer actually uses, and ports the orchestrator itself to bash. The live ISO ships no Python at all — python, kbd, clonezilla, cloud-init and reflector are gone from the airootfs. The library started as its own repo and is now vendored into this tree with its history (subtree merge): one repo, one reviewable diff, no submodule pointing at a personal remote.

The installer's source code shrinks about fivefold, from 25,564 lines of Python to 4,957 lines of bash. The archinstall 4.4 package alone is 22,827 lines, plus an orchestrator on top; this branch ships the vendored library at 2,291 lines and the orchestrator with its context files at 2,666. Raw line counts, comments included, split out in the table below. Not the same functionality in a fifth of the code: the port covers the surface Omarchy actually calls, instead of carrying every archinstall feature onto every install medium.

It carries #113. The root-image install (btrfs send/receive of a prebuilt system, verified before the disk is formatted) was ported function-for-function into the bash orchestrator and has tracked every #113 review round since, including round 7's test fixes. If #113 merges first, this branch's shared content resolves at merge; the choice between the two is the same #108-vs-#113-vs-this discussion.

What changes beyond #113

  • Bash orchestrator (configs/airootfs/usr/share/omarchy-iso/orchestrator/*.sh) driving the vendored library one step at a time; the Python orchestrator and its tests are gone. Port fidelity was validated by comparing an installed system against a Python-archinstall install: limine.conf byte-identical modulo machine-id/hashes/PARTUUIDs, same package set.
  • Offline mirror as plain files on the ISO9660 tree (arch/x86_64/mirror/), outside the squashfs, bind-mounted read-only onto the unchanged /var/cache/omarchy/mirror/offline. The mirror is now verified before the installer touches the disk (omarchy-verify-mirror checks every package the medium carries against the repo database's %SHA256SUM% — the same number pacman validates on install — restricted by the build's shipped-package list, since the database deliberately describes packages the root image already provides), which it never was inside the squashfs. ISO size unchanged by the move, byte for byte. The move itself shaved install time — pacstrap reads packages straight off the ISO9660 tree instead of through squashfs decompression — and the whole-mirror verification spent that saving again: net install time is a wash, with the difference converted from speed into a guarantee, the medium proven good before anything is formatted.
  • Keyring init as a shipped unit (omarchy-target-keyring.service, RemainAfterExit=yes so unit state alone answers the orchestrator's join; the last systemd-run left the tree) fed by a shared context.env that both systemd units and bash source.
  • Pre-flight gate hardening: boot medium checked with mountpoint plus stream existence before formatting, with the actionable copytoram message; RequiresMountsFor= ordering on the verify units.
  • Library/orchestrator seams dissolved: ui-helper precedence is a define-if-missing contract instead of source-order luck; installer_setup_swap no longer writes a conf the vendor drop-in outranks; strapping only what the target lacks is the only mode.

This failure class is not hypothetical. omacom/omarchy#7704 (open) is a 4.0.0 install that consistently dies mid-pacstrap on an offline-mirror problem with the disk already formatted, and omacom/omarchy#6985 was a Quattro free-space install — alongside Windows — halting twice in "Configuring system" on missing mirror files, leaving the user to hand-patch scripts inside a chroot. Those two are missing-package bugs (a build-side gap the verification here narrows but cannot fully close); the corrupt-medium half of the class — the bad flash, the dying stick — is exactly what the pre-format verify gates turn from a half-wiped machine into a "re-flash it" message while the old system is still intact.

Where this is heading: systemd as the orchestrator

The endpoint this branch is walking toward is an omarchy-install.target where the install's phases are oneshot units and the ordering lives in After=/Requires= edges instead of a bash loop — the phase runner dissolves into a dependency graph, the pre-format gates become plain unit dependencies, cleanup becomes lifecycle (PartOf=, BindsTo=, ExecStopPost=) rather than trap code, and every phase gets journal logging, timing and a Result for free. A failed phase could even be fixed and resumed, which neither archinstall ever offered.

The first step is in the branch already: omarchy-install.target exists as the umbrella — phase units and the keyring init declare PartOf= it, so stopping the target is a cgroup-complete group abort — and the root-image unpack is the first migrated phase: omarchy-install-image.service hosts the existing phase function in its own process via a small run-phase entrypoint, receives its context through layered EnvironmentFile= (defaults, this run's resolved values, and the raw configurator inputs the context code now persists), and is bounded by a build-time timeout drop-in sized to the stream, like the verify units. The orchestrator's loop starts the unit where it used to call the function; a oneshot's blocking start is the join. Phases migrate one at a time behind that pattern, each dragging its ordering into the target as it moves.

Measured (KVM rig, identical VM shape, all KVM cells re-measured 2026-08-29 in one sitting; X200 = real-hardware A/B from #113)

quattro (pacstrap) #113 (Python + image) this branch
ISO size 5.74 GiB 6.76 GiB 6.30 GiB²
Install, KVM (full VM lifetime) 86 s 66 s 77 s³
Install phase, X200 USB 496 s 159 s (warm)¹ 241 s (cold)¹
Installed boot (systemd-analyze) 10.61 s 10.98 s 10.99 s
Packages installed 940 940 942
Installer source aboard 25,564 lines Python 26,066 lines Python 4,957 lines bash

² The root image stream now ships behind an outer whole-stream zstd pass (level 15, 128 MiB long-range window): the per-extent compression inside the send stream cannot reach its framing or cross-extent redundancy, and the one pass reclaims 11% of the stream — 398 MiB of ISO. The layer landed on #113's branch after its column was measured; the same delta applies there.

³ Two opposing moves inside this number, both measured on the same rig the same day. The zstd layer made the medium read smaller: the installer VM's kernel clock at power-down went from 21.0 s to 18.4 s when the layer landed. The phases moving into their own systemd units (13 oneshot units, each a fresh process that reloads the library and rebuilds context) added ~5 s over the in-process loop — visible only on KVM, where the whole install is seconds; on the X200 the same overhead vanishes inside a read-bound install a hundred times longer. A shared context daemon or thinner per-phase reload could claw it back if it ever matters.

¹ The two image legs differ in cache state, not in orchestrator: the #113 run pressed install while the boot-time hash was still reading the stream, so the receive ran page-cache-warm; this branch's run began 46 minutes after boot with a cold cache, reading at the stick's raw ~16 MB/s — still 2.1× over pacstrap. Total wall for the run: 538 s / 323 s / 289 s. The pre-format verify gates cost 0.17 s on this run — the hashing hid entirely inside the time the user spent in the wizard.

The +0.56 GiB over quattro is the root-image stream's intrinsic cost (zstd:15 extents pass through btrfs receive untouched — no decompress/recompress, which is what buys the 3× install phase on slow hardware), already shaved by the outer stream pass; ~71 MB comes back from dropping Python. Installed-system boot is unchanged across all three; more than half of it is the loader delay common to every variant.

Verification

  • ./test/all: 16 shell suites + the vendored library's own config tests, green.
  • Integration (QEMU, per scenario run): unattended base install, corrupt-image, slow-medium, firmware-boot, pxe (netboot → NBD → install) — green on built ISOs at multiple points along the branch; the mirror-file layout was additionally proven with the payload manifest verified off the medium.
  • Interactive installs driven by hand at several heads, including the medium-eject flow at the finish screen.

🤖 Generated with Claude Code

hegjon and others added 30 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
Non-interactive guided install from archinstall's --config/--creds JSON,
scoped to what omarchy-iso's orchestrator uses: sfdisk partitioning,
LUKS2, btrfs subvolumes, pacstrap, users, locale, zram swap, network,
PipeWire/Bluetooth, Limine. Mirrors upstream ede2bd3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
Drop bootloader installation (Limine/UKI/Plymouth — the orchestrator does
it), key files for non-root LUKS partitions, snapper/timeshift, append-mode
partitioning, the NTP/reflector/WKD waits, nm/iwd network types, PulseAudio
and the extra minimal_installation options. Configurations that ask for
them now fail with a clear message. 2781 → 2327 lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
Keeps the parsed configuration and installer state in a 0600 file between
invocations so a driver in another language can run the install as the
sequence of calls archinstall's Installer exposes; `query` prints the
partitions, flags and kernel cmdline as JSON. installer_finish now prints
upstream's three-line completion banner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
The orchestrator now drives archinstall-bash (new submodule, shipped at
/usr/share/archinstall-bash) one archinstall-step process at a time;
archinstall_adapter.py is the only module that knows the command lines,
and `query` hands the Limine setup the partition nodes, PARTUUIDs and
kernel cmdline that used to come from Installer internals.

The Python archinstall package leaves the live ISO (python and kbd, which
it pulled in, are listed explicitly). keyboard.py goes: the library writes
the keymap the same way, with systemd-firstboot, inside
minimal-installation. The NTP/reflector/WKD sanity waits were always
skipped here and the port has none.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
mkarchiso copies the airootfs without preserving modes and applies
profiledef's file_permissions afterwards; the submodule copy in
build-iso.sh bypasses the lint that catches this for configs/airootfs,
so the step runner landed 0644 and the first phase died with EACCES.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
Comments and docstrings that still described it, and the
archinstall-language key the configurator wrote into every
user_configuration.json — it only ever picked the Python TUI's language.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
bash does not set $! for >(…), so the step runner (set -euo pipefail)
died with 'unbound variable' right after a successful pacstrap. Stream
through a plain pipe with a function-local pipefail instead; covered by
tests/test_step.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
main.sh runs the same fourteen phases in one shell, sourcing
archinstall-bash directly instead of spawning archinstall-step; the
modules map to the Python ones (context, phases, install, limine,
target_setup, provisioning, lifecycle, ui). Failures — fail(), the
library's die(), or any failing command under errexit — land in
state.json for the dashboard through the EXIT trap, which also does the
cleanup Python's finally did.

The unit tests move to bash (test/unit/orchestrator-*-test.sh, sharing
a harness that shadows arch-chroot/mount/findmnt/btrfs) and cover the
context, the phase runner's failure paths, provisioning, login, SSH,
Tailscale and the factory snapshot. python leaves the ISO package list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
They were the remaining reasons Python (plus ~20 python-* libraries,
~95 MiB uncompressed) was on the ISO after archinstall left: the cidata
autoinstall drive is read by omarchy-cidata-load, and reflector's
service was already removed for the offline install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
After cloud-init and reflector left, the rebuilt ISO still carried
python 3.14 — clonezilla (releng default) depends on it directly.
Nothing on the ISO clones disks; the partition tools it pulls in are
releng entries of their own and remain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
They let a driver in another language run the install one process at a
time; Omarchy's orchestrator is bash now and sources the library, and no
other driver exists. The sys_cmd_peek regression test moves to
tests/test_config.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
Redirections apply left to right, so an unprivileged caller saw
'Permission denied' from the open before 2>/dev/null took effect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
Both define info, error and list_contains; the library was sourced last
(from arch_load_library inside main), so from that point the
orchestrator's progress lines lost the indented shape the dashboard log
is written around. main.sh now sources the library first and ui.sh
wins; the dashboard's banner filter tolerates the indentation the
library's own lines pick up. The archinstall-step symlink goes with the
removed step runner (submodule bump).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
Restores the ordering coverage the Python suite had (a recording stub
of the library; full-disk, pre-mounted, optional steps, tailscale, a
failing step) and adds tests for the limine-entry-tool config parsers,
the efibootmgr snapshot/boot-order handling, /etc/default/limine
generation, the EFI file install and the package target / runtime
package list helpers. OMARCHY_ISO_SHARE makes the ISO's share directory
overridable for the latter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1eQiTQoDh5Jp6Cq7xmTyU
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
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
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
…pkill

Writing over a session with an install in flight tore the install's
units out from under it: the gate blamed the medium ("install medium is
corrupt") for a stop we caused, and the failure screen painted over the
console notice. The prepare now SIGSTOPs the dashboard and orchestrator
first -- a stopped process observes nothing, so nothing misleading lands
anywhere, and the session was sacrificial to begin with.

The first attempt froze itself: the whole prepare script rides in the
remote shell's cmdline, so pkill -f matched -- and stopped -- the shell
running it, mid-line (pkill spares itself, not its parent). Bracketing
one character of each pattern breaks the self-match, the same trick the
wait-loop pgreps already use.

Field-proven: a write fired into a running install left the console
showing only the notice, and the write streamed on undisturbed.

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

hegjon commented Aug 31, 2026

Copy link
Copy Markdown
Author

The install graph now fans out. After the provisioning phase, two branches run concurrently — the Limine phase (the UKI build, the longest tail phase) beside a user branch (user → login → ssh → tailscale → dns) — joining at validate-boot. ssh and tailscale stay ordered within their branch because both drive ufw; hibernation, system and provisioning keep their edges into Limine because they feed the initramfs.

Measured on the X200: the two branches open in the same second, the full user branch (~16 s) completes inside Limine's ~23 s window, and the join fires the moment Limine latches — the tail now costs what Limine alone costs, where the serial graph spent ~39 s.

The groundwork that made the flip itself a four-line unit edit:

  • Every chroot in a private mount namespace. arch-chroot mounts /proc, /dev and /sys under the target and tears them down on exit, so two concurrent chroots would unmount each other's API filesystems. A function shadowing the command name runs every invocation under unshare --mount --propagation private.
  • No parallel state document. The phase units latch (RemainAfterExit=yes), so systemd's own records are the install's state: the dashboard builds its roster by topologically sorting the Requires= edges in the unit files and polls one systemctl show per frame. state.json and the phase-error files are gone entirely.
  • Failure stories keyed per unit, race-free. A failing phase hands its own words to the journal under a dedicated identifier, each line prefixed with the unit name. Two measured journalctl lessons along the way: -u and -t don't compose (the -u expansion is a +-joined disjunction), and cgroup-based _SYSTEMD_UNIT attribution loses the race against the failing unit's teardown — hence identifier plus in-message prefix.
  • Progress by weights, not bands. Each phase owns a share of the bar; latched units contribute their full weight, running units their credit (time asymptote or work signal). On the linear graph this reproduces the old band walk position for position (pinned in a playback test); on the fan, concurrent phases push the bar together.
  • The installed-in time is the installer's own span: -----BEGIN/END OMARCHY INSTALL----- journal sentinels from the configurator handover to after the timing record, not the UI's lifetime.

Validated: the unit suite (23 tests), the QEMU scenario set (install, corrupt-image, slow-medium, stalled-medium, firmware-boot), repeated unattended X200 hardware installs driven by the new omarchy-iso-remote autoinstall, and interactive installs both on hardware (encrypted) and in QEMU. Boot-to-verdict on the X200 is ~4½ minutes.

I will continue cleaning this up and making it more systemd idiomatic, and I want to explore whether more of the units can run in parallel.

hegjon and others added 4 commits August 31, 2026 22:30
Round 9 caught the fixture's comment claiming more than the scenario
proved: a build that hashed the wrong or a stale file would mismatch on
the pristine ISO and on the corrupted copy alike, and every assertion
would still pass -- and in --reuse-base and standalone runs no install
ever boots the untouched ISO to catch it implicitly. The fixture now
proves the claim instead of retracting it: before flipping anything it
extracts the recorded digest and recomputes sha256 over the stream's
extent host-side, refusing with both digests named on mismatch.

Also makes the extent-failure diagnostic reachable: under set -e the
read off an empty pipe killed the scenario before the guard could say
what went wrong (round 9's second find, verified in a scratch shell on
both sides of the fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
Round 9's completeness note: a KeyboardInterrupt aimed at the
orchestrator's PID alone (the process-group kill and a terminal Ctrl-C
both reach zstd directly) could land inside the second Popen, and
`except Exception` would skip the half-built-pipeline cleanup on its way
out. The block re-raises, so BaseException swallows nothing. The new
test raises KeyboardInterrupt from the receive's spawn and asserts the
decompressor was reaped; reverting the one word fails exactly that test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
Emits the phase graph as Mermaid, derived with the dashboard roster's own
parse -- display names from ExecStart, Kahn order over the Requires=
edges -- so the drawing cannot drift from the graph systemd walks, and a
future edge change refreshes the picture with one command. Paste the
output into a GitHub comment inside a mermaid fence and it renders
inline.

Three edge kinds, all derived: solid Requires=+After= phase edges,
dashed After=-only ordering (the keyring before the factory snapshot),
dotted RequiresMountsFor= paths. The boot-started world rides along as
its own cluster (everything WantedBy=multi-user.target: the verifies,
the prefetch, the CPU boost) with its After= ordering -- and the
pre-flight gate's dependency on the verifies, which is no unit edge at
all, is drawn from the orchestrator's own *_VERIFY_UNIT constants as a
labeled "latched verdict read in-process" arrow. Unit names render in
full, split over three lines so the boxes stay narrow.

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

hegjon commented Aug 31, 2026

Copy link
Copy Markdown
Author

The install graph as it stands, generated from the unit files by the new bin/omarchy-systemd-graph (so the picture cannot drift from what systemd walks — one command refreshes it after any edge change):

%% omarchy install graph at 3ac59e9 -- generated by bin/omarchy-systemd-graph
flowchart LR
  classDef phase fill:#e3eee6,stroke:#2f7d4f,color:#20242a,font-family:monospace;
  classDef aux fill:#eceff3,stroke:#8b94a1,color:#20242a,font-family:monospace,stroke-dasharray:4 3;
  classDef boot fill:#fdf3e3,stroke:#a8722c,color:#20242a,font-family:monospace;

  subgraph boot["boot-started (WantedBy=multi-user.target)"]
    cpu_boost["omarchy-install-<br/>cpu-boost<br/>.service"]:::boot
    omarchy_mirror_verify["omarchy-<br/>mirror-verify<br/>.service"]:::boot
    omarchy_pacman_sync["omarchy-<br/>pacman-sync<br/>.service"]:::boot
    omarchy_prefetch["omarchy-<br/>prefetch<br/>.service"]:::boot
    omarchy_root_image_verify["omarchy-<br/>root-image-verify<br/>.service"]:::boot
  end
  omarchy_mirror_verify -. After= .-> omarchy_pacman_sync
  omarchy_mirror_verify -. After= .-> omarchy_prefetch
  omarchy_pacman_sync -. After= .-> omarchy_prefetch
  omarchy_root_image_verify -. After= .-> omarchy_prefetch
  omarchy_mirror_verify -. After= .-> omarchy_root_image_verify
  omarchy_pacman_sync -. After= .-> omarchy_root_image_verify

  prepare_live["omarchy-install-<br/>prepare-live<br/>.service"]:::phase
  prepare_target["omarchy-install-<br/>prepare-target<br/>.service"]:::phase
  disk["omarchy-install-<br/>disk<br/>.service"]:::phase
  image["omarchy-install-<br/>image<br/>.service"]:::phase
  strap["omarchy-install-<br/>strap<br/>.service"]:::phase
  base["omarchy-install-<br/>base<br/>.service"]:::phase
  hibernation["omarchy-install-<br/>hibernation<br/>.service"]:::phase
  system["omarchy-install-<br/>system<br/>.service"]:::phase
  provisioning["omarchy-install-<br/>provisioning<br/>.service"]:::phase
  user["omarchy-install-<br/>user<br/>.service"]:::phase
  limine["omarchy-install-<br/>limine<br/>.service"]:::phase
  login["omarchy-install-<br/>login<br/>.service"]:::phase
  ssh["omarchy-install-<br/>ssh<br/>.service"]:::phase
  tailscale["omarchy-install-<br/>tailscale<br/>.service"]:::phase
  dns["omarchy-install-<br/>dns<br/>.service"]:::phase
  validate_boot["omarchy-install-<br/>validate-boot<br/>.service"]:::phase
  factory_snapshot["omarchy-install-<br/>factory-snapshot<br/>.service"]:::phase

  omarchy_mirror_verify -. "latched verdict read in-process" .-> prepare_target
  omarchy_root_image_verify -. "latched verdict read in-process" .-> prepare_target

  prepare_live --> prepare_target
  prepare_target --> disk
  disk --> image
  image --> strap
  mnt_mnt_var_cache_pacman_pkg["/mnt/var/cache/pacman/pkg"]:::aux
  mnt_mnt_var_cache_pacman_pkg -. RequiresMountsFor .-> strap
  strap --> base
  base --> hibernation
  hibernation --> system
  system --> provisioning
  provisioning --> user
  provisioning --> limine
  user --> login
  login --> ssh
  ssh --> tailscale
  tailscale --> dns
  dns --> validate_boot
  limine --> validate_boot
  validate_boot --> factory_snapshot
  omarchy_target_keyring["omarchy-<br/>target-keyring<br/>.service"]:::aux
  omarchy_target_keyring -. "After= only" .-> factory_snapshot
Loading

Reading it: solid arrows are Requires=+After= pairs — a failure cancels everything downstream. The amber cluster is boot-started; the three verifies hash the medium one reader at a time and the pre-flight gate reads their latched verdicts in-process (no unit edge, hence the labeled arrows). Dashed arrows are ordering-only — the keyring can still be initializing when the snapshot phase starts, but never fails the install. Dotted RequiresMountsFor= is the strap's package-cache mount, released with plain umount(8) in the base phase because a systemctl stop inside the running transaction would cancel the chain.

hegjon and others added 13 commits August 31, 2026 23:33
Two moves of imperative orchestration into unit declarations, the last
two candidates from the idiomatic sweep:

The target's deferred-boot-hook mask around omarchy-apply-system was
bash bracketing with the orchestrator's exit trap as the failure net.
It is now the system unit's own ExecStartPre=/ExecStopPost= pair:
systemd runs the unmask on every exit path -- failure, group abort,
SIGKILL of the phase included -- which no trap can promise. The trap's
unmask call goes; the limine phase's repair-and-assert before handover
stays as the belt over these braces. The Pre/Post lines host their
functions through run-phase but stay out of every roster: the dashboard,
the timing record and the graph tool all anchor on ^ExecStart=.

The live-tree binds (mirror, /opt/packages) were systemctl starts inside
prepare_target_setup; the system and user units now declare them as
RequiresMountsFor=, the same shape the strap's package-cache mount
already has. The helper shrinks to the idempotent pacman.conf copy and
the cross-process memo variable disappears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
The audit found no dead code -- every variable, guard and marker has a
consumer -- but the comments carried the migration's history alongside
its conclusions. Kept: the measured rationales (the attribution race
behind the handover prefix, the loud-failure-not-Condition= boundary,
signals-are-teardown, the verified library-state edges). Dropped: the
narration around them. 133 lines to 97, behavior untouched, the test
extraction anchors intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
The RequiresMountsFor= migration carried a hidden hazard: Requires= from
a phase unit pulls the mount's start job in at transaction dispatch, and
with no ordering the binds mounted onto the live /mnt minutes before the
disk phase put the target there. Ordering after disk is not enough
either -- the image phase unmounts the whole target tree for the
subvolume swap and replays only the layout, sweeping any earlier bind
with it. The old prepare_target_setup start ran in the system phase,
after the swap, which is exactly the ordering that was load-bearing.
Found the loud way, twice: the user phase's bundled-Node lookup hit an
empty /opt/packages on both the dispatch-time and the after-disk builds.

One After=omarchy-install-image.service on each mount unit lands the
binds on the target's final tree. The strap's package-cache bind gets
the same edge: it has ridden the dispatch-time hazard since its own
migration -- swept by the swap, pacstrap silently lost the I/O dedupe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
The filtered tee in run-phase duplicated every phase's output into the
flat install log, stripped of the timestamps the journal had all along
-- X200 phase timing once had to be recovered from btrfs subvolume
otimes because of it. The tee goes: a phase unit's stdout is the
journal's, timestamped and per-unit queryable.

What each consumer keeps: the dashboard now authors the session log's
phase markers itself (one line per phase transition it observes, so the
support file keeps its human-readable spine and the autoinstall follow
keeps working); the failure screen's tail and every scenario assertion
already read dashboard- or chroot-authored lines, which stay in the
file; the chroot-side OMARCHY_INSTALL_LOG_FILE contract is untouched;
and phases_finalize exports the full timestamped journal onto the
installed system, so the target ends up with more than it had.
omarchy-iso-remote log fetches both halves.

Reference logs from a green pre-migration install are banked in
test-runs/log-reference/ for the completeness comparison.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
The last two mask duties leave the orchestrator's exit trap. The live
system's boot-image hooks were bracketed around the pacstraps in bash
with cleanup_live_hook_masks as the failure net; they are now the strap
unit's own ExecStartPre=/ExecStopPost= pair, the same shape the system
unit's target-side masks got -- systemd runs the unmask on every exit
path, SIGKILL included. stop_target_keyring_init goes too: the keyring
unit is PartOf=omarchy-install.target, so the group stop one line above
it already took it down. The trap is left with the group stop and the
protected-target release, which is all a trap should own.

Hygiene from an earlier sweep: omarchy-prefetch gains TimeoutStartSec=
1800 -- nothing waits on it, but a reader hung on a dying medium must
not linger unbounded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
Since the phases log to the journal alone, a failure left the session
log with the headline and nothing of the failing phase's own output: the
pacstrap rejection, the receive's error, systemd's Failed line. That is
the log "Upload log for support" (omarchy-upload-log, from the live
ISO's omarchy-settings) sends and the media diagnosis greps, so both
had gone quiet. The finalize export is now a function,
export_install_journal, and the exit trap appends the same section to
the live session log on every failure path, after the group stop has
flushed the phases' last words.

The END milestone moves ahead of the finalize so the journal copy
exported onto the installed system carries both ends of the span.

The media-diagnosis test's journalctl stub matched the query the
handover no longer makes; it answers the identifier query in the
line-prefixed form now, so the failed phase's words reach the screen
under test as they do in a real failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
The verifies, the prefetch, the pacman sync and the keyring unit are
the install's supporting cast, and a failure often starts in one of
them -- a medium the verify rejects is the first thing the export should
show. The unit glob widens to omarchy-*, in the finalize/failure export
and in omarchy-iso-remote's fetch of the same.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
*omarchy* rather than omarchy-*: the mirror mount units carry the name
in the middle, and a mount that failed to come up is as much a part of
the failure story as the verify that gated on it. Same glob in
omarchy-iso-remote's fetch. A JSON companion export for field-level
debugging is noted as a later stage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
The install mounts and unmounts constantly -- the stage binds, the
subvolume swap, the chroots' namespaces -- and systemd's record of the
mount units is where "target is busy" gets its explanation. A second
-u glob, *.mount, joins *omarchy* in the export and in
omarchy-iso-remote's fetch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
Catch-up with the branch this one grew out of: fifteen commits that came
from quattro (the tagline, the OCR/VNC test fixes, the release tooling,
the rc package channel, omarchy-iso-test-stop, the docker-through-sudo
fallback) and five of its own rounds on the receive path.

Three conflicts, resolved as follows.

phases_impl.py and test_root_image.py stay deleted: the orchestrator is
bash on this branch. Their two commits were about the receive pipeline
-- reaping a decompressor whose receive never started, and running the
same cleanup when a KeyboardInterrupt rather than an Exception leaves
the block. Neither hazard exists here: both children are started and
waited on unconditionally, a failed exec still closes its end of the
fifo, and an interrupt is answered by systemd tearing down the phase
unit's whole cgroup. The third intent -- naming both stages when both
of them failed -- is a real gap and follows in its own commit.

corrupt-image-test.sh takes the other branch's fixture. Flipping a
character of the recorded checksum, which is what this branch did,
proves the verify compares two strings; flipping a byte a megabyte deep
inside the stream extent proves it reads the medium, which is the
failure the scenario exists for. The extent is located through the
ISO9660 directory records, and the pristine bytes are checked against
the recorded digest before anything is damaged, so a build that hashed
a stale file fails the fixture instead of passing the test. The
assertions around it stay this branch's: the failing phase's words come
from the journal under the handover identifier, keyed by unit prefix.
test/integration gains libisoburn, which that fixture needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
Ported from the branch this one just merged, where the same fix landed
on the Python receive. A corrupt outer layer kills the decompressor and
starves the receive; a receive that dies first EPIPEs the decompressor.
Either way both children exit nonzero, and the exit codes do not say
which one broke the pipe -- so the old headline, which named btrfs
receive whenever it had failed, sent the reader after the wrong stage
half the time. It names both now, and the detail the two children share
disambiguates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVPbW91RD1JkdAK5dXxJab
cryptsetup picks the encryption sector from the drive: 4096 only on 4Kn
and 512e disks, 512 on anything reporting a 512-byte physical sector
(SATA SSDs, most NVMe at their factory LBA format, virtio). btrfs is
always at a 4096-byte sectorsize, so on those drives every filesystem
block was encrypted as eight separately tweaked XTS sectors. Ask for
4096 explicitly on both call sites: the configurator's direct luksFormat
(protected/free-space installs) and archinstall-bash's luks_format
(full-disk installs). Partitions are MiB-aligned, so cryptsetup never
has to revert to 512.

Measured 2026-09-02 while working on PR omacom#132, with its encrypted
unattended-install rig (same commit with and without the flag): QEMU
3+3 runs 51.8±1.4 s vs 51.0±0.9 s; X200 (Core 2 Duo, no AES-NI) 1+1
runs 359.2 s vs 355.9 s, root-image unpack through dm-crypt 112.8 s vs
111.5 s. No performance case; an alignment and consistency change kept
out of omacom#132 for its own discussion. Caveat: a fresh install then shows
sector 4096 in luksDump where existing systems show 512.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJaUDuCBipUhuP1YYfEyuP
--encrypt wrote the configurator's flag file and the password into the
credentials, but never a disk_config.disk_encryption block in the
archinstall JSON. The disk phase reads only the JSON, so it formatted
plain btrfs, while the boot validation reads the flag and expected LUKS:
every remote encrypted autoinstall halted at validate-boot with
"Encrypted install but /mnt/boot/limine.conf has no cryptdevice=" (X200,
2026-09-02). The validator was right; the tool was describing two
different installs.

Write the same block the configurator writes for a direct encrypted
install (luks, iter_time 2000, the root partition's obj_id, the
password), and nothing when --encrypt is not given.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJaUDuCBipUhuP1YYfEyuP
Format LUKS2 with a 4096-byte encryption sector on both call sites, per
the measurements in the merged commit and the discussion in omacom#147.

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

hegjon commented Sep 2, 2026

Copy link
Copy Markdown
Author

Fixed #147 in commit aaf64ec

hegjon and others added 4 commits September 2, 2026 23:44
dfd4989 untracked configs/airootfs/root/.ssh/authorized_keys, the public
key omarchy-iso-remote bakes into local test ISOs so the live system
answers root SSH. Keep it out of the tree for good: a recreated key
showed up as untracked, one `git add -A` away from being committed
again, and an ISO built from a tree carrying it answers root SSH to
that key on every machine that boots it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJaUDuCBipUhuP1YYfEyuP
Three comments still pointed at phases_impl.py and test_root_image.py,
which this branch removed with the Python orchestrator. Name what does
the work now (the target-keyring unit) or say "the former Python
orchestrator", so a grep for those files finds nothing dangling.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJaUDuCBipUhuP1YYfEyuP
Five simplifications found reviewing the port on top of PR omacom#113, no
behavior change, full unit suite green:

- boot_intent_rel() replaces four `boot_intent … | sed 's|^/||'` sites
  (install.sh, limine.sh) that forked a sed to drop one leading slash;
  the value is stripped with ${v#/} for joining onto $CTX_TARGET, while
  the four sites that want the absolute mountpoint keep plain boot_intent.
- root_image.sh replay_target_mounts: one `jq … @tsv` into a read loop
  instead of 1 + 4×N jq calls per mount row.
- root_image.sh root_image_target_mounts: the four .[0] fields read in a
  single jq pass instead of four (empty-array case still yields an empty
  target and the same failure).
- omarchy-iso-remote write: stream the ISO with `< "$iso"` instead of
  `cat "$iso" |`, one fewer process on the 6.4 GiB path.
- omarchy-iso-remote check-key: capture the xorriso+unsquashfs key
  extraction once instead of running it twice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJaUDuCBipUhuP1YYfEyuP
@hegjon
hegjon marked this pull request as ready for review September 3, 2026 11:56
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.

2 participants