Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ad5bd60
Merge mac/41-deferred-first-session (#528) for the deferred hardware …
maralcbr Sep 25, 2026
1da3b47
Pin update-m1n1's device-tree order to the C locale
maralcbr Sep 25, 2026
9c8f04c
Accept an m1n1 image with the expected device trees in any order
maralcbr Sep 25, 2026
1855e11
Hand deferred hardware setup to omarchy-provision-hardware at first boot
maralcbr Sep 25, 2026
602266c
Stop first boot of a fresh image that deferred no hardware setup
maralcbr Sep 25, 2026
95d6d53
Disable rather than delete the hardware service once its queue is empty
maralcbr Sep 25, 2026
487f83b
Keep a factory reset from authorising another in-place encryption
maralcbr Sep 25, 2026
1304f81
Hold the hardware service while Mac first boot is pending
maralcbr Sep 25, 2026
186848e
Describe the Mac first-boot hand-off in the file layout
maralcbr Sep 25, 2026
e677df3
Let Mac first boot hand off when a deferred hardware step cannot finish
maralcbr Sep 25, 2026
af275f0
Say the hardware service is disabled, and who retires the manifest
maralcbr Sep 25, 2026
7237f6d
Check a first-boot hand-off leaves no error behind
maralcbr Sep 25, 2026
3a58abb
Merge mac/04-land-503 (#527) into mac/09-mac-boot-source
maralcbr Sep 25, 2026
e9c5ff1
Merge quattro-upstream into mac/09-mac-boot-source
maralcbr Sep 25, 2026
d46c1bf
Hand off Mac first boot only for a failed step, and retry it with the…
maralcbr Sep 25, 2026
2df61fd
Wait for NetworkManager before retrying deferred Mac hardware setup
maralcbr Sep 25, 2026
916d569
Merge branch 'quattro-upstream' into mac/09-mac-boot-source
maralcbr Sep 25, 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
15 changes: 11 additions & 4 deletions bin/omarchy-provision-hardware
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
# Steps run in order and each one that succeeds leaves the queue, so a failure
# stops the run and the next run resumes at that step. Once the queue is empty
# the initramfs is rebuilt if any step changed its configuration, and the
# service disarms itself. With nothing deferred this does nothing.
# service is disabled. With nothing deferred this does nothing. A step that
# fails exits 75 (the step stays queued for the next run); anything else that
# stops the run, such as an untrusted manifest or queue, exits 1.

set -euo pipefail

Expand Down Expand Up @@ -92,8 +94,10 @@ while (( ${#remaining[@]} )); do
echo "Skipping deferred hardware step $step: this Omarchy no longer ships it"
else
echo "Running deferred hardware step $step"
run_logged "$script" ||
fail "Deferred hardware step failed: $step (see $OMARCHY_INSTALL_LOG_FILE); the next run resumes here"
run_logged "$script" || {
echo "Error: Deferred hardware step failed: $step (see $OMARCHY_INSTALL_LOG_FILE); the next run resumes here" >&2
exit 75
}
fi

remaining=("${remaining[@]:1}")
Expand All @@ -112,7 +116,10 @@ if [[ $(initramfs_inputs) != "$(<"$omarchy_image_initramfs_baseline")" ]]; then
fi
rm -f "$omarchy_image_initramfs_baseline"

rm -f "$omarchy_image_systemd_dir/multi-user.target.wants/$omarchy_image_unit" "$omarchy_image_systemd_dir/$omarchy_image_unit"
# Disabled, not deleted: a platform first boot that runs this while the
# service's start job waits would otherwise fail that job at the next
# daemon-reload. The emptied queue keeps the unit from running.
rm -f "$omarchy_image_systemd_dir/multi-user.target.wants/$omarchy_image_unit"
rm -f "$omarchy_image_queue"
if (( EUID == 0 )); then
systemctl daemon-reload 2>/dev/null || true
Expand Down
10 changes: 6 additions & 4 deletions bin/omarchy-system-factory-reset
Original file line number Diff line number Diff line change
Expand Up @@ -432,14 +432,16 @@ sanitize_factory_baseline() {
btrfs property set -ts "$factory" ro true
}

# @factory must not carry first-boot markers or the ESP install.conf (v3 item
# 2e); the Boot partition's encrypt.state lives outside the subvolume and is
# reopened before committing the temporary key. The reset re-arms markers on
# the cloned next root, not on the sealed baseline.
# @factory must not carry first-boot markers, the fresh-image conversion token
# or the ESP install.conf (v3 item 2e); the Boot partition's encrypt.state lives
# outside the subvolume and is reopened before committing the temporary key.
# The reset re-arms markers on the cloned next root, not on the sealed baseline,
# and never the conversion token.
scrub_factory_boot_state() {
local root=$1
rm -f \
"$root/var/lib/omarchy/mac-first-boot/pending" \
"$root/var/lib/omarchy/mac-first-boot/deferred-steps" \
"$root/var/lib/omarchy/mac-first-boot/install.conf" \
"$root/var/lib/omarchy/provisioning/pending" \
"$root/var/lib/omarchy/provisioning/wipe-pending" \
Expand Down
2 changes: 1 addition & 1 deletion docs/file-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ On an encrypted install `omarchy-provision-owner` then re-keys LUKS from the sta

`omarchy-drive-password` reuses those journal helpers when the owner changes the password of the disk holding `/`: it changes the LUKS key, confirms the new key opens the disk and the old one no longer does, and only then sets the login and root passwords. `~/.local/state/omarchy/drive-password.state` records the disk's UUID, the key slots and the phase (never a password) until the accounts match, and the next run finishes an interrupted change with whichever of the old or new password the disk opens with.

Prebuilt images are set up away from the machine they will run on, so their hardware setup waits for that machine. The builder writes a root-owned manifest, `/var/lib/omarchy/image/target` (`format=1`, `platform=<omarchy-hw-platform value>`), before `omarchy-apply-system`. While it exists, `omarchy-apply-hardware` runs no hardware leaf: it queues each one in `/var/lib/omarchy/image/deferred-steps` and arms `omarchy-provision-hardware.service` (shipped from `install/provisioning/`). On the machine's first boot, before owner setup and the login screen, `bin/omarchy-provision-hardware` renames the manifest to `target.booted` and runs the queue in order, dropping each step that succeeds. A failed step stays queued for the next boot; once the queue is empty it rebuilds the initramfs if a step changed it and disarms the service, so later runs do nothing. `install/helpers/image-target.sh` holds the contract. The manifest is also the one source of an image's platform: while the root is being built, `omarchy-hw-platform` prints the manifest's platform and never reads the build host's device tree, so the initramfs HOOKS, services and package lists the build sets up are the target's. The root counts as built, not booted, when it shows it: no `/run/systemd/system` (arch-chroot, pacstrap and the image builder give it a `/run` of its own), PID 1's root is another one (a chroot), or PID 1 is not systemd (a PID namespace whose PID 1 is the build). What the caller cannot see, such as PID 1's root to a normal user or `/proc/1` behind `hidepid`, counts for a booted system, so a booted system always answers from its hardware, even with a stale or broken manifest, and builds run as root with a `/run` of their own. A caller in a private PID namespace looks like a build, so no unit that asks the detector uses `PrivatePIDs=`. Root always uses these fixed paths, and the detector restarts in an empty environment first; no environment variable turns a live system into an image build or changes the platform it reports.
Prebuilt images are set up away from the machine they will run on, so their hardware setup waits for that machine. The builder writes a root-owned manifest, `/var/lib/omarchy/image/target` (`format=1`, `platform=<omarchy-hw-platform value>`), before `omarchy-apply-system`. While it exists, `omarchy-apply-hardware` runs no hardware leaf: it queues each one in `/var/lib/omarchy/image/deferred-steps` and arms `omarchy-provision-hardware.service` (shipped from `install/provisioning/`). On the machine's first boot, before owner setup and the login screen, `bin/omarchy-provision-hardware` renames the manifest to `target.booted` and runs the queue in order, dropping each step that succeeds. A failed step stays queued for the next boot; once the queue is empty it rebuilds the initramfs if a step changed it and disables the service, so later runs do nothing. On a Mac, `omarchy-mac-first-boot` (from `omarchy-mac-boot`) runs `omarchy-provision-hardware` itself once the Mac's package keyring exists, and a drop-in holds the service until first boot is done; a step that cannot finish then (one that needs the network on an offline first boot) does not stop first boot but stays queued for the service, which the Mac drop-in has wait for NetworkManager to bring up its saved connections (`nm-online -s`; `network-online.target` waits for nothing on Omarchy, which masks `NetworkManager-wait-online.service`) (`omarchy-provision-hardware` exits 75 for such a step and 1 when it refuses to run at all, which does stop first boot); the service's unit file stays in place so its queued start is skipped on the emptied queue rather than failed. `install/helpers/image-target.sh` holds the contract. The manifest is also the one source of an image's platform: while the root is being built, `omarchy-hw-platform` prints the manifest's platform and never reads the build host's device tree, so the initramfs HOOKS, services and package lists the build sets up are the target's. The root counts as built, not booted, when it shows it: no `/run/systemd/system` (arch-chroot, pacstrap and the image builder give it a `/run` of its own), PID 1's root is another one (a chroot), or PID 1 is not systemd (a PID namespace whose PID 1 is the build). What the caller cannot see, such as PID 1's root to a normal user or `/proc/1` behind `hidepid`, counts for a booted system, so a booted system always answers from its hardware, even with a stale or broken manifest, and builds run as root with a `/run` of their own. A caller in a private PID namespace looks like a build, so no unit that asks the detector uses `PrivatePIDs=`. Root always uses these fixed paths, and the detector restarts in an empty environment first; no environment variable turns a live system into an image build or changes the platform it reports.

Current generated theme state lives under
`~/.local/state/omarchy/current/`. Keep `~/.config/omarchy/` for files a user
Expand Down
4 changes: 4 additions & 0 deletions docs/quattro-encryption-limine-source-port-2026-09-22.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,7 @@ Landed together with the generic re-key journal (#517) and the merge blockers fr
- `install/omarchy-apple.packages` names `omarchy-mac-boot`, which owner setup and factory reset require on Apple Silicon.

Merge criteria are the runtime suite, the boot package tests and these blockers. The complete candidate transaction and VM runs move to the signed candidate set and the first convergence install on the M2 Max; an M3 install is out of scope for the convergence.

## Deferred hardware setup at first boot (2026-09-25)

First boot no longer sources the Limine leaf. Once the Mac's package keyring exists it runs `omarchy-provision-hardware`, which works through the image's own queue in `/var/lib/omarchy/image/deferred-steps` (see `docs/file-layout.md`); `omarchy-provision-hardware.service` is ordered after first boot. `mac-first-boot/deferred-steps` stays the initramfs's fresh-image conversion token with the same contents, and a fresh image (token present) must also carry the build manifest `/var/lib/omarchy/image/target` (`target.booted` on a retry). The conversion gate never reads the image queue: `@factory` keeps that queue, so a factory reset restores it next to a re-armed first-boot marker. The reset also drops the conversion token from `@factory` and the next root.
7 changes: 4 additions & 3 deletions install/helpers/image-target.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
#
# While it exists, omarchy-apply-hardware queues each hardware leaf in
# /var/lib/omarchy/image/deferred-steps instead of running it, and arms
# omarchy-provision-hardware.service. On the machine's first boot that service
# retires the manifest (to target.booted) and runs the queue on the real
# hardware. Unknown manifest keys are ignored so the format can grow.
# omarchy-provision-hardware.service. On the machine's first boot that service,
# or the platform's own first boot, retires the manifest (to target.booted) and
# runs the queue on the real hardware. Unknown manifest keys are ignored so the
# format can grow.
#
# The manifest is also the only source of the image's platform: until that boot,
# omarchy-hw-platform reports its platform instead of the build host's, so the
Expand Down
3 changes: 2 additions & 1 deletion install/provisioning/omarchy-provision-hardware.service
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Runs the hardware setup an image build deferred, on the machine it was built
# for, before owner setup and the login screen. Armed with its queue by
# omarchy-apply-hardware during the build (install/helpers/image-target.sh);
# retried on every boot until the queue is empty, then it disarms itself.
# retried on every boot until the queue is empty, then it disables itself (the
# unit file stays; the emptied queue keeps it from running).
[Unit]
Description=Omarchy first-boot hardware setup deferred from the image build
ConditionPathExists=/var/lib/omarchy/image/deferred-steps
Expand Down
2 changes: 1 addition & 1 deletion packages/omarchy-mac/boot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This subtree is built into `omarchy-mac-boot`, separately from the kernel-neutral `omarchy-mac` hardware package. `../install` intentionally does not stage this directory. The boot recipe pins a runtime revision, the same one as the desktop candidate, and stages this subtree from it; the recipe keeps only packaging metadata and the pacman scriptlet.

`install DESTDIR` stages commands in `/usr/bin`, sourced modules/setup leaves in `/usr/lib/omarchy-mac/boot`, and the initramfs, in-place encryption and first-boot payload in `files/` at its installed paths (mkinitcpio drop-ins, initcpio hooks, units, presets and the ALPM hook). It performs no live setup. The recipe's `backup=` covers every `files/etc` path and `files/usr/lib/omarchy/initcpio`. `test/all` runs command fixtures and the payload's offline checks; `OMARCHY_DISPOSABLE_BOOT_TESTS=1` also builds real initramfs images and converts loop-device disks in privileged containers. Shared provisioning/reset integration remains covered by the runtime suite.
`install DESTDIR` stages commands in `/usr/bin`, sourced modules/setup leaves in `/usr/lib/omarchy-mac/boot`, and the initramfs, in-place encryption and first-boot payload in `files/` at its installed paths (mkinitcpio drop-ins, initcpio hooks, units, presets, the ALPM hook and `/etc/default/update-m1n1`, which pins update-m1n1's device-tree order to the C locale). It performs no live setup. The recipe's `backup=` covers every `files/etc` path and `files/usr/lib/omarchy/initcpio`. `test/all` runs command fixtures and the payload's offline checks; `OMARCHY_DISPOSABLE_BOOT_TESTS=1` also builds real initramfs images and converts loop-device disks in privileged containers. Shared provisioning/reset integration remains covered by the runtime suite.

The implementation was moved from runtime integration `e82fe3c8850b184dfa22eefd571646ad85446122`, preserving Marcelo's provenance recorded in the runtime source-port ledger. The payload in `files/` comes from the boot recipe paired with #503 (omarchy-mac/omarchy-pkgs-aarch64#65 at `617a907f99a1`) with maralcbr/omarchy-pkgs#202 applied (omarchy-mac-boot 20260921-10: the owner's keyboard layout and dock keyboards at the disk prompt). The owner wizard, recovery-slot operations, journal/retry handling and shared Limine menu/hash operations remain core code. Provisioning and reset refuse to proceed on Apple Silicon when their required package module is unavailable.

Expand Down
60 changes: 48 additions & 12 deletions packages/omarchy-mac/boot/bin/omarchy-apple-silicon-boot-check
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,8 @@ root_real=$(realpath -m "${root:-/}")
[[ $root_real == / ]] || m1n1_file=${m1n1_file#"$root_real"}
grep -Fxq -- "$m1n1_file" "$workdir/bootloader-files" || fail "M1N1 $M1N1 is not owned by $bootloader"

# Split and globbed the way update-m1n1's unquoted $DTBS is, in the collation
# pacman ran it under: C for omarchy update, else this session's locale.
# Split and globbed the way update-m1n1's unquoted $DTBS is, in the C collation
# /etc/default/update-m1n1 pins.
dtb_list() {
local LC_ALL=$1 words word matches
set -f
Expand Down Expand Up @@ -494,12 +494,37 @@ build_image() {
}

build_image "$workdir/boot.bin" "${dtbs[@]}"
expected=("$workdir/boot.bin")
mapfile -t session_dtbs < <(dtb_list "${LC_ALL:-${LC_COLLATE:-${LANG:-C}}}" 2>/dev/null)
if [[ ${session_dtbs[*]} != "${dtbs[*]}" ]]; then
build_image "$workdir/boot-session.bin" "${session_dtbs[@]}"
expected+=("$workdir/boot-session.bin")
fi

# The device trees in IMAGE from byte OFFSET on, one per flattened device tree
# header (magic, then its big-endian total size), named by the expected device
# tree with the same bytes. Fails unless they are exactly the expected ones,
# each as often, in whatever order.
dtb_order() {
local image=$1 offset=$2 end magic size hash dtb
local -A by_hash=() left=()
for dtb in "${dtbs[@]}"; do
hash=$(sha256sum <"$root$dtb")
hash=${hash%% *}
by_hash[$hash]=$dtb
left[$hash]=$(( ${left[$hash]:-0} + 1 ))
done
end=$(stat -c %s "$image")
while (( offset + 8 <= end )); do
magic=$(od -An -tx1 -j "$offset" -N4 "$image" | tr -d ' \n')
[[ $magic == d00dfeed ]] || break
size=$(od -An -tu4 --endian=big -j $(( offset + 4 )) -N4 "$image" | tr -d ' \n')
(( size >= 8 && offset + size <= end )) || return 1
hash=$(dd if="$image" iflag=skip_bytes,count_bytes skip="$offset" count="$size" bs=64K status=none | sha256sum)
hash=${hash%% *}
(( ${left[$hash]:-0} > 0 )) || return 1
left[$hash]=$(( left[$hash] - 1 ))
printf '%s\n' "${by_hash[$hash]}"
offset=$(( offset + size ))
done
for hash in "${!left[@]}"; do
(( left[$hash] == 0 )) || return 1
done
}

# The system ESP, found the way asahi-scripts' mount_sys_esp finds it. It is
# only ever read through a mount that is read-only from the start: a fresh one
Expand Down Expand Up @@ -549,10 +574,21 @@ report_running_release() {
echo "Apple Silicon boot check: running $kernel $kver; installed boot files match"
fi
}
for image in "${expected[@]}"; do
! sudo cmp -s "$image" "$mountpoint/m1n1/boot.bin" || {
if sudo cmp -s "$workdir/boot.bin" "$mountpoint/m1n1/boot.bin"; then
report_running_release
exit 0
fi
# An image update-m1n1 wrote before the C locale was pinned holds the same
# device trees in its caller's collation: rebuild it in the order it holds
# them and compare again.
sudo cat "$mountpoint/m1n1/boot.bin" >"$workdir/esp-boot.bin" ||
fail "cannot read m1n1/boot.bin on the system ESP ($esp)"
if esp_order=$(dtb_order "$workdir/esp-boot.bin" "$(stat -L -c %s "$root$M1N1")") && [[ -n $esp_order ]]; then
mapfile -t esp_dtbs <<<"$esp_order"
build_image "$workdir/boot-esp-order.bin" "${esp_dtbs[@]}"
if cmp -s "$workdir/boot-esp-order.bin" "$workdir/esp-boot.bin"; then
report_running_release
exit 0
}
done
fi
fi
fail "m1n1/boot.bin on the system ESP ($esp) is not m1n1, $kernel $kver's device trees, U-Boot and $CONFIG as installed; run sudo update-m1n1, then omarchy update"
7 changes: 7 additions & 0 deletions packages/omarchy-mac/boot/files/etc/default/update-m1n1
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Read by update-m1n1 (asahi-scripts) before it builds m1n1/boot.bin.
#
# update-m1n1 appends the device trees in the order its unquoted $DTBS glob
# expands, and that order follows the locale of whoever runs it: pacman's
# hook inherits the caller's. The C locale gives the same boot.bin however it
# is rebuilt, which the boot check can then reproduce byte for byte.
export LC_ALL=C
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,12 @@ omarchy_mac_encrypt_check_root_device() {
}

# Only a fresh image is encrypted: the image arms the first-boot marker and
# ships its deferred hardware steps; an installed Mac that gains this unit
# through a package update has neither (a migrated legacy first-boot marker
# comes without the steps file). A root that cannot be probed is not fresh.
# rescue=nologreplay keeps the probe from replaying the btrfs log.
# ships the mac-first-boot/deferred-steps token; an installed Mac that gains
# this unit through a package update has neither (a migrated legacy first-boot
# marker comes without the token). The image's hardware queue in
# /var/lib/omarchy/image is not the token: @factory keeps it, so a factory
# reset restores it along with a re-armed marker. A root that cannot be probed
# is not fresh. rescue=nologreplay keeps the probe from replaying the btrfs log.
omarchy_mac_encrypt_fresh_image() {
local mnt=/run/omarchy-mac-encrypt/probe fresh=1 dir
mkdir -p "$mnt"
Expand Down
Loading
Loading