Files
omarchycn/install/orchestrator/phases_impl.py
T
Ryan Hughes c6ccd62550 Wire archinstall adapter + concrete phase implementations
Chunks 2-6 of the C3 refactor. With the adapter in place, the phase
list collapses from 12 stubs to 5 real phases:

  prepare_live          parse user_*.json into archinstall handlers
  arch_install          partition + base + bootloader + limine config +
                        early omarchy + useradd + runtime omarchy (all
                        inside a single Installer context manager)
  run_chroot_finalizer  arch-chroot -u $user → finalize.sh
  validate_boot         halt if /boot/limine.conf, /etc/kernel/cmdline,
                        or UKI are missing/malformed
  finish                reboot prompt

Key reordering vs upstream guided.py: write_limine_config runs BETWEEN
add_bootloader and the first add_additional_packages call. By the time
omarchy-limine pulls limine-mkinitcpio-hook (in the runtime package
install), /etc/default/limine and /etc/kernel/cmdline are in place, so
the UKI is built correctly on the first try — no stale-UKI purge,
no follow-up limine-update.

add_additional_packages is split into two calls so /etc/skel is
populated (via omarchy-settings in EARLY_PACKAGES) BEFORE useradd:

  EARLY_PACKAGES = base-devel git omarchy-keyring omarchy-settings omarchy-installer
  → installer.create_users(users)
  → runtime: omarchy + omarchy-base.packages

archinstall_adapter.py is the ONLY module that imports from archinstall.
If archinstall's API churns, the blast radius is contained here. Tested
against archinstall 4.3 / Python 3.14.

Limine template lookup tries the new install/assets/limine/ path first
and falls back to the legacy default/limine/ location during the
template-ownership migration (lands in a follow-up chunk).
2026-06-04 18:34:35 -04:00

262 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Concrete phase implementations.
Phase ordering:
prepare_live → live ISO env + load arch config
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
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 subprocess
from pathlib import Path
from . import archinstall_adapter as arch
from .context import InstallContext
from .ui import info
# Packages installed BEFORE useradd. omarchy-settings populates /etc/skel so
# the user's home gets seeded correctly; omarchy-installer ships the install/
# tree + finalize.sh that the in-chroot finalizer needs.
EARLY_PACKAGES = [
"base-devel",
"git",
"omarchy-keyring",
"omarchy-settings",
"omarchy-installer",
]
# ─────────────────────────────────────────────────────────────────────────────
# prepare_live: parse user_configuration.json/user_credentials.json, build the
# archinstall handlers. Cached on ctx.state for downstream phases.
# ─────────────────────────────────────────────────────────────────────────────
def prepare_live(ctx: InstallContext) -> None:
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)
# ─────────────────────────────────────────────────────────────────────────────
# 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.
# ─────────────────────────────────────────────────────────────────────────────
def arch_install(ctx: InstallContext) -> None:
handler = ctx.state["arch_config_handler"]
mirror_handler = ctx.state["mirror_handler"]
config = handler.config
info(" partitioning + formatting + encrypting")
arch.perform_filesystem_operations(config)
info(" opening installer context")
with arch.open_installer(config, ctx.target, silent=True) as installer:
if not arch.is_pre_mount(config):
installer.mount_ordered_layout()
installer.sanity_check(
offline=True,
skip_ntp=True,
skip_wkd=True,
)
if not arch.is_pre_mount(config) and arch.is_encrypted(config):
installer.generate_key_files()
if config.mirror_config:
installer.set_mirrors(mirror_handler, config.mirror_config, on_target=False)
info(" installing base system")
installer.minimal_installation(
optional_repositories=(
config.mirror_config.optional_repositories
if config.mirror_config else []
),
mkinitcpio=True,
hostname=config.hostname,
locale_config=config.locale_config,
pacman_config=config.pacman_config,
)
if config.mirror_config:
installer.set_mirrors(mirror_handler, config.mirror_config, on_target=True)
if config.swap and config.swap.enabled:
installer.setup_swap(algo=config.swap.algorithm)
info(" installing bootloader (Limine)")
if config.bootloader_config:
installer.add_bootloader(
config.bootloader_config.bootloader,
config.bootloader_config.uki,
config.bootloader_config.removable,
)
info(" writing Limine config (so limine-mkinitcpio-hook fires correctly)")
_write_limine_defaults(ctx)
info(f" installing early Omarchy packages: {', '.join(EARLY_PACKAGES)}")
installer.add_additional_packages(EARLY_PACKAGES)
info(" creating user (with /etc/skel populated)")
if config.auth_config and config.auth_config.users:
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)
# Standard arch finishers.
if config.timezone:
installer.set_timezone(config.timezone)
if config.ntp:
installer.activate_time_synchronization()
if root := arch.root_user(config):
installer.set_user_password(root)
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
limine_conf = ctx.target / "boot" / "limine.conf"
if not limine_conf.exists():
raise RuntimeError(f"{limine_conf} not found after add_bootloader")
cmdline = _extract_cmdline(limine_conf)
if not cmdline.strip():
raise RuntimeError("Could not extract kernel cmdline from limine.conf")
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 = 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}")
default_limine = ctx.target / "etc" / "default" / "limine"
default_limine.parent.mkdir(parents=True, exist_ok=True)
default_limine.write_text(template.read_text().replace("@@CMDLINE@@", cmdline))
kernel_cmdline = ctx.target / "etc" / "kernel" / "cmdline"
kernel_cmdline.parent.mkdir(parents=True, exist_ok=True)
kernel_cmdline.write_text(cmdline + "\n")
def _extract_cmdline(limine_conf: Path) -> str:
for line in limine_conf.read_text().splitlines():
stripped = line.strip()
if stripped.startswith("cmdline:"):
return stripped[len("cmdline:"):].strip()
return ""
def _runtime_package_list(ctx: InstallContext) -> list[str]:
"""omarchy + every package in install/omarchy-base.packages that isn't
already in EARLY_PACKAGES."""
base_pkgs_file = ctx.omarchy_path / "install" / "omarchy-base.packages"
pkgs = ["omarchy"]
early = set(EARLY_PACKAGES)
for raw in base_pkgs_file.read_text().splitlines():
s = raw.strip()
if not s or s.startswith("#"):
continue
if s not in early and s not in pkgs:
pkgs.append(s)
return pkgs
# ─────────────────────────────────────────────────────────────────────────────
# 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.
# ─────────────────────────────────────────────────────────────────────────────
def run_chroot_finalizer(ctx: InstallContext) -> None:
env_extras = [
f"OMARCHY_INSTALL_MODE=offline",
f"OMARCHY_USER_NAME={ctx.full_name}",
f"OMARCHY_USER_EMAIL={ctx.email}",
f"USER={ctx.username}",
f"HOME=/home/{ctx.username}",
]
cmd = [
"arch-chroot",
"-u", ctx.username,
str(ctx.target),
"env", "--unset=XDG_RUNTIME_DIR",
*env_extras,
"/bin/bash", "-lc",
f"bash {ctx.omarchy_path}/finalize.sh",
]
subprocess.run(cmd, check=True)
# ─────────────────────────────────────────────────────────────────────────────
# 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.
# ─────────────────────────────────────────────────────────────────────────────
def validate_boot(ctx: InstallContext) -> None:
limine_conf = ctx.target / "boot" / "limine.conf"
if not limine_conf.exists():
raise RuntimeError(f"{limine_conf} missing")
content = limine_conf.read_text()
if "^/+Omarchy" not in content and "Omarchy" not in content:
raise RuntimeError("/boot/limine.conf has no Omarchy entry")
if ctx.encrypt and "cryptdevice=" not in content:
raise RuntimeError("Encrypted install but /boot/limine.conf has no cryptdevice=")
kernel_cmdline = ctx.target / "etc" / "kernel" / "cmdline"
if not kernel_cmdline.exists():
raise RuntimeError(f"{kernel_cmdline} missing — UKI would have no cmdline")
uki_dir = ctx.target / "boot" / "EFI" / "Linux"
if uki_dir.exists():
ukis = list(uki_dir.glob("*_linux*.efi"))
if not ukis:
raise RuntimeError(f"No UKI found in {uki_dir}")
# ─────────────────────────────────────────────────────────────────────────────
# finish: show completion + offer reboot. No mutation.
# ─────────────────────────────────────────────────────────────────────────────
def finish(ctx: InstallContext) -> None:
from .ui import confirm
info("Installation finished. Reboot when ready.")
if confirm("Reboot now?", default=True):
os.system("reboot")