Add OEM install mode and factory snapshots

OEM mode installs the entire system but defers user creation to first boot,
where the runtime's omarchy-oem-setup asks for the owner and lands them in the
already-installed system — and the same machinery arms omarchy-reset-computer.

The configurator's install-mode step gains an OEM choice, which required
moving the user step after mode selection (OEM skips it entirely). Autoinstall
rigs select OEM with an `oem` marker file on the cidata drive, which also
replaces the user_credentials.json requirement.

In the orchestrator, OEM swaps the user phases for a stage_oem_state phase:
it stashes the bundled Node tarball (every install, for offline factory
resets), arms omarchy-oem-setup.service in the target, and on encrypted
installs stages the throwaway LUKS passphrase — generated by InstallContext
when the input carries none — with a keyfile embedded in the UKI so boot
auto-unlocks until first-boot setup re-keys to the owner's password. The
install fails with a clear error if the bundled runtime lacks OEM support.

Every install now also ends with a read-only @factory snapshot of @ at the
btrfs top level — outside snapper's reach — which is what makes
omarchy-reset-computer a true factory reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier HanssonandClaude Fable 5 committed 2026-08-07 15:41:04 -07:00
1 parent 6004e80485
commit 679edce2e3
12 files changed
+927 -108

No files matched your search

+82 -23
View File
@@ -14,6 +14,8 @@
# Usage: omarchy-iso-test [release/omarchy.iso] [options]
#
# --encrypt Drive the encrypted install flow (default: unencrypted)
# --oem Drive the OEM install flow: no user during install, the
# first boot runs omarchy-oem-setup and creates it there
# --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/acceptance into the guest before running
@@ -36,6 +38,7 @@ MEMORY=8192
INSTALL_TIMEOUT=2400
BOOT_TIMEOUT=600
ENCRYPT=false
OEM=false
REUSE_BASE=false
INSTALL_ONLY=false
KEEP_RUNNING=false
@@ -50,6 +53,7 @@ GUEST_HOSTNAME="omarchy-test"
while (($#)); do
case "$1" in
--encrypt) ENCRYPT=true ;;
--oem) OEM=true ;;
--reuse-base) REUSE_BASE=true ;;
--install-only) INSTALL_ONLY=true ;;
--keep-running) KEEP_RUNNING=true ;;
@@ -86,6 +90,7 @@ OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
BASE_NAME="$(basename "$ISO" .iso)"
$ENCRYPT && BASE_NAME+="-encrypted"
$OEM && BASE_NAME+="-oem"
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"
@@ -580,59 +585,111 @@ drive_configurator() {
capture_console "success-installer-01-keyboard-layout"
press ret # English (US) is preselected
wait_for_screen "install disk" 120
capture_console "success-installer-02-install-disk"
press ret # the single virtio disk
wait_for_screen "installation mode" 60
capture_console "success-installer-03-install-mode"
if $OEM; then
press down # "OEM install" sits below "Full disk install" on a blank disk
fi
press ret
wait_for_screen "recovery possible" 60
capture_console "success-installer-04-disk-warning-encrypted"
if ! $ENCRYPT; then
press ctrl-c # toggles the confirm to the unencrypted flow
wait_for_screen "without encryption" 30
capture_console "success-installer-05-disk-warning-unencrypted"
fi
press ret
if $OEM; then
# OEM installs skip the user step entirely; the install starts right away.
sleep 2
capture_console "success-installer-13-install-started"
return 0
fi
wait_for_screen "Username" 120
type_text "$GUEST_USER"
capture_console "success-installer-02-username"
capture_console "success-installer-06-username"
press ret
wait_for_screen "Password" 60
type_text "$GUEST_PASSWORD"
capture_console "success-installer-03-password"
capture_console "success-installer-07-password"
press ret
wait_for_screen "Must match" 60
type_text "$GUEST_PASSWORD"
capture_console "success-installer-04-password-confirmation"
capture_console "success-installer-08-password-confirmation"
press ret
wait_for_screen "Full name" 60
type_text "Omarchy Test"
capture_console "success-installer-05-full-name"
capture_console "success-installer-09-full-name"
press ret
wait_for_screen "Email address" 60
type_text "test@omarchy.org"
capture_console "success-installer-06-email"
capture_console "success-installer-10-email"
press ret
wait_for_screen "Hostname" 60
type_text "$GUEST_HOSTNAME"
capture_console "success-installer-07-hostname"
capture_console "success-installer-11-hostname"
press ret
wait_for_screen "Timezone" 180 # tzupdate geo-guess can take a moment
capture_console "success-installer-08-timezone"
capture_console "success-installer-12-timezone"
press ret
wait_for_screen "look right" 60
capture_console "success-installer-09-review"
press ret
wait_for_screen "install disk" 60
capture_console "success-installer-10-install-disk"
press ret # the single virtio disk
wait_for_screen "recovery possible" 60
capture_console "success-installer-11-disk-warning-encrypted"
if ! $ENCRYPT; then
press ctrl-c # toggles the confirm to the unencrypted flow
wait_for_screen "without encryption" 30
capture_console "success-installer-12-disk-warning-unencrypted"
fi
capture_console "success-installer-13-review"
press ret
sleep 2
capture_console "success-installer-13-install-started"
capture_console "success-installer-14-install-started"
}
# The OEM first boot runs the user form on tty1 (omarchy-oem-setup) before
# SDDM. Encrypted OEM installs auto-unlock from the staged keyfile, so there
# is no LUKS prompt to answer here either way.
drive_oem_setup() {
log "Driving the OEM first-boot setup"
wait_for_screen "Username" 600
capture_console "success-oem-01-username"
type_text "$GUEST_USER"
press ret
wait_for_screen "Password" 60
type_text "$GUEST_PASSWORD"
capture_console "success-oem-02-password"
press ret
wait_for_screen "Must match" 60
type_text "$GUEST_PASSWORD"
press ret
wait_for_screen "Full name" 60
type_text "Omarchy Test"
press ret
wait_for_screen "Email address" 60
type_text "test@omarchy.org"
capture_console "success-oem-03-email"
press ret
wait_for_screen "look right" 60
capture_console "success-oem-04-review"
press ret
# Finalization (offline Node unpack, LUKS re-key on encrypted installs)
# runs before SDDM appears.
capture_console "success-oem-05-finalizing"
}
wait_for_install() {
@@ -835,7 +892,9 @@ install_phase() {
drive_configurator
wait_for_install
if $ENCRYPT; then
if $OEM; then
drive_oem_setup
elif $ENCRYPT; then
unlock_luks
fi
bootstrap_ssh
+2 -1
View File
@@ -117,4 +117,5 @@ rm -f /run/omarchy-install/state.json
--email-file /root/user_email_address.txt \
--encrypt-file /root/user_encrypt_installation.txt \
--authorized-keys-file /root/authorized_keys \
--tailscale-authkey-file /root/tailscale_authkey
--tailscale-authkey-file /root/tailscale_authkey \
--oem-file /root/oem
+105 -45
View File
@@ -136,7 +136,7 @@ Ukrainian|ua'
keyboard_form
# STEP 2: USER
# STEP 2: USER (run after the install mode is chosen — OEM installs skip it)
user_form() {
step "Let's setup your user account..."
@@ -197,11 +197,12 @@ user_form() {
fi
}
user_form
user_step() {
user_form
while true; do
# Add manual padding since gum table -p doesn't respect padding
echo -e "Field,Value
while true; do
# Add manual padding since gum table -p doesn't respect padding
echo -e "Field,Value
Username,$username
Password,$(printf "%${#password}s" | tr ' ' '*')
Full name,${full_name:-[Skipped]}
@@ -209,16 +210,17 @@ Email address,${email_address:-[Skipped]}
Hostname,$hostname
Timezone,$timezone
Keyboard,$keyboard" |
gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/"
gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/"
echo
if gum confirm --negative "No, change it" "Does this look right?"; then
break
else
keyboard_form
user_form
fi
done
echo
if gum confirm --negative "No, change it" "Does this look right?"; then
break
else
keyboard_form
user_form
fi
done
}
# STEP 3: DISK
@@ -347,11 +349,22 @@ wait_for_device() {
# Save the configurator answers the installer consumes: git identity and
# encryption choice as plain files, account details as archinstall credentials.
# OEM installs defer the user to first boot: no credentials, and on encrypted
# targets the orchestrator generates a throwaway LUKS passphrase itself.
write_user_files() {
echo "$full_name" >user_full_name.txt
echo "$email_address" >user_email_address.txt
printf '%s\n' "$encrypt_installation" >user_encrypt_installation.txt
if $oem_install; then
cat <<-_EOF_ >user_credentials.json
{
"users": []
}
_EOF_
return
fi
local password_escaped password_hash_escaped username_escaped
password_escaped=$(echo -n "$password" | jq -Rsa)
password_hash_escaped=$(echo -n "$password_hash" | jq -Rsa)
@@ -413,10 +426,10 @@ not_enough_space() {
return 1
}
run_partition_install() {
local dry=false
[[ "${1:-}" == "dry" ]] && dry=true
# Decide half of the free-space install: checks, free-space analysis, and the
# encryption confirm. Sets the globals run_partition_execute needs. Returns 1
# to fall back to the install-mode picker.
run_partition_decide() {
step "Checking for BitLocker on $disk"
local bl_part
bl_part=$(detect_bitlocker || true)
@@ -448,7 +461,8 @@ run_partition_install() {
# An unlabeled disk has no partition table for parted to scan, so we
# synthesize a whole-disk free region and mklabel gpt before mkpart.
local FREE_SPACE_INFO needs_mklabel=false pt_type disk_size_b
local FREE_SPACE_INFO pt_type disk_size_b
needs_mklabel=false
pt_type=$(lsblk -dno PTTYPE "$disk" 2>/dev/null)
if [[ -z "$pt_type" ]]; then
disk_size_b=$(lsblk -bdno SIZE "$disk" 2>/dev/null)
@@ -491,7 +505,6 @@ run_partition_install() {
local EFI_SIZE_B=$((2 * gib))
local MIN_INSTALL_B=$((32 * gib)) # Minimum total Omarchy allocation
local EFI_START_B EFI_END_B ROOT_START_B ROOT_END_B
EFI_START_B=$(( (FREE_START_B + ALIGN - 1) / ALIGN * ALIGN ))
EFI_END_B=$((EFI_START_B + EFI_SIZE_B))
ROOT_START_B=$(( (EFI_END_B + 1 + ALIGN - 1) / ALIGN * ALIGN ))
@@ -554,7 +567,6 @@ run_partition_install() {
esac
done
local esp_mount_in_target
if [[ "$encrypt_installation" == "true" ]]; then
esp_mount_in_target=/boot
else
@@ -563,12 +575,11 @@ run_partition_install() {
kernel_choice=$(detect_kernel)
local last_part efi_part_num root_part_num
local last_part
last_part=$(lsblk -n -o PARTN "$disk" 2>/dev/null | grep -E '^[0-9]+$' | sort -n | tail -1)
efi_part_num=$(( ${last_part:-0} + 1 ))
root_part_num=$(( efi_part_num + 1 ))
local efi_dev root_partition_device root_mapper
efi_dev=$(partition_path "$disk" "$efi_part_num")
root_partition_device=$(partition_path "$disk" "$root_part_num")
if [[ "$encrypt_installation" == "true" ]]; then
@@ -576,6 +587,14 @@ run_partition_install() {
else
root_mapper="$root_partition_device"
fi
}
# Execute half of the free-space install: partition, format, mount, and write
# the pre-mounted (protected) install configuration. Runs after the user step
# so LUKS can be formatted with the user's password.
run_partition_execute() {
local dry=false
[[ "${1:-}" == "dry" ]] && dry=true
if $dry; then
$needs_mklabel && say "[dry] would initialize GPT on $disk"
@@ -634,10 +653,6 @@ run_partition_install() {
mount "$efi_dev" /mnt"$esp_mount_in_target"
fi
clear
write_user_files
local luks_uuid_json="null" luks_uuid
if [[ "$encrypt_installation" == "true" ]]; then
if $dry; then
@@ -716,10 +731,6 @@ run_partition_install() {
"version": "3.0.9"
}
_EOF_
if $dry; then
print_dry_run_files
fi
}
disk_form() {
@@ -771,8 +782,7 @@ open_partition_tool() {
}
# If the disk has no partition table or no unallocated free space, the only
# viable path is a full-disk install. Skip the install-mode picker and go
# straight to confirmation.
# viable paths write the whole disk, so the free-space mode is not offered.
requires_full_disk_install() {
local pt_type free_space_b
pt_type=$(lsblk -dno PTTYPE "$disk" 2>/dev/null)
@@ -799,9 +809,12 @@ install_mode_form() {
# The free-space/protected path registers an EFI boot entry and is UEFI-only.
# Full-disk installs still support BIOS through the orchestrator's BIOS branch.
if [[ -d /sys/firmware/efi ]]; then
if [[ -d /sys/firmware/efi ]] && ! $full_disk_only; then
choices+=("Free space install (alongside existing data)")
fi
# OEM: install the whole system now, defer user setup to the machine's
# first boot — for machines built to be sold or handed on.
choices+=("OEM install (new owner creates their user on first boot)")
choices+=("Choose a different disk")
step "Let's select how to install Omarchy..."
@@ -846,30 +859,41 @@ confirm_disk_overwrite() {
done
}
# Decide what and how to install: sets install_target (full_disk|free_space),
# oem_install, and encrypt_installation. Nothing is written to disk yet — the
# user step (skipped for OEM) runs between this and the execution below.
select_installation() {
local full_disk_only
while true; do
if requires_full_disk_install; then
full_disk_only=true
install_mode="Full disk install"
else
full_disk_only=false
install_mode_form
fi
install_mode_form
case "$install_mode" in
"Full disk install")
oem_install=false
if confirm_disk_overwrite; then
install_target="full_disk"
return 0
fi
if $full_disk_only; then
disk_form
fi
;;
"Free space install (alongside existing data)")
if run_partition_install "$@"; then
exit 0
"OEM install"*)
oem_install=true
if confirm_disk_overwrite; then
install_target="full_disk"
return 0
fi
oem_install=false
;;
"Free space install"*)
oem_install=false
if run_partition_decide; then
install_target="free_space"
return 0
fi
;;
"Choose a different disk")
@@ -879,13 +903,41 @@ select_installation() {
done
}
# STEP 3: WHERE AND HOW TO INSTALL
oem_install=false
install_target="full_disk"
disk_form
select_installation "$@"
select_installation
# STEP 4: USER (OEM installs defer this to the machine's first boot)
if $oem_install; then
username=""
password=""
password_hash=""
full_name=""
email_address=""
hostname="omarchy"
timezone="UTC"
else
user_step
fi
clear
write_user_files
if [[ $install_target == "free_space" ]]; then
run_partition_execute "$@"
if [[ ${1:-} == "dry" ]]; then
print_dry_run_files
fi
exit 0
fi
# Setup partition layout
disk_size=$(lsblk -bdno SIZE "$disk")
mib=$((1024 * 1024))
@@ -902,14 +954,21 @@ main_partition_size=$((disk_size_in_mib - main_partition_start - gpt_backup_rese
kernel_choice=$(detect_kernel)
if [[ $encrypt_installation == true ]]; then
# OEM installs carry no password: the orchestrator generates a throwaway
# LUKS passphrase and stages it for the first-boot re-key.
if $oem_install; then
encryption_password_line=""
else
encryption_password_line=",
\"encryption_password\": $(echo -n "$password" | jq -Rsa)"
fi
disk_encryption_config=$(cat <<-_EOF_
,
"disk_encryption": {
"encryption_type": "luks",
"lvm_volumes": [],
"iter_time": 2000,
"partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ],
"encryption_password": $(echo -n "$password" | jq -Rsa)
"partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ]$encryption_password_line
}
_EOF_
)
@@ -927,6 +986,7 @@ cat <<-_EOF_ >user_configuration.json
"custom_commands": [],
"omarchy_install": {
"mode": "full_disk",
"oem": $oem_install,
"target_mount": "/mnt",
"boot": {
"esp_mount": "/boot",
@@ -11,6 +11,9 @@
# Anything less than the pair means this isn't an autoinstall drive, and the
# caller falls back to the wizard.
#
# An `oem` marker file stands in for user_credentials.json: OEM installs defer
# user creation to first boot, so imaging rigs ship no credentials at all.
#
# The optional argument prefixes every path the script touches; tests use it to
# run against a sandbox instead of the live system.
@@ -36,9 +39,9 @@ mkdir -p "$mnt"
mount -o ro "$device" "$mnt" || exit 1
loaded=1
if [[ -f $mnt/user_configuration.json && -f $mnt/user_credentials.json ]]; then
files=("$mnt"/user_configuration.json "$mnt"/user_credentials.json)
for file in user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys tailscale_authkey; do
if [[ -f $mnt/user_configuration.json ]] && [[ -f $mnt/user_credentials.json || -f $mnt/oem ]]; then
files=("$mnt"/user_configuration.json)
for file in user_credentials.json oem user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys tailscale_authkey; do
[[ -f $mnt/$file ]] && files+=("$mnt/$file")
done
if cp "${files[@]}" "$dest"/; then
@@ -321,11 +321,13 @@ install_progress() {
"Installing Arch + Omarchy") lo=65 hi=700 tau=150 pkg_signal=1 ;;
"Configuring hibernation") lo=700 hi=720 tau=10 ;;
"Configuring system") lo=720 hi=850 tau=120 ;;
"Finalizing Limine boot") lo=850 hi=950 tau=90 ;;
"Staging OEM state") lo=850 hi=855 tau=5 ;;
"Finalizing Limine boot") lo=855 hi=950 tau=90 ;;
"Finalizing user") lo=950 hi=978 tau=30 ;;
"Configuring login") lo=978 hi=988 tau=5 ;;
"Configuring DNS resolver") lo=988 hi=994 tau=5 ;;
"Validating boot setup") lo=994 hi=1000 tau=5 ;;
"Validating boot setup") lo=994 hi=998 tau=5 ;;
"Creating factory snapshot") lo=998 hi=1000 tau=5 ;;
*)
# Unknown names used to inherit a 1->99 sweep. Derive an even band from
# the published index instead; with no index, a bounded slice, so a
@@ -18,6 +18,7 @@ while [[ $# -gt 0 ]]; do
--encrypt-file) export OMARCHY_INSTALL_ENCRYPT_FILE="$2"; shift 2 ;;
--authorized-keys-file) export OMARCHY_INSTALL_AUTHORIZED_KEYS_FILE="$2"; shift 2 ;;
--tailscale-authkey-file) export OMARCHY_INSTALL_TAILSCALE_AUTHKEY_FILE="$2"; shift 2 ;;
--oem-file) export OMARCHY_INSTALL_OEM_FILE="$2"; shift 2 ;;
*) echo "omarchy-iso-install: unknown arg $1" >&2; exit 2 ;;
esac
done
@@ -6,6 +6,7 @@ from __future__ import annotations
import json
import os
import secrets
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@@ -25,6 +26,7 @@ class InstallContext:
user_credentials: dict
arch_config_path: Path
omarchy_install: dict[str, Any]
oem: bool = False
target: Path = Path("/mnt")
omarchy_path: Path = Path("/usr/share/omarchy")
@@ -48,10 +50,37 @@ class InstallContext:
user_configuration = json.loads(config_path.read_text())
omarchy_install = user_configuration.get("omarchy_install") or _default_omarchy_install(user_configuration)
# OEM mode: the whole system installs but user creation is deferred to
# first boot. Selected by the configurator (omarchy_install.oem) or by
# an `oem` marker file on an autoinstall drive, which also replaces the
# user_credentials.json requirement.
oem_marker = _optional_path(os.environ.get("OMARCHY_INSTALL_OEM_FILE"))
oem = bool(omarchy_install.get("oem")) or oem_marker is not None
omarchy_install["oem"] = oem
if creds_path.exists():
user_credentials = json.loads(creds_path.read_text())
elif oem:
user_credentials = {"users": []}
else:
raise RuntimeError(f"credentials file missing: {creds_path}")
arch_configuration = dict(user_configuration)
arch_configuration.pop("omarchy_install", None)
state_dir = Path(os.environ.get("OMARCHY_INSTALL_STATE_DIR", "/run/omarchy-install"))
state_dir.mkdir(parents=True, exist_ok=True)
if oem:
# Encrypted OEM installs have no user password to protect LUKS
# with. Generate a throwaway passphrase (staged for first-boot
# re-key by the stage_oem_state phase) and hand it to archinstall
# through both places it may look: the disk_encryption block and
# the credentials file.
_inject_oem_encryption_password(arch_configuration, user_credentials)
creds_path = state_dir / "oem-user_credentials.json"
creds_path.write_text(json.dumps(user_credentials, indent=2) + "\n")
creds_path.chmod(0o600)
arch_config_path = state_dir / "archinstall-user_configuration.json"
arch_config_path.write_text(json.dumps(arch_configuration, indent=2) + "\n")
@@ -64,9 +93,10 @@ class InstallContext:
authorized_keys_path=_optional_path(os.environ.get("OMARCHY_INSTALL_AUTHORIZED_KEYS_FILE")),
tailscale_authkey_path=_optional_path(os.environ.get("OMARCHY_INSTALL_TAILSCALE_AUTHKEY_FILE")),
user_configuration=user_configuration,
user_credentials=json.loads(creds_path.read_text()),
user_credentials=user_credentials,
arch_config_path=arch_config_path,
omarchy_install=omarchy_install,
oem=oem,
state_dir=state_dir,
)
disk_config = user_configuration.get("disk_config", {})
@@ -77,7 +107,12 @@ class InstallContext:
@property
def username(self) -> str:
return self.user_credentials["users"][0]["username"]
users = self.user_credentials.get("users") or []
if users:
return users[0]["username"]
if self.oem:
return ""
raise RuntimeError("user_credentials.json contains no users")
@property
def mode(self) -> str:
@@ -91,6 +126,24 @@ class InstallContext:
return self.mode == "protected"
def _inject_oem_encryption_password(arch_configuration: dict, user_credentials: dict) -> None:
"""Ensure an encrypted OEM install has a LUKS passphrase archinstall can
use. Reuse one the input already carries (an autoinstall rig may supply
its own); otherwise generate a throwaway. Mutates the disk_encryption
block in place — arch_configuration shares it with user_configuration, so
the stage_oem_state phase can read the effective passphrase back."""
disk_encryption = (arch_configuration.get("disk_config") or {}).get("disk_encryption")
if disk_encryption is None:
return
password = disk_encryption.get("encryption_password") or user_credentials.get("encryption_password")
if not password:
password = secrets.token_urlsafe(24)
disk_encryption["encryption_password"] = password
user_credentials["encryption_password"] = password
def _default_omarchy_install(user_configuration: dict) -> dict[str, Any]:
disk_config = user_configuration.get("disk_config", {})
mode = "protected" if disk_config.get("config_type") == "pre_mounted_config" else "full_disk"
@@ -34,6 +34,7 @@ def build_phases(ctx: InstallContext):
arch_install_system,
configure_hibernation,
run_system_finalizer,
stage_oem_state,
finalize_limine_boot,
run_chroot_finalizer,
configure_dns_resolver,
@@ -41,6 +42,7 @@ def build_phases(ctx: InstallContext):
configure_ssh_access,
configure_tailscale,
validate_boot,
create_factory_snapshot,
)
return [
@@ -49,6 +51,9 @@ def build_phases(ctx: InstallContext):
("Installing Arch + Omarchy", arch_install_system),
("Configuring hibernation", configure_hibernation),
("Configuring system", run_system_finalizer),
# Before finalize_limine_boot: the OEM cryptkey drop-in and keyfile
# must be in place for the final UKI build.
("Staging OEM state", stage_oem_state),
("Finalizing Limine boot", finalize_limine_boot),
("Finalizing user", run_chroot_finalizer),
("Configuring login", configure_login),
@@ -56,6 +61,7 @@ def build_phases(ctx: InstallContext):
("Configuring Tailscale", configure_tailscale),
("Configuring DNS resolver", configure_dns_resolver),
("Validating boot setup", validate_boot),
("Creating factory snapshot", create_factory_snapshot),
]
@@ -66,7 +72,8 @@ def main() -> int:
error(f"Configuration error: {e}")
return 2
info(f"Installing Omarchy for {ctx.username} → {ctx.target}")
who = ctx.username or "OEM (user created at first boot)"
info(f"Installing Omarchy for {who} → {ctx.target}")
from .phases_impl import (
boost_cpu_governor,
@@ -1136,16 +1136,140 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str
def run_system_finalizer(ctx: InstallContext) -> None:
if ctx.oem:
cmd = ["/usr/bin/omarchy-setup-system", "--oem", "--first-install"]
else:
cmd = ["/usr/bin/omarchy-setup-system", "--install-user", ctx.username, "--first-install"]
_mask_mkinitcpio_pacman_hooks(ctx, ctx.target, TARGET_DEFERRED_BOOT_HOOKS)
try:
_run_target_setup_command(
ctx,
["/usr/bin/omarchy-setup-system", "--install-user", ctx.username, "--first-install"],
)
_run_target_setup_command(ctx, cmd)
finally:
_unmask_mkinitcpio_pacman_hooks(ctx, ctx.target, TARGET_DEFERRED_BOOT_HOOKS)
# ─────────────────────────────────────────────────────────────────────────────
# stage_oem_state: produce the on-disk "OEM state" the runtime's first-boot
# setup (omarchy-oem-setup) and factory reset (omarchy-reset-computer) consume.
#
# Every install stashes the bundled Node tarball in /var/lib/omarchy/oem/
# so a later factory reset can finalize the new owner's user offline. OEM-mode
# installs additionally arm the first-boot setup service and, on encrypted
# targets, stage the throwaway LUKS passphrase: the keyfile embedded in the
# initramfs auto-unlocks boot during the OEM window, and first-boot setup
# re-keys the volume to the owner's password and removes it.
#
# Runs before finalize_limine_boot so the cryptkey cmdline drop-in and the
# keyfile land in the final UKI build.
# ─────────────────────────────────────────────────────────────────────────────
OEM_STATE_DIR = "var/lib/omarchy/oem"
OEM_KEYFILE = "etc/omarchy/oem.key"
NODE_PACKAGES_DIR = Path("/opt/packages")
def stage_oem_state(ctx: InstallContext) -> None:
oem_dir = ctx.target / OEM_STATE_DIR
oem_dir.mkdir(parents=True, exist_ok=True)
oem_dir.chmod(0o700)
_stage_node_tarball(ctx, oem_dir)
if not ctx.oem:
return
service_src = ctx.target / "usr/share/omarchy/install/oem/omarchy-oem-setup.service"
setup_bin = ctx.target / "usr/bin/omarchy-oem-setup"
if not service_src.exists() or not setup_bin.exists():
raise RuntimeError(
"OEM install requested, but the installed Omarchy runtime does not ship "
"first-boot setup (omarchy-oem-setup + install/oem/omarchy-oem-setup.service). "
"Update the runtime package this ISO bundles before installing in OEM mode."
)
info("› arming first-boot OEM setup")
(oem_dir / "pending").touch()
unit_dst = ctx.target / "etc/systemd/system/omarchy-oem-setup.service"
unit_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(service_src, unit_dst)
wants_dir = ctx.target / "etc/systemd/system/multi-user.target.wants"
wants_dir.mkdir(parents=True, exist_ok=True)
link = wants_dir / "omarchy-oem-setup.service"
link.unlink(missing_ok=True)
link.symlink_to("/etc/systemd/system/omarchy-oem-setup.service")
if _oem_install_encrypted(ctx):
_stage_oem_luks_unlock(ctx, oem_dir)
def _stage_node_tarball(ctx: InstallContext, oem_dir) -> None:
tarballs = sorted(NODE_PACKAGES_DIR.glob("node-v*-linux-x64.tar.gz"))
if not tarballs:
if ctx.oem:
raise RuntimeError(
"no bundled Node tarball in /opt/packages — OEM first-boot setup "
"could not finalize the user offline"
)
info("› no bundled Node tarball to stash for factory reset; skipping")
return
packages_dir = oem_dir / "packages"
packages_dir.mkdir(parents=True, exist_ok=True)
target_tarball = packages_dir / tarballs[0].name
if not target_tarball.exists():
info("› stashing Node tarball for offline first-boot setup")
shutil.copy2(tarballs[0], target_tarball)
def _oem_install_encrypted(ctx: InstallContext) -> bool:
if _storage_intent(ctx).get("luks_uuid"):
return True
disk_encryption = (ctx.user_configuration.get("disk_config") or {}).get("disk_encryption")
if disk_encryption and disk_encryption.get("encryption_type", "luks") != "no_encryption":
return True
return ctx.encrypt
def _oem_encryption_password(ctx: InstallContext) -> str | None:
disk_encryption = (ctx.user_configuration.get("disk_config") or {}).get("disk_encryption") or {}
return disk_encryption.get("encryption_password") or ctx.user_credentials.get("encryption_password")
def _stage_oem_luks_unlock(ctx: InstallContext, oem_dir) -> None:
password = _oem_encryption_password(ctx)
if not password:
# Full-disk OEM installs get a generated passphrase injected by
# InstallContext; only a pre-mounted (rig-partitioned) LUKS target can
# land here, and it must hand over the passphrase it formatted with.
raise RuntimeError(
"OEM install on a pre-encrypted target requires the LUKS passphrase "
"in user_credentials.json (encryption_password) so first boot can re-key"
)
info("› staging LUKS auto-unlock for the OEM window")
# Byte-for-byte the slot passphrase: no trailing newline anywhere.
luks_key = oem_dir / "luks-key"
luks_key.write_text(password)
luks_key.chmod(0o600)
keyfile = ctx.target / OEM_KEYFILE
keyfile.parent.mkdir(parents=True, exist_ok=True)
keyfile.write_text(password)
keyfile.chmod(0o600)
cmdline_dropin = ctx.target / "etc/limine-entry-tool.d/99-omarchy-oem-unlock.conf"
cmdline_dropin.parent.mkdir(parents=True, exist_ok=True)
cmdline_dropin.write_text(
'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/oem.key"\n'
)
files_dropin = ctx.target / "etc/mkinitcpio.conf.d/99-omarchy-oem-key.conf"
files_dropin.parent.mkdir(parents=True, exist_ok=True)
files_dropin.write_text("FILES+=(/etc/omarchy/oem.key)\n")
def finalize_limine_boot(ctx: InstallContext) -> None:
"""Finalize Limine after target system setup has written all dynamic
boot drop-ins (hibernation, hardware quirks, protected-mode ESP settings).
@@ -1239,6 +1363,10 @@ def _limine_kernel_cmdline(config_text: str) -> str:
def run_chroot_finalizer(ctx: InstallContext) -> None:
if ctx.oem:
info("› OEM install: user finalization deferred to first boot")
return
_run_target_setup_command(
ctx,
["/usr/bin/omarchy-finalize-user", "--force", "--first-install"],
@@ -1285,29 +1413,32 @@ def configure_login(ctx: InstallContext) -> None:
)
autologin_conf = sddm_dir / "autologin.conf"
if ctx.encrypt:
if ctx.encrypt and not ctx.oem:
autologin_conf.write_text(
"[Autologin]\n"
f"User={ctx.username}\n"
"Session=omarchy.desktop\n"
)
else:
# OEM installs have no user yet; omarchy-oem-setup writes autologin
# and SDDM state at first boot once the owner exists.
autologin_conf.unlink(missing_ok=True)
state_dir = ctx.target / "var" / "lib" / "sddm"
state_dir.mkdir(parents=True, exist_ok=True)
(state_dir / "state.conf").write_text(
f"[Last]\nSession=omarchy.desktop\nUser={ctx.username}\n"
)
if not ctx.oem:
state_dir = ctx.target / "var" / "lib" / "sddm"
state_dir.mkdir(parents=True, exist_ok=True)
(state_dir / "state.conf").write_text(
f"[Last]\nSession=omarchy.desktop\nUser={ctx.username}\n"
)
subprocess.run(
["arch-chroot", str(ctx.target), "chown", "sddm:sddm",
"/var/lib/sddm", "/var/lib/sddm/state.conf"],
check=False, capture_output=True,
)
autologin = ctx.target / "etc" / "systemd" / "system" / "getty@tty1.service.d" / "autologin.conf"
autologin.unlink(missing_ok=True)
subprocess.run(
["arch-chroot", str(ctx.target), "chown", "sddm:sddm",
"/var/lib/sddm", "/var/lib/sddm/state.conf"],
check=False, capture_output=True,
)
subprocess.run(
["arch-chroot", str(ctx.target), "systemctl", "enable", "sddm.service"],
check=False, capture_output=True,
@@ -1326,24 +1457,36 @@ def configure_ssh_access(ctx: InstallContext) -> None:
return
keys = _authorized_keys(ctx.authorized_keys_path)
info(f"› installing {len(keys)} SSH key(s) for {ctx.username}")
ssh_dir = ctx.target / "home" / ctx.username / ".ssh"
ssh_dir.mkdir(parents=True, exist_ok=True)
ssh_dir.chmod(0o700)
if ctx.oem:
# No user to authorize yet. Stage the keys in OEM state for
# omarchy-oem-setup to install once first boot creates the owner, and
# still open the door (sshd + ufw) below.
info(f"› staging {len(keys)} SSH key(s) for the first-boot user")
oem_dir = ctx.target / OEM_STATE_DIR
oem_dir.mkdir(parents=True, exist_ok=True)
staged = oem_dir / "authorized_keys"
staged.write_text("".join(f"{key}\n" for key in keys))
staged.chmod(0o600)
else:
info(f"› installing {len(keys)} SSH key(s) for {ctx.username}")
authorized_keys = ssh_dir / "authorized_keys"
authorized_keys.write_text("".join(f"{key}\n" for key in keys))
authorized_keys.chmod(0o600)
ssh_dir = ctx.target / "home" / ctx.username / ".ssh"
ssh_dir.mkdir(parents=True, exist_ok=True)
ssh_dir.chmod(0o700)
# Ask the target for the uid rather than assuming the first user is 1000.
# Uncaptured so a failure shows up in the install log, and checked because
# a root-owned authorized_keys is one sshd refuses to read.
subprocess.run(
["arch-chroot", str(ctx.target), "chown", "-R",
f"{ctx.username}:{ctx.username}", f"/home/{ctx.username}/.ssh"],
check=True,
)
authorized_keys = ssh_dir / "authorized_keys"
authorized_keys.write_text("".join(f"{key}\n" for key in keys))
authorized_keys.chmod(0o600)
# Ask the target for the uid rather than assuming the first user is 1000.
# Uncaptured so a failure shows up in the install log, and checked because
# a root-owned authorized_keys is one sshd refuses to read.
subprocess.run(
["arch-chroot", str(ctx.target), "chown", "-R",
f"{ctx.username}:{ctx.username}", f"/home/{ctx.username}/.ssh"],
check=True,
)
info("› enabling sshd")
subprocess.run(
@@ -1550,6 +1693,34 @@ def validate_boot(ctx: InstallContext) -> None:
if ctx.is_protected:
_validate_pre_mounted_filesystems(ctx)
if ctx.oem:
_validate_oem_state(ctx)
def _validate_oem_state(ctx: InstallContext) -> None:
"""An OEM install that boots without a working first-boot setup is a
user-less brick; insist the armed state is complete before reboot."""
oem_dir = ctx.target / OEM_STATE_DIR
if not (oem_dir / "pending").exists():
raise RuntimeError(f"OEM install but {oem_dir / 'pending'} is missing")
link = ctx.target / "etc/systemd/system/multi-user.target.wants/omarchy-oem-setup.service"
if not link.is_symlink():
raise RuntimeError("OEM install but omarchy-oem-setup.service is not enabled")
if not list((oem_dir / "packages").glob("node-v*.tar.gz")):
raise RuntimeError(f"OEM install but no Node tarball staged in {oem_dir / 'packages'}")
if _oem_install_encrypted(ctx):
for required in (oem_dir / "luks-key", ctx.target / OEM_KEYFILE):
if not required.exists():
raise RuntimeError(f"encrypted OEM install but {required} is missing")
limine_conf_text = (
ctx.target / _boot_intent(ctx)["esp_mount"].lstrip("/") / "limine.conf"
).read_text()
if "cryptkey=rootfs:" not in limine_conf_text:
raise RuntimeError("encrypted OEM install but limine.conf has no cryptkey= for auto-unlock")
def _assert_boot_hooks_restored(ctx: InstallContext) -> None:
"""Never hand over a system whose UKI rebuild hook is still masked.
@@ -1607,6 +1778,62 @@ def _validate_pre_mounted_filesystems(ctx: InstallContext) -> None:
raise RuntimeError(f"{crypttab} missing LUKS UUID {storage['luks_uuid']}")
# ─────────────────────────────────────────────────────────────────────────────
# create_factory_snapshot: read-only snapshot of @ kept at the btrfs top level
# as @factory — outside snapper's .snapshots, so cleanup timers and the Limine
# snapshot menu never touch it. Zero bytes at creation; grows only with drift.
# Taken at the end of every install, it is what makes omarchy-reset-computer a
# true factory reset.
# ─────────────────────────────────────────────────────────────────────────────
def create_factory_snapshot(ctx: InstallContext) -> None:
fstype = _findmnt_value(ctx.target, "FSTYPE")
if fstype != "btrfs":
info(f"› target root is {fstype or 'unknown'}, not btrfs; skipping factory snapshot")
return
options = (_findmnt_value(ctx.target, "OPTIONS") or "").split(",")
if not any(opt in ("subvol=/@", "subvol=@") for opt in options):
info("› target root is not the @ subvolume; skipping factory snapshot")
return
device = (_findmnt_value(ctx.target, "SOURCE") or "").split("[")[0]
if not device:
raise RuntimeError(f"could not determine the btrfs device backing {ctx.target}")
top = ctx.state_dir / "factory-top"
top.mkdir(parents=True, exist_ok=True)
subprocess.run(["mount", "-o", "subvolid=5", device, str(top)], check=True)
try:
root_subvol = top / "@"
if not root_subvol.is_dir():
raise RuntimeError(f"no @ subvolume at the top level of {device}")
factory = top / "@factory"
if factory.exists():
subprocess.run(
["btrfs", "subvolume", "delete", str(factory)],
check=True, capture_output=True,
)
info("› snapshotting @ as @factory (read-only)")
subprocess.run(
["btrfs", "subvolume", "snapshot", "-r", str(root_subvol), str(factory)],
check=True,
)
finally:
subprocess.run(["umount", str(top)], check=False, capture_output=True)
def _findmnt_value(path: Path, column: str) -> str | None:
res = subprocess.run(
["findmnt", "-no", column, str(path)],
capture_output=True, text=True,
)
value = res.stdout.strip()
return value if res.returncode == 0 and value else None
CPU_SYSFS = Path("/sys/devices/system/cpu")
+29
View File
@@ -124,6 +124,35 @@ run_load || fail "required pair plus authorized_keys loads"
[[ ! -e $sandbox/root/tailscale_authkey ]] || fail "absent tailscale_authkey is not copied"
pass "present optional files are copied, absent ones skipped"
# An oem marker replaces user_credentials.json: OEM installs defer user
# creation to first boot, so imaging rigs ship no credentials at all.
new_sandbox
attach_drive cidata
echo '{"disk_config": {}}' >"$sandbox/media/user_configuration.json"
: >"$sandbox/media/oem"
run_load || fail "config plus oem marker loads"
[[ -f $sandbox/root/oem ]] || fail "oem marker is copied"
[[ ! -e $sandbox/root/user_credentials.json ]] || fail "oem drive copies no credentials"
pass "oem marker stands in for credentials"
# The oem marker and credentials can coexist (rig supplies its own LUKS
# passphrase in the credentials file); both are copied.
new_sandbox
attach_drive cidata
write_required_pair
: >"$sandbox/media/oem"
run_load || fail "oem marker plus credentials loads"
[[ -f $sandbox/root/oem && -f $sandbox/root/user_credentials.json ]] || fail "both oem marker and credentials are copied"
pass "oem marker plus credentials copies both"
# An oem marker without the configuration is still not an autoinstall drive.
new_sandbox
attach_drive cidata
: >"$sandbox/media/oem"
! run_load || fail "oem marker alone is not an autoinstall drive"
[[ ! -e $sandbox/root/oem ]] || fail "oem marker alone copies nothing"
pass "oem marker alone falls back to the wizard"
# Half the required pair is not an autoinstall drive: unmount and fall back.
new_sandbox
attach_drive cidata
+1 -1
View File
@@ -60,7 +60,7 @@ class ConfigureSshAccessTest(unittest.TestCase):
if authorized_keys is not None:
authorized_keys_path = self.target / "authorized_keys"
authorized_keys_path.write_text(authorized_keys)
return types.SimpleNamespace(target=self.target, username="jeff", authorized_keys_path=authorized_keys_path)
return types.SimpleNamespace(target=self.target, username="jeff", authorized_keys_path=authorized_keys_path, oem=False)
def configure(self, **kwargs):
phases_impl.configure_ssh_access(self.ctx(**kwargs))
+377
View File
@@ -0,0 +1,377 @@
"""Unit tests for OEM install support in the orchestrator.
Covers the InstallContext OEM plumbing (marker file, credential-less input,
throwaway LUKS passphrase injection) and the stage_oem_state /
create_factory_snapshot / configure_login phases against a temp target with
subprocess mocked out.
"""
import json
import os
import sys
import tempfile
import types
import unittest
from pathlib import Path
from subprocess import CompletedProcess
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "configs/airootfs/usr/share/omarchy-iso"))
sys.modules.setdefault(
"orchestrator.archinstall_adapter", types.ModuleType("orchestrator.archinstall_adapter")
)
from orchestrator import phases_impl # noqa: E402
from orchestrator.context import InstallContext # noqa: E402
class ContextOemTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.dir = Path(self.tmp.name)
self.state_dir = self.dir / "state"
self.env = {
"OMARCHY_INSTALL_CONFIG": str(self.dir / "user_configuration.json"),
"OMARCHY_INSTALL_CREDS": str(self.dir / "user_credentials.json"),
"OMARCHY_INSTALL_STATE_DIR": str(self.state_dir),
}
def write_config(self, config):
(self.dir / "user_configuration.json").write_text(json.dumps(config))
def write_creds(self, creds):
(self.dir / "user_credentials.json").write_text(json.dumps(creds))
def from_env(self, **extra_env):
env = {**self.env, **extra_env}
with mock.patch.dict(os.environ, env, clear=False):
for key in ("OMARCHY_INSTALL_OEM_FILE",):
if key not in env:
os.environ.pop(key, None)
return InstallContext.from_env()
def base_config(self, oem=None, disk_encryption=None):
config = {
"disk_config": {"config_type": "default_layout"},
"omarchy_install": {"mode": "full_disk", "target_mount": "/mnt"},
}
if oem is not None:
config["omarchy_install"]["oem"] = oem
if disk_encryption is not None:
config["disk_config"]["disk_encryption"] = disk_encryption
return config
def test_oem_flag_from_config(self):
self.write_config(self.base_config(oem=True))
ctx = self.from_env()
self.assertTrue(ctx.oem)
self.assertEqual(ctx.user_credentials, {"users": []})
self.assertEqual(ctx.username, "")
def test_oem_marker_file_arms_oem_without_credentials(self):
self.write_config(self.base_config())
marker = self.dir / "oem"
marker.touch()
ctx = self.from_env(OMARCHY_INSTALL_OEM_FILE=str(marker))
self.assertTrue(ctx.oem)
self.assertTrue(ctx.omarchy_install["oem"])
def test_absent_marker_file_is_not_oem(self):
self.write_config(self.base_config())
self.write_creds({"users": [{"username": "jeff"}]})
ctx = self.from_env(OMARCHY_INSTALL_OEM_FILE=str(self.dir / "missing-oem"))
self.assertFalse(ctx.oem)
self.assertEqual(ctx.username, "jeff")
def test_missing_credentials_without_oem_raises(self):
self.write_config(self.base_config())
with self.assertRaisesRegex(RuntimeError, "credentials file missing"):
self.from_env()
def test_oem_generates_throwaway_luks_passphrase(self):
self.write_config(self.base_config(oem=True, disk_encryption={
"encryption_type": "luks", "partitions": ["x"],
}))
ctx = self.from_env()
password = ctx.user_configuration["disk_config"]["disk_encryption"]["encryption_password"]
self.assertTrue(password)
self.assertEqual(ctx.user_credentials["encryption_password"], password)
# archinstall reads both files from disk; the injected passphrase must
# be present in each.
arch_config = json.loads(ctx.arch_config_path.read_text())
self.assertEqual(
arch_config["disk_config"]["disk_encryption"]["encryption_password"], password
)
synthesized = json.loads(ctx.creds_path.read_text())
self.assertEqual(synthesized["encryption_password"], password)
def test_oem_reuses_rig_supplied_passphrase(self):
self.write_config(self.base_config(oem=True, disk_encryption={
"encryption_type": "luks", "partitions": ["x"],
}))
self.write_creds({"users": [], "encryption_password": "rig-secret"})
ctx = self.from_env()
self.assertEqual(
ctx.user_configuration["disk_config"]["disk_encryption"]["encryption_password"],
"rig-secret",
)
def test_oem_unencrypted_touches_nothing(self):
self.write_config(self.base_config(oem=True))
ctx = self.from_env()
self.assertNotIn("encryption_password", ctx.user_credentials)
def make_ctx(target, **overrides):
defaults = dict(
target=target,
oem=True,
encrypt=False,
username="",
omarchy_install={"mode": "full_disk", "oem": True, "storage": {}},
user_configuration={"disk_config": {}},
user_credentials={"users": []},
state_dir=target / "state",
authorized_keys_path=None,
)
defaults.update(overrides)
return types.SimpleNamespace(**defaults)
class StageOemStateTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.target = Path(self.tmp.name) / "mnt"
self.target.mkdir()
info_patch = mock.patch.object(phases_impl, "info")
info_patch.start()
self.addCleanup(info_patch.stop)
# A fake bundled Node tarball on the "live ISO".
self.packages = Path(self.tmp.name) / "opt-packages"
self.packages.mkdir()
(self.packages / "node-v24.0.0-linux-x64.tar.gz").write_bytes(b"node")
node_patch = mock.patch.object(phases_impl, "NODE_PACKAGES_DIR", self.packages)
node_patch.start()
self.addCleanup(node_patch.stop)
def install_runtime_oem_support(self):
service = self.target / "usr/share/omarchy/install/oem/omarchy-oem-setup.service"
service.parent.mkdir(parents=True, exist_ok=True)
service.write_text("[Unit]\n")
setup_bin = self.target / "usr/bin/omarchy-oem-setup"
setup_bin.parent.mkdir(parents=True, exist_ok=True)
setup_bin.write_text("#!/bin/bash\n")
def oem_dir(self):
return self.target / "var/lib/omarchy/oem"
def test_non_oem_install_stages_only_the_node_tarball(self):
ctx = make_ctx(self.target, oem=False)
phases_impl.stage_oem_state(ctx)
self.assertTrue((self.oem_dir() / "packages/node-v24.0.0-linux-x64.tar.gz").exists())
self.assertFalse((self.oem_dir() / "pending").exists())
self.assertFalse((self.target / "etc/systemd/system/omarchy-oem-setup.service").exists())
def test_oem_against_runtime_without_support_fails_clearly(self):
ctx = make_ctx(self.target)
with self.assertRaisesRegex(RuntimeError, "does not ship"):
phases_impl.stage_oem_state(ctx)
def test_oem_without_node_tarball_fails(self):
for tarball in self.packages.glob("*.tar.gz"):
tarball.unlink()
self.install_runtime_oem_support()
ctx = make_ctx(self.target)
with self.assertRaisesRegex(RuntimeError, "Node tarball"):
phases_impl.stage_oem_state(ctx)
def test_oem_arms_first_boot_setup(self):
self.install_runtime_oem_support()
ctx = make_ctx(self.target)
phases_impl.stage_oem_state(ctx)
self.assertTrue((self.oem_dir() / "pending").exists())
self.assertTrue((self.target / "etc/systemd/system/omarchy-oem-setup.service").exists())
link = self.target / "etc/systemd/system/multi-user.target.wants/omarchy-oem-setup.service"
self.assertTrue(link.is_symlink())
self.assertEqual(
os.readlink(link), "/etc/systemd/system/omarchy-oem-setup.service"
)
# Unencrypted: no LUKS staging.
self.assertFalse((self.oem_dir() / "luks-key").exists())
def test_oem_encrypted_stages_luks_auto_unlock(self):
self.install_runtime_oem_support()
ctx = make_ctx(
self.target,
user_configuration={"disk_config": {"disk_encryption": {
"encryption_type": "luks",
"encryption_password": "throwaway-secret",
}}},
)
phases_impl.stage_oem_state(ctx)
# Byte-for-byte the slot passphrase — no trailing newline.
self.assertEqual((self.oem_dir() / "luks-key").read_text(), "throwaway-secret")
keyfile = self.target / "etc/omarchy/oem.key"
self.assertEqual(keyfile.read_text(), "throwaway-secret")
self.assertEqual(keyfile.stat().st_mode & 0o777, 0o600)
cmdline = (self.target / "etc/limine-entry-tool.d/99-omarchy-oem-unlock.conf").read_text()
self.assertIn("cryptkey=rootfs:/etc/omarchy/oem.key", cmdline)
files = (self.target / "etc/mkinitcpio.conf.d/99-omarchy-oem-key.conf").read_text()
self.assertIn("FILES+=(/etc/omarchy/oem.key)", files)
def test_oem_pre_encrypted_without_passphrase_fails(self):
self.install_runtime_oem_support()
ctx = make_ctx(
self.target,
omarchy_install={"mode": "protected", "oem": True, "storage": {"luks_uuid": "abc"}},
)
with self.assertRaisesRegex(RuntimeError, "passphrase"):
phases_impl.stage_oem_state(ctx)
class ConfigureLoginOemTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.target = Path(self.tmp.name)
self.calls = []
run_patch = mock.patch.object(
phases_impl.subprocess, "run",
side_effect=lambda cmd, **kw: (self.calls.append(cmd), CompletedProcess(cmd, 0))[1],
)
run_patch.start()
self.addCleanup(run_patch.stop)
def test_oem_login_leaves_user_state_to_first_boot(self):
ctx = make_ctx(self.target, encrypt=True)
phases_impl.configure_login(ctx)
self.assertTrue((self.target / "etc/sddm.conf.d/99-omarchy-login.conf").exists())
self.assertFalse((self.target / "etc/sddm.conf.d/autologin.conf").exists())
self.assertFalse((self.target / "var/lib/sddm/state.conf").exists())
self.assertTrue(any("sddm.service" in cmd for cmd in self.calls))
def test_normal_encrypted_login_still_autologs_in(self):
ctx = make_ctx(self.target, oem=False, encrypt=True, username="jeff")
phases_impl.configure_login(ctx)
autologin = (self.target / "etc/sddm.conf.d/autologin.conf").read_text()
self.assertIn("User=jeff", autologin)
state = (self.target / "var/lib/sddm/state.conf").read_text()
self.assertIn("User=jeff", state)
class ConfigureSshAccessOemTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.target = Path(self.tmp.name) / "mnt"
self.target.mkdir()
info_patch = mock.patch.object(phases_impl, "info")
info_patch.start()
self.addCleanup(info_patch.stop)
def fake_run(cmd, **kwargs):
self.calls.append(cmd)
if "ufw" in cmd:
rules = self.target / "etc/ufw/user.rules"
rules.parent.mkdir(parents=True, exist_ok=True)
rules.write_text("-A ufw-user-input -p tcp --dport 22 -j ACCEPT\n")
return CompletedProcess(cmd, 0)
self.calls = []
run_patch = mock.patch.object(phases_impl.subprocess, "run", side_effect=fake_run)
run_patch.start()
self.addCleanup(run_patch.stop)
def test_oem_stages_keys_for_first_boot(self):
keys = Path(self.tmp.name) / "authorized_keys"
keys.write_text("ssh-ed25519 AAAA rig@host\n")
ctx = make_ctx(self.target, authorized_keys_path=keys)
phases_impl.configure_ssh_access(ctx)
staged = self.target / "var/lib/omarchy/oem/authorized_keys"
self.assertEqual(staged.read_text(), "ssh-ed25519 AAAA rig@host\n")
self.assertEqual(staged.stat().st_mode & 0o777, 0o600)
# No user yet — nothing under /home, no chown.
self.assertFalse((self.target / "home").exists())
self.assertFalse(any("chown" in cmd for cmd in self.calls))
# The door is still opened for first boot.
self.assertTrue(any("sshd.service" in cmd for cmd in self.calls))
class CreateFactorySnapshotTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.target = Path(self.tmp.name) / "mnt"
self.target.mkdir()
self.state_dir = Path(self.tmp.name) / "state"
self.state_dir.mkdir()
info_patch = mock.patch.object(phases_impl, "info")
info_patch.start()
self.addCleanup(info_patch.stop)
self.findmnt = {
"FSTYPE": "btrfs",
"OPTIONS": "rw,noatime,compress=zstd:3,subvol=/@",
"SOURCE": "/dev/mapper/omarchy_root[/@]",
}
self.calls = []
def fake_run(cmd, **kwargs):
self.calls.append(cmd)
if cmd[0] == "findmnt":
return CompletedProcess(cmd, 0, stdout=self.findmnt[cmd[2]] + "\n", stderr="")
if cmd[0] == "mount":
# Simulate the top level containing @.
(Path(cmd[-1]) / "@").mkdir(parents=True, exist_ok=True)
return CompletedProcess(cmd, 0, stdout="", stderr="")
run_patch = mock.patch.object(phases_impl.subprocess, "run", side_effect=fake_run)
run_patch.start()
self.addCleanup(run_patch.stop)
def ctx(self):
return make_ctx(self.target, oem=False, state_dir=self.state_dir)
def test_snapshots_root_as_factory(self):
phases_impl.create_factory_snapshot(self.ctx())
top = str(self.state_dir / "factory-top")
self.assertIn(["mount", "-o", "subvolid=5", "/dev/mapper/omarchy_root", top], self.calls)
self.assertIn(
["btrfs", "subvolume", "snapshot", "-r", f"{top}/@", f"{top}/@factory"],
self.calls,
)
self.assertIn(["umount", top], self.calls)
def test_non_btrfs_target_skips(self):
self.findmnt["FSTYPE"] = "ext4"
phases_impl.create_factory_snapshot(self.ctx())
self.assertFalse(any(cmd[0] == "mount" for cmd in self.calls))
def test_non_subvol_root_skips(self):
self.findmnt["OPTIONS"] = "rw,noatime"
phases_impl.create_factory_snapshot(self.ctx())
self.assertFalse(any(cmd[0] == "mount" for cmd in self.calls))
if __name__ == "__main__":
unittest.main()