orchestrator: shell out to bash helpers; patch archinstall before Python imports

This commit is contained in:
Ryan Hughes committed 2026-05-21 17:48:37 -04:00
1 parent 1742e6d997
commit 26f87f6789
3 files changed
+150 -46

No files matched your search

@@ -1,14 +1,19 @@
#!/bin/bash
#
# Live-ISO entry point for the Omarchy install orchestrator. Invoked from
# /root/.automated_script.sh after the configurator wizard has produced
# user_configuration.json / user_credentials.json / user_full_name.txt /
# user_email_address.txt / user_encrypt_installation.txt.
#
# Just sets cwd so 'python -m orchestrator.main' can find the package,
# then hands off all args.
# /root/.automated_script.sh after the configurator wizard has produced its
# user_*.json / user_*.txt files. Tees the full output to
# /var/log/omarchy-install.log so omarchy-upload-log can pick it up.
set -eEo pipefail
mkdir -p /var/log
touch /var/log/omarchy-install.log
# Patch archinstall BEFORE Python loads it. orchestrator/main.py top-level
# imports archinstall_adapter, which top-level imports archinstall.*; patching
# inside a phase would be too late (bytecode already loaded).
omarchy-iso-patch-archinstall
cd /usr/share/omarchy-iso
exec python -m orchestrator.main "$@"
exec python -m orchestrator.main "$@" 2>&1 | tee -a /var/log/omarchy-install.log
@@ -35,6 +35,7 @@ def build_phases():
prepare_live,
arch_install,
run_chroot_finalizer,
configure_login,
validate_boot,
finish,
)
@@ -43,6 +44,7 @@ def build_phases():
("Preparing live environment", prepare_live),
("Installing Arch + Omarchy", arch_install),
("Finalizing in chroot", run_chroot_finalizer),
("Configuring login", configure_login),
("Validating boot setup", validate_boot),
("Finishing", finish),
]
@@ -2,28 +2,28 @@
Phase ordering:
prepare_live → live ISO env + load arch config
prepare_live → pacman-key init, disk cleanup, load configurator
handlers (archinstall patch happens in the
wrapper before Python imports archinstall)
arch_install → archinstall-driven install (partition, base,
bootloader, write limine config, early omarchy
pkgs, useradd, runtime omarchy pkgs)
run_chroot_finalizer → arch-chroot finalize.sh as the install user
run_chroot_finalizer → bind mounts + sudoers shim + arch-chroot finalize.sh
configure_login → sddm autologin for unencrypted installs
validate_boot → assert UKI / limine.conf / kernel cmdline are sane
finish → reboot prompt
Heavy lifting in arch_install lives in archinstall_adapter (for the Installer
context manager) and in this file (for our limine-config write + omarchy
package selection). Other phases are kept small.
"""
from __future__ import annotations
import os
import shutil
import subprocess
from pathlib import Path
from . import archinstall_adapter as arch
from .context import InstallContext
from .ui import info
from .ui import confirm, info
# Packages installed BEFORE useradd. omarchy-settings populates /etc/skel so
@@ -39,22 +39,48 @@ EARLY_PACKAGES = [
# ─────────────────────────────────────────────────────────────────────────────
# prepare_live: parse user_configuration.json/user_credentials.json, build the
# archinstall handlers. Cached on ctx.state for downstream phases.
# prepare_live: ready the live ISO for the install — pacman keyring init,
# tear down any previous holders on the install disk (via the bash helper),
# then parse the configurator output.
#
# archinstall is patched in the wrapper (omarchy-iso-install) BEFORE Python
# imports it, so no patching happens here.
# ─────────────────────────────────────────────────────────────────────────────
def prepare_live(ctx: InstallContext) -> None:
info("› initializing pacman keyrings")
subprocess.run(["pacman-key", "--init"], check=True)
subprocess.run(["pacman-key", "--populate", "archlinux"], check=True)
subprocess.run(["pacman-key", "--populate", "omarchy"], check=True)
subprocess.run(["pacman", "-Sy", "--noconfirm"], check=True)
disk = _install_disk(ctx)
if disk:
info(f"› cleaning up holders on install disk: {disk}")
subprocess.run(["omarchy-iso-cleanup-disk", disk], check=True)
info("› loading configurator output")
ctx.state["arch_config_handler"] = arch.load_arch_config(
ctx.config_path, ctx.creds_path
)
ctx.state["mirror_handler"] = arch.make_mirror_handler(offline=True)
def _install_disk(ctx: InstallContext) -> str | None:
"""Return the device path of the disk being wiped, or None for
pre_mounted / no-wipe configs."""
config = ctx.user_configuration
for mod in config.get("disk_config", {}).get("device_modifications", []):
if mod.get("wipe"):
return mod.get("device")
return None
# ─────────────────────────────────────────────────────────────────────────────
# arch_install: everything inside a single Installer context manager. Mirrors
# guided.py's perform_installation() but reorders so our limine config write
# lands between add_bootloader and the first add_additional_packages call, and
# user creation happens AFTER early omarchy packages populate /etc/skel.
# arch_install: everything inside a single Installer context manager. Reorders
# guided.py's perform_installation() so our limine config write lands between
# add_bootloader and the first add_additional_packages call, and user
# creation happens AFTER early omarchy packages populate /etc/skel.
# ─────────────────────────────────────────────────────────────────────────────
def arch_install(ctx: InstallContext) -> None:
@@ -119,8 +145,7 @@ def arch_install(ctx: InstallContext) -> None:
installer.create_users(config.auth_config.users)
info("› installing Omarchy runtime + omarchy-base.packages")
runtime_pkgs = _runtime_package_list(ctx)
installer.add_additional_packages(runtime_pkgs)
installer.add_additional_packages(_runtime_package_list(ctx))
# Standard arch finishers.
if config.timezone:
@@ -133,13 +158,6 @@ def arch_install(ctx: InstallContext) -> None:
installer.genfstab()
# ─────────────────────────────────────────────────────────────────────────────
# Limine config write — extracted from install/login/limine-snapper.sh logic.
# Reads cmdline from /mnt/boot/limine.conf (which add_bootloader wrote),
# substitutes @@CMDLINE@@ into the template, writes /etc/default/limine +
# /etc/kernel/cmdline.
# ─────────────────────────────────────────────────────────────────────────────
def _write_limine_defaults(ctx: InstallContext) -> None:
if not arch.is_limine(ctx.state["arch_config_handler"].config):
return
@@ -154,13 +172,10 @@ def _write_limine_defaults(ctx: InstallContext) -> None:
if "root=" not in cmdline:
raise RuntimeError(f"Extracted cmdline has no root=: {cmdline!r}")
# The template lives in omarchy-installer (this package), so it's
# available from /mnt/usr/share/omarchy/... as soon as the early
# omarchy-installer pacstrap completes — but we want it BEFORE that.
# Read from our own runtime tree on the live ISO instead.
# Template lives in omarchy-installer's install tree, present on the live
# ISO via the omarchy-installer package.
template = ctx.omarchy_path / "install" / "assets" / "limine" / "default.conf"
if not template.exists():
# Fallback to the legacy path while we migrate templates between packages.
template = ctx.omarchy_path / "default" / "limine" / "default.conf"
if not template.exists():
raise RuntimeError(f"Limine template not found at {template}")
@@ -198,28 +213,66 @@ def _runtime_package_list(ctx: InstallContext) -> list[str]:
# ─────────────────────────────────────────────────────────────────────────────
# run_chroot_finalizer: arch-chroot -u $user into /mnt and run finalize.sh.
# Inherits stdout/stderr so the in-target output streams to our log capture.
# run_chroot_finalizer:
# 1. point the target at the offline pacman.conf so chroot pacman uses the
# bundled mirror
# 2. bind-mount the offline mirror + /opt/packages into /mnt so chroot sees
# the same paths
# 3. write a passwordless-sudo shim for the install user (finalize.sh's
# scripts run as the user and shell out to sudo repeatedly)
# 4. copy the omarchy install tooling into /mnt/tmp/omarchy-install (the
# target never gets the omarchy-installer package installed)
# 5. arch-chroot -u $user → /tmp/omarchy-install/finalize.sh
# ─────────────────────────────────────────────────────────────────────────────
def run_chroot_finalizer(ctx: InstallContext) -> None:
# omarchy-installer (the package) is NOT on the target system. Copy its
# tree from the live ISO into /mnt/tmp/omarchy-install/ so finalize.sh
# and install/ are reachable from inside the chroot. /tmp is ephemeral,
# so this leaves zero install detritus on the target after reboot.
# 1: offline pacman.conf
shutil.copy("/etc/pacman.conf", str(ctx.target / "etc" / "pacman.conf"))
# 2: bind mounts. Tracked so the finish phase can tear them down cleanly.
bind_mounts = [
("/var/cache/omarchy/mirror/offline", "/var/cache/omarchy/mirror/offline"),
("/opt/packages", "/opt/packages"),
]
for src, dst in bind_mounts:
target_dst = ctx.target / dst.lstrip("/")
target_dst.mkdir(parents=True, exist_ok=True)
subprocess.run(["mount", "--bind", src, str(target_dst)], check=True)
ctx.state["bind_mounts"] = [str(ctx.target / d.lstrip("/")) for _, d in bind_mounts]
# 3: sudoers shim. Cleaned up by omarchy's first-run flow.
sudoers = ctx.target / "etc" / "sudoers.d" / "99-omarchy-installer"
sudoers.parent.mkdir(parents=True, exist_ok=True)
sudoers.write_text(
"root ALL=(ALL:ALL) NOPASSWD: ALL\n"
"%wheel ALL=(ALL:ALL) NOPASSWD: ALL\n"
f"{ctx.username} ALL=(ALL:ALL) NOPASSWD: ALL\n"
)
sudoers.chmod(0o440)
# 4: copy install tooling to /mnt/tmp (ephemeral on first reboot, leaves
# zero install detritus on the target).
target_tooling = ctx.target / "tmp" / "omarchy-install"
target_tooling.parent.mkdir(exist_ok=True)
if target_tooling.exists():
shutil.rmtree(target_tooling)
subprocess.run(
["cp", "-a", f"{ctx.omarchy_path}/.", str(target_tooling)],
check=True,
)
subprocess.run(["chown", "-R", f"{ctx.username}:{ctx.username}", str(target_tooling)])
subprocess.run(
["chown", "-R", f"{ctx.username}:{ctx.username}", str(target_tooling)],
check=False,
)
# 5: arch-chroot -u $user → finalize.sh
mirror_channel = _read_omarchy_mirror()
env_extras = [
"OMARCHY_INSTALL_MODE=offline",
"OMARCHY_PATH=/tmp/omarchy-install",
f"OMARCHY_USER_NAME={ctx.full_name}",
f"OMARCHY_USER_EMAIL={ctx.email}",
f"OMARCHY_MIRROR={mirror_channel}",
f"USER={ctx.username}",
f"HOME=/home/{ctx.username}",
]
@@ -235,9 +288,51 @@ def run_chroot_finalizer(ctx: InstallContext) -> None:
subprocess.run(cmd, check=True)
def _read_omarchy_mirror() -> str:
p = Path("/root/omarchy_mirror")
return p.read_text().strip() if p.exists() else "stable"
# ─────────────────────────────────────────────────────────────────────────────
# validate_boot: hard checks before reboot. If the install ran but produced
# a UKI that can't actually boot, we want to halt here, not surprise the user.
# configure_login: sddm autologin for unencrypted installs only (encrypted
# installs already get a LUKS unlock prompt, no need for sddm autologin).
# ─────────────────────────────────────────────────────────────────────────────
def configure_login(ctx: InstallContext) -> None:
if ctx.encrypt:
return
sddm_dir = ctx.target / "etc" / "sddm.conf.d"
sddm_dir.mkdir(parents=True, exist_ok=True)
(sddm_dir / "autologin.conf").unlink(missing_ok=True)
(sddm_dir / "99-omarchy-login.conf").write_text(
"[Theme]\nCurrent=omarchy\n\n"
"[Users]\nRememberLastUser=true\nRememberLastSession=true\n"
)
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"
)
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,
)
# ─────────────────────────────────────────────────────────────────────────────
# validate_boot: hard checks before reboot. If the install ran but produced a
# UKI that can't actually boot, halt here rather than surprise the user.
# ─────────────────────────────────────────────────────────────────────────────
def validate_boot(ctx: InstallContext) -> None:
@@ -246,7 +341,7 @@ def validate_boot(ctx: InstallContext) -> None:
raise RuntimeError(f"{limine_conf} missing")
content = limine_conf.read_text()
if "^/+Omarchy" not in content and "Omarchy" not in content:
if "Omarchy" not in content:
raise RuntimeError("/boot/limine.conf has no Omarchy entry")
if ctx.encrypt and "cryptdevice=" not in content:
@@ -264,11 +359,13 @@ def validate_boot(ctx: InstallContext) -> None:
# ─────────────────────────────────────────────────────────────────────────────
# finish: show completion + offer reboot. No mutation.
# finish: unwind bind mounts, prompt for reboot.
# ─────────────────────────────────────────────────────────────────────────────
def finish(ctx: InstallContext) -> None:
from .ui import confirm
for mount_point in ctx.state.get("bind_mounts", []):
subprocess.run(["umount", mount_point], check=False, capture_output=True)
info("Installation finished. Reboot when ready.")
if confirm("Reboot now?", default=True):
os.system("reboot")