Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
9c73ea3
Install the target from a btrfs root image instead of pacstrapping it
hegjon Aug 21, 2026
0cefda3
Force zstd compression at level 15 when building the root image
hegjon Aug 21, 2026
484cf7a
Drive the free-space install in omarchy-iso-test
hegjon Aug 21, 2026
fbbcdc1
Ship only the mirror packages the root image does not provide
hegjon Aug 21, 2026
6406d49
Validate the root image and the target layout before touching the disk
hegjon Aug 22, 2026
418c840
Prune and index the offline mirror before building the root image
hegjon Aug 22, 2026
01ea166
Restore the nproc-capped -smp in omarchy-iso-boot
hegjon Aug 22, 2026
145b786
Fail the build on an implausible target package count
hegjon Aug 22, 2026
c994369
Leave already-masked hooks alone in build-root-image.sh
hegjon Aug 22, 2026
5e99a67
Ship the root image as a plain file on the ISO
hegjon Aug 21, 2026
99e0a2e
Keep the pacman keyring out of the root image; create it per machine
hegjon Aug 22, 2026
f58d9e2
Run the target keyring init as a transient systemd unit
hegjon Aug 22, 2026
ce6b51f
Verify the root image in a systemd unit started at boot
hegjon Aug 23, 2026
8ef3a32
Put the re-flash advice on screen, and test a corrupt medium end to end
hegjon Aug 23, 2026
07fc5f0
Corrupt the root image itself in the corrupt-image scenario
hegjon Aug 23, 2026
8def994
Pin copytoram=n: the root image is streamed from the boot medium
hegjon Aug 23, 2026
10e2073
Put the boot medium on BFQ so the verify hash yields to the live system
hegjon Aug 23, 2026
30596b0
Gate the free-space install on the root-image verify, via one shared …
hegjon Aug 23, 2026
347c96c
Run the integration suite on legacy BIOS, and assert the boot firmware
hegjon Aug 23, 2026
059ee84
Commit the boot-menu default before console-login on BIOS
hegjon Aug 23, 2026
bf1fc98
Fix stale comment: the image compresses higher than the installer, no…
hegjon Aug 24, 2026
ec9a5c4
Pin copytoram=n on the loopback boot entries too
omarchybot Aug 24, 2026
e8a793d
Bound the verify hash with a build-time, size-based start timeout
hegjon Aug 24, 2026
edc065a
Pin copytoram=n on the PXE entries that can still install; drop HTTP
hegjon Aug 24, 2026
50cdf15
Track the verify hash's actual read position on the dashboard
hegjon Aug 24, 2026
20c37b7
Skip OVMF vars on a BIOS corrupt-image run; own up to the BFQ scope
hegjon Aug 24, 2026
f63e0e8
Show live hash progress while the free-space gate waits
hegjon Aug 25, 2026
b99fb88
Surface the verify percent on the dashboard's full-disk gate wait
hegjon Aug 25, 2026
640877a
Merge remote-tracking branch 'origin/quattro' into btrfs-image-install
hegjon Aug 25, 2026
6325a25
Guard the free-space path's verify gate against silent removal
omarchybot Aug 25, 2026
36c6849
Wait out the stop when the verify unit hits its start timeout
omarchybot Aug 25, 2026
eec294f
Integration scenario: a timed-out verify names the slow medium
hegjon Aug 25, 2026
d99c555
Let the install medium be removed at the Reboot Now screen
hegjon Aug 25, 2026
6fd6965
Draw the removal note above the button, where gum cannot erase it
hegjon Aug 25, 2026
8559a84
Release the target before offering the medium's removal
hegjon Aug 25, 2026
caf8fe5
Say what debugging actually loses: a pre-mount, not access
hegjon Aug 25, 2026
7affaa7
Swapoff the target's swapfile before the release umount
hegjon Aug 25, 2026
03479fe
Swapoff the target's swap in the protected failure cleanup too
hegjon Aug 25, 2026
e4ecf47
One release-the-target implementation, in bash, for both callers
hegjon Aug 25, 2026
544691a
Close the LUKS mappers in the target release too
hegjon Aug 25, 2026
2fc4322
Fold the stale-mapper fallback into the release script
hegjon Aug 25, 2026
54831ac
Drop a stale phrase: the cleanup's own sweep is gone
hegjon Aug 25, 2026
ad4df0a
Unmount a mapper's stray mountpoints before closing it
hegjon Aug 25, 2026
70fb704
Name the holders when the target release fails
hegjon Aug 25, 2026
1974a0b
Drop cms_verify from the PXE entries: unsigned ISOs cannot pass it
hegjon Aug 26, 2026
c65a7be
Add a PXE integration scenario: netboot, NBD medium, full install
hegjon Aug 26, 2026
10bdf42
Add omarchy-iso-boot-pxe: hands-on netboot of an ISO in a visible VM
hegjon Aug 26, 2026
bc14beb
Warm ttfx and gum too, not just systemctl
omarchybot Aug 26, 2026
e10840f
Close only crypt mappers: bound the sweep by dm UUID, not by trust
hegjon Aug 26, 2026
938f238
Rebuild the netboot TFTP cache when the ISO changes, not its name
hegjon Aug 26, 2026
f82c6d0
Round-5 tidyups: credit a real function, normalize pxe-test's mode
hegjon Aug 26, 2026
6394e38
Withhold the removal offer when the release fails; reboot gracefully
hegjon Aug 26, 2026
a253017
A mapper that stays open fails the release, and the offer with it
hegjon Aug 26, 2026
49995e8
Warm the finish window by dry-running it, measured to zero
hegjon Aug 26, 2026
96a19e2
Trim the warm's ttfx dry run to --version, and cat the logo
hegjon Aug 26, 2026
e9bff64
Share the netboot rig between the PXE scenario and its hands-on twin
hegjon Aug 26, 2026
4cf2fa8
Share the unattended-install wait between the base install and PXE
hegjon Aug 26, 2026
d7aba7b
A read error off the medium must not leave btrfs receive blocked
hegjon Aug 27, 2026
a8bdcaf
Cover the release sweep, and let device-mapper judge its own closes
hegjon Aug 27, 2026
f41d074
Cover the two things the release's verdict decides
hegjon Aug 27, 2026
c4d0614
Model the forgiven close: cryptsetup fails, the mapper is gone anyway
hegjon Aug 28, 2026
8329635
Wrap the root image stream in an outer zstd layer
hegjon Aug 28, 2026
af4329e
Reap a decompressor its receive never joined; name both broken stages
hegjon Aug 30, 2026
826a9bc
Corrupt the image bytes again, located by the ISO's own metadata
hegjon Aug 30, 2026
7577f67
Describe the corruption the scenario actually injects
omarchybot Aug 31, 2026
1f94df1
Prove the pristine stream verifies before corrupting it
hegjon Aug 31, 2026
759702d
Reap the decompressor on BaseException, not just Exception
hegjon Aug 31, 2026
0dcc9ba
Merge remote-tracking branch 'origin/quattro' into btrfs-image-install
hegjon Aug 31, 2026
f684071
Stop OCR-waiting on the free-space unencrypted confirm
omarchybot Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Omarchy ISO

The Omarchy ISO is the only supported way to install Omarchy. It ships the Omarchy Configurator, installs Arch Linux, installs the Omarchy packages from the bundled mirror, runs target system setup in the chroot, creates the user, and runs `omarchy-setup-user` for that user.
The Omarchy ISO is the only supported way to install Omarchy. It ships the Omarchy Configurator, unpacks a pre-built Arch Linux + Omarchy root image onto the target with `btrfs receive`, installs the per-machine packages (kernel, microcode, audio firmware) from the bundled mirror, runs target system setup in the chroot, creates the user, and runs `omarchy-setup-user` for that user.

## Downloading the latest ISO

Expand Down Expand Up @@ -130,6 +130,8 @@ Scenarios under `test/integration.d/` boot a real ISO install in QEMU and assert

The first scenario is `factory-reset`: it proves `omarchy-system-factory-reset` hands a machine on without destroying a shared ESP. The installed ESP gets a Windows entry with payload plus a second Linux cloned under a foreign machine-id with its own boot directory and UKIs; a real factory reset is then driven through a guest pty, and the harness asserts the foreign entries survive both the staged reset and first-boot provisioning, that the old Omarchy identity is fully retired, and that the machine reaches first-boot setup unattended.

`corrupt-image` proves a bad install medium is refused before the disk is touched. It copies the ISO and flips one byte a megabyte into the root image stream itself — addressed from the extent xorriso reports out of the ISO9660 directory records, so the damage lands in the shipped bytes and nowhere else — then autoinstalls from that copy: `omarchy-root-image-verify.service` (which hashes the image at boot, while a user would be in the configurator) must fail, the install must halt in its pre-flight phase with the "re-flash" advice on screen, and the target disk must still have no partition table. It boots the ISO itself rather than the base image, so it also works standalone with `OMARCHY_INTEGRATION_ISO` set.

Artifacts — screenshots, the fixtured/staged/final `limine.conf`, the reset typescript, and the factory-reset log — land under `test-runs/<iso>-integration/runs/<timestamp>-<scenario>/`, and `--no-preview` skips the `imv` review just like the acceptance harness.

## Signing the ISO
Expand Down
53 changes: 53 additions & 0 deletions bin/lib/netboot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Shared netboot rig: the TFTP root and NBD export that fake a PXE LAN
# inside QEMU's user-mode networking. Sourced by test/integration.d/
# pxe-test.sh (the scenario) and bin/omarchy-iso-boot-pxe (its hands-on
# twin) so the two can never drift apart — what the test proves is exactly
# what the tool boots.

NBD_PORT=10809 # not configurable: archiso_nbd_srv carries an address only
NBD_PID=""

# The ISO's own boot trees plus the one-line PXELINUX config entry point
# every real deployment writes: PXELINUX looks for pxelinux.cfg/default
# next to lpxelinux.0, and the INCLUDE routes it into the ISO's own config,
# whose whichsys.c32 line detects PXELINUX and switches to the PXE menu.
# Only the boot trees are extracted — the airootfs and the root image stay
# on the ISO and travel over NBD. Rebuilt when the ISO changes, keyed on
# size+mtime: a pathname key would keep serving a previous build's kernel
# and initramfs after a rebuild to the same release/*.iso name.
assemble_tftp_root() {
local iso="$1" root="$2" stamp
stamp="$iso $(stat -c '%s %Y' "$iso")"
if [[ -f "$root/.from-iso" && $(cat "$root/.from-iso") == "$stamp" ]]; then
return 0
fi
rm -rf "$root"
mkdir -p "$root"
bsdtar -xf "$iso" -C "$root" boot arch/boot
# The ISO's read-only modes would block the glue file below.
chmod -R u+w "$root"
mkdir -p "$root/boot/syslinux/pxelinux.cfg"
echo "INCLUDE syslinux.cfg" >"$root/boot/syslinux/pxelinux.cfg/default"
echo "$stamp" >"$root/.from-iso"
}

# Serve the ISO as the NBD export the initramfs dials back to: name
# 'archiso' on the well-known port, bound to the host loopback — which is
# where the guest's 10.0.2.2 (and therefore the hook's ${pxeserver}) lands.
# Sets NBD_PID; the caller's exit trap owns the kill.
serve_nbd() {
local iso="$1"
if [[ -n $(ss -Htln "sport = :$NBD_PORT") ]]; then
echo "Port $NBD_PORT is already taken (the port nbd-client dials is fixed); stop whatever serves it first." >&2
return 1
fi
qemu-nbd --read-only --persistent --export-name=archiso \
--bind=127.0.0.1 --port="$NBD_PORT" "$iso" &
NBD_PID=$!
sleep 1
if ! kill -0 "$NBD_PID" 2>/dev/null; then
NBD_PID=""
echo "qemu-nbd could not serve 127.0.0.1:$NBD_PORT." >&2
return 1
fi
}
139 changes: 139 additions & 0 deletions bin/omarchy-iso-boot-pxe
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/bin/bash
#
# Boot an Omarchy ISO over PXE in a visible QEMU VM: real display, real
# keyboard, no install medium attached. The LAN is faked the same way
# test/integration.d/pxe-test.sh does it — QEMU's built-in TFTP server hands
# iPXE lpxelinux.0 and the boot files out of the ISO's own trees, and
# qemu-nbd serves the ISO as the NBD export the initramfs dials back to (the
# guest reaches the host at 10.0.2.2). BIOS/SeaBIOS only: that is the ISO's
# netboot path. Drive the install by hand; after "Reboot Now" the VM boots
# the installed disk (the NBD server outlives the reboot).
#
# The PXE menu has no auto-boot timeout — press Enter on the NBD entry.

set -e

source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/lib/netboot.sh"

ISO=""
REUSE=""
SSH_PORT="${OMARCHY_VM_SSH_PORT:-2222}"
WORK="${OMARCHY_VM_PXE_DIR:-/tmp/omarchy-pxe-boot}"
EXTRA_QEMU_ARGS=()

usage() {
cat <<USAGE
Usage: omarchy-iso-boot-pxe [options] [release/omarchy.iso]

Options:
--reuse Reuse the existing VM disk
--ssh-port PORT Forward host PORT to guest port 22 (default: $SSH_PORT)
--work PATH Working directory for disk + TFTP root (default: $WORK)
-h, --help Show this help
-- Pass the remaining arguments directly to QEMU
USAGE
}

while (($# > 0)); do
case "$1" in
--reuse | reuse)
REUSE="reuse"
shift
;;
--ssh-port)
SSH_PORT="${2:-}"
shift 2
;;
--work)
WORK="${2:-}"
shift 2
;;
-h | --help)
usage
exit 0
;;
--)
shift
EXTRA_QEMU_ARGS+=("$@")
break
;;
*)
if [[ -z $ISO ]]; then
ISO="$1"
else
echo "Unexpected argument: $1" >&2
usage >&2
exit 1
fi
shift
;;
esac
done

if [[ -z $ISO && -t 0 ]]; then
ISO=$(ls -t release/*.iso 2>/dev/null | gum choose --header "Select ISO to netboot")
if gum confirm --default=false "Reuse existing disk?"; then
REUSE="reuse"
fi
fi

if [[ ! -f ${ISO:-} ]]; then
usage >&2
exit 1
fi
ISO=$(realpath "$ISO")

omarchy-pkg-add qemu-full

# QEMU needs the desktop session for its SDL window (same dance as
# omarchy-iso-boot).
if [[ -z ${WAYLAND_DISPLAY:-} && -z ${DISPLAY:-} ]]; then
while IFS='=' read -r name value; do
case "$name" in
WAYLAND_DISPLAY | DISPLAY | XAUTHORITY | XDG_SESSION_TYPE)
export "$name=$value"
;;
esac
done < <(systemctl --user show-environment 2>/dev/null || true)
fi

if [[ -z ${WAYLAND_DISPLAY:-} && -z ${DISPLAY:-} ]]; then
echo "Unable to find a graphical session for QEMU." >&2
echo "Run omarchy-iso-boot-pxe from the Omarchy desktop session." >&2
exit 1
fi

TFTP_ROOT="$WORK/tftp"
DISK="$WORK/disk.qcow2"
mkdir -p "$WORK"

echo "Assembling the TFTP root from $ISO (cached until the ISO changes)"
assemble_tftp_root "$ISO" "$TFTP_ROOT"

echo "Serving the ISO as NBD export 'archiso' on 127.0.0.1:$NBD_PORT"
serve_nbd "$ISO"
trap 'kill $NBD_PID 2>/dev/null || true' EXIT

if [[ ! -f $DISK || $REUSE != "reuse" ]]; then
qemu-img create -f qcow2 "$DISK" 40G
fi

echo "SSH forwarding enabled: ssh -p $SSH_PORT <user>@localhost"

# SeaBIOS, no pflash: BIOS is the ISO's only netboot path. The empty disk is
# first in the boot order so the post-install reboot lands on it; SeaBIOS
# falls through to the NIC while the disk has no boot sector.
qemu-system-x86_64 \
-cpu host -enable-kvm -machine q35,accel=kvm \
-smp "$(($(nproc) < 8 ? $(nproc) : 8))" \
-m 8192 \
-drive file="$DISK",format=qcow2,if=none,id=drive0 \
-device virtio-blk-pci,drive=drive0,bootindex=1 \
-device virtio-vga-gl \
-display sdl,gl=on \
-usb -device usb-tablet \
-serial none \
-netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$SSH_PORT-:22,tftp=$TFTP_ROOT,bootfile=/boot/syslinux/lpxelinux.0" \
-device virtio-net-pci,netdev=net0,bootindex=2 \
-boot menu=on \
"${EXTRA_QEMU_ARGS[@]}"
89 changes: 87 additions & 2 deletions bin/omarchy-iso-test
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# --encrypt Drive the encrypted install flow (default: unencrypted)
# --provision Drive the deferred-provisioning install flow: no user during install, the
# first boot runs omarchy-provision-owner and creates it there
# --free-space Install into the free space of a synthetic Windows-style
# disk (existing ESP + data partition kept) and verify the
# existing partitions survive
# --reuse-base Skip the install phase if a base image already exists
# --install-only Stop after producing the installed base image
# --sync-omarchy DIR Push DIR's test suite into the guest before running
Expand All @@ -40,6 +43,7 @@ INSTALL_TIMEOUT=2400
BOOT_TIMEOUT=600
ENCRYPT=false
PROVISION=false
FREE_SPACE=false
REUSE_BASE=false
INSTALL_ONLY=false
KEEP_RUNNING=false
Expand All @@ -55,6 +59,7 @@ while (($#)); do
case "$1" in
--encrypt) ENCRYPT=true ;;
--provision) PROVISION=true ;;
--free-space) FREE_SPACE=true ;;
--reuse-base) REUSE_BASE=true ;;
--install-only) INSTALL_ONLY=true ;;
--keep-running) KEEP_RUNNING=true ;;
Expand Down Expand Up @@ -92,6 +97,7 @@ OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
BASE_NAME="$(basename "$ISO" .iso)"
$ENCRYPT && BASE_NAME+="-encrypted"
$PROVISION && BASE_NAME+="-provision"
$FREE_SPACE && BASE_NAME+="-free-space"
BASE_DIR="$REPO_DIR/test-runs/$BASE_NAME"
RUN_DIR="$BASE_DIR/runs/$(date +%Y%m%d-%H%M%S)"
BASE_DISK="$BASE_DIR/base.qcow2"
Expand Down Expand Up @@ -696,6 +702,26 @@ drive_configurator() {
capture_console "success-installer-10-install-disk"
press ret # the single virtio disk

if $FREE_SPACE; then
# The picker only appears when the disk has usable free space on UEFI.
wait_for_screen "installation mode" 60
capture_console "success-installer-11-install-mode"
press down # "Free space install (alongside existing data)"
press ret
# BitLocker/ESP checks and the free-space analysis run, then the confirm.
wait_for_screen "of free space" 120
capture_console "success-installer-12-free-space-confirm-encrypted"
if ! $ENCRYPT; then
press ctrl-c # toggles the confirm to the unencrypted flow
# Same highlighted button OCR cannot read as the full-disk confirm below.
sleep 2
capture_console "success-installer-13-free-space-confirm-unencrypted"
fi
press ret
sleep 2
capture_console "success-installer-14-install-started"
return 0
fi
wait_for_screen "installation mode\|recovery possible" 60
if ocr_screen | grep -qi "installation mode"; then
capture_console "success-installer-11-install-mode"
Expand Down Expand Up @@ -981,13 +1007,71 @@ establish_session() {

# -------------------------------------------------------------------- phases

# A synthetic Windows-style disk for the free-space install: a FAT32 ESP and
# an ext4 data partition carrying a marker file, followed by ~76GiB of
# unallocated space. Built without root: parted partitions the raw file, the
# filesystems are made straight into it at their offsets. Unlike
# omarchy-iso-test-windows-disk it has no EFI/Microsoft directory (that needs
# mtools or a mount), so the configurator reports no Windows ESP; the
# free-space mechanics it exercises are the same either way.
create_free_space_disk() {
local disk="$1" raw="$1.raw" populate="$RUN_DIR/.preserve"
local esp_start_mib=1 esp_size_mib=512 data_end_mib=$((20 * 1024))

rm -f "$disk" "$raw"
truncate -s 96G "$raw"
parted --script "$raw" \
mklabel gpt \
mkpart WINDOWS_ESP fat32 "${esp_start_mib}MiB" "$((esp_start_mib + esp_size_mib))MiB" \
set 1 esp on \
mkpart WINDOWS_DATA ext4 "$((esp_start_mib + esp_size_mib))MiB" "${data_end_mib}MiB"

mkfs.fat -F32 -n WINDOWS_ESP -C "$RUN_DIR/.esp.img" $((esp_size_mib * 1024)) >/dev/null
dd if="$RUN_DIR/.esp.img" of="$raw" bs=1M seek="$esp_start_mib" conv=notrunc,sparse status=none
rm -f "$RUN_DIR/.esp.img"

rm -rf "$populate"
mkdir -p "$populate"
echo "This file must survive the Omarchy install." >"$populate/OMARCHY-MUST-NOT-TOUCH.txt"
mkfs.ext4 -q -F -L WINDOWS_DATA -d "$populate" \
-E offset=$(( (esp_start_mib + esp_size_mib) * 1024 * 1024 )) \
"$raw" "$(( (data_end_mib - esp_start_mib - esp_size_mib) * 1024 ))k"
rm -rf "$populate"

qemu-img convert -f raw -O qcow2 "$raw" "$disk"
rm -f "$raw"
}

# The partitions the free-space install had to leave alone, checked from
# inside the installed system.
verify_preserved_partitions() {
log "Verifying the pre-existing partitions survived"
local report="$RUN_DIR/preserved-partitions.txt"

ssh_guest "lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINTS" >"$report" 2>&1 || true
if ! ssh_guest "echo $GUEST_PASSWORD | sudo -S -p '' sh -c '
blkid -L WINDOWS_ESP >/dev/null &&
mkdir -p /run/preserve && mount -o ro LABEL=WINDOWS_DATA /run/preserve &&
test -f /run/preserve/OMARCHY-MUST-NOT-TOUCH.txt; rc=\$?; umount /run/preserve 2>/dev/null; exit \$rc'"; then
capture_console "failure-free-space-partitions-lost"
echo "Pre-existing partitions were not preserved; see $report" >&2
return 1
fi
log "WINDOWS_ESP and WINDOWS_DATA (with its marker file) are intact"
}

install_phase() {
log "Installing $(basename "$ISO") into a fresh VM (headless, driven via console)"

[[ -f $SSH_KEY ]] || ssh-keygen -t ed25519 -N "" -q -C "omarchy-iso-test" -f "$SSH_KEY"

rm -f "$BASE_DISK"
qemu-img create -f qcow2 "$BASE_DISK" 40G >/dev/null
if $FREE_SPACE; then
log "Building the synthetic Windows-style disk"
create_free_space_disk "$BASE_DISK"
else
rm -f "$BASE_DISK"
qemu-img create -f qcow2 "$BASE_DISK" 40G >/dev/null
fi
cp "$OVMF_VARS_TEMPLATE" "$BASE_OVMF"

start_vm "$BASE_DISK" "$RUN_DIR/install-serial.log" \
Expand All @@ -1003,6 +1087,7 @@ install_phase() {
unlock_luks
fi
bootstrap_ssh
$FREE_SPACE && verify_preserved_partitions

# SSH can come up (console bootstrap logs in on a spare TTY) while
# omarchy-provision-owner is still finalizing the user and re-keying LUKS. Saving
Expand Down
Loading