diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py index 06a0b2d..aa6c1f5 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -33,6 +33,7 @@ from __future__ import annotations import json import os +import re import shutil import subprocess from pathlib import Path @@ -340,12 +341,312 @@ def arch_install_base(ctx: InstallContext) -> None: # ───────────────────────────────────────────────────────────────────────────── # configure_protected_boot: Omarchy-owned fstab/crypttab/mkinitcpio/bootloader. -# Implemented in Step 8. +# +# Order: +# 1. fstab — written from our known mount intent (no genfstab) +# 2. crypttab + mkinitcpio.conf — encrypt/btrfs hooks + initramfs btrfs deps +# 3. mkinitcpio -P — generate /boot/initramfs-{kernel}{,-fallback}.img +# 4. Limine into EFI/Omarchy — never touches EFI/Microsoft or EFI/BOOT +# 5. efibootmgr entry, BootOrder preserves whatever was there + Omarchy first +# 6. Sanity: Windows entry must survive # ───────────────────────────────────────────────────────────────────────────── def configure_protected_boot(ctx: InstallContext) -> None: - raise NotImplementedError( - "Implemented in Step 8 — see REFACTOR-PLAN-C3-EXECUTION.md" + protected = ctx.state["protected"] + + info("› ensuring limine + efibootmgr installed in target") + subprocess.run( + ["arch-chroot", str(ctx.target), "pacman", "-S", + "--needed", "--noconfirm", "limine", "efibootmgr"], + check=True, + ) + + info("› writing /etc/fstab") + _write_fstab(ctx, protected) + + if protected.get("luks_uuid"): + info("› writing /etc/crypttab.initramfs") + _write_crypttab(ctx, protected) + + info("› editing /etc/mkinitcpio.conf") + _edit_mkinitcpio_conf(ctx, protected) + + info("› running mkinitcpio -P") + _run_mkinitcpio(ctx) + + info("› capturing efibootmgr state pre-install") + pre_state = _read_efibootmgr() + windows_before = _find_label_entries(pre_state["entries"], "Windows") + + info("› installing Limine into EFI/Omarchy") + _install_limine(ctx, protected) + + info("› registering efibootmgr entry") + _register_efibootmgr_entry(ctx, protected, pre_state) + + info("› verifying Windows boot entry survived") + post_state = _read_efibootmgr() + windows_after = _find_label_entries(post_state["entries"], "Windows") + if windows_before and not windows_after: + raise RuntimeError( + "Windows boot entry disappeared during Limine install — aborting" + ) + + +# ── fstab ──────────────────────────────────────────────────────────────────── + +def _btrfs_root_device(protected: dict) -> str: + """Device that holds the btrfs filesystem (mapper for encrypted, raw + partition for plain).""" + if protected.get("luks_uuid"): + return "/dev/mapper/omarchy_root" + return protected["root_device"] + + +def _blkid_uuid(device: str) -> str: + res = subprocess.run( + ["blkid", "-s", "UUID", "-o", "value", device], + capture_output=True, text=True, check=True, + ) + uuid = res.stdout.strip() + if not uuid: + raise RuntimeError(f"blkid returned no UUID for {device}") + return uuid + + +def _esp_device(ctx: InstallContext, protected: dict) -> str: + esp_mp = ctx.target / protected["esp_mount"].lstrip("/") + res = subprocess.run( + ["findmnt", "-n", "-o", "SOURCE", str(esp_mp)], + capture_output=True, text=True, check=True, + ) + dev = res.stdout.strip() + if not dev: + raise RuntimeError(f"could not resolve ESP device at {esp_mp}") + return dev + + +def _write_fstab(ctx: InstallContext, protected: dict) -> None: + btrfs_dev = _btrfs_root_device(protected) + btrfs_uuid = _blkid_uuid(btrfs_dev) + esp_dev = _esp_device(ctx, protected) + esp_uuid = _blkid_uuid(esp_dev) + esp_mount = protected["esp_mount"] + + btrfs_opts = "noatime,compress=zstd,subvol=" + lines = [ + "# /etc/fstab — generated by omarchy installer (protected mode)", + "# ", + f"UUID={btrfs_uuid} / btrfs {btrfs_opts}@ 0 0", + f"UUID={btrfs_uuid} /home btrfs {btrfs_opts}@home 0 0", + f"UUID={btrfs_uuid} /var/log btrfs {btrfs_opts}@log 0 0", + f"UUID={btrfs_uuid} /var/cache/pacman/pkg btrfs {btrfs_opts}@pkg 0 0", + f"UUID={esp_uuid} {esp_mount} vfat umask=0077 0 2", + "", + ] + fstab = ctx.target / "etc" / "fstab" + fstab.write_text("\n".join(lines)) + + +# ── crypttab ───────────────────────────────────────────────────────────────── + +def _write_crypttab(ctx: InstallContext, protected: dict) -> None: + luks_uuid = protected["luks_uuid"] + crypttab = ctx.target / "etc" / "crypttab.initramfs" + crypttab.write_text(f"omarchy_root UUID={luks_uuid} none luks,discard\n") + + +# ── mkinitcpio.conf ────────────────────────────────────────────────────────── + +def _edit_mkinitcpio_conf(ctx: InstallContext, protected: dict) -> None: + conf = ctx.target / "etc" / "mkinitcpio.conf" + text = conf.read_text() + + if protected.get("luks_uuid"): + hooks = ( + "HOOKS=(base udev autodetect microcode modconf kms keyboard " + "keymap consolefont block encrypt filesystems fsck)" + ) + else: + hooks = ( + "HOOKS=(base udev autodetect microcode modconf kms keyboard " + "keymap consolefont block filesystems fsck)" + ) + + text = _replace_assignment(text, "HOOKS", hooks) + text = _replace_assignment(text, "MODULES", "MODULES=(btrfs)") + text = _replace_assignment(text, "BINARIES", "BINARIES=(/usr/bin/btrfs)") + + conf.write_text(text) + + +def _replace_assignment(text: str, key: str, new_line: str) -> str: + """Replace a `KEY=(...)` line in mkinitcpio.conf-style text. If the key + isn't present, append the line.""" + pattern = re.compile(rf"^{re.escape(key)}=\(.*\)\s*$", re.MULTILINE) + if pattern.search(text): + return pattern.sub(new_line, text, count=1) + if not text.endswith("\n"): + text += "\n" + return text + new_line + "\n" + + +# ── mkinitcpio ─────────────────────────────────────────────────────────────── + +def _run_mkinitcpio(ctx: InstallContext) -> None: + subprocess.run( + ["arch-chroot", str(ctx.target), "mkinitcpio", "-P"], + check=True, + ) + + +# ── Limine ─────────────────────────────────────────────────────────────────── + +LIMINE_CONF_TEMPLATE = """\ +timeout: 3 +default_entry: 1 +interface_branding: Omarchy Bootloader + +/Omarchy Linux + protocol: linux + path: uuid({btrfs_uuid}):/@/boot/vmlinuz-{kernel} + cmdline: {cmdline} + module_path: uuid({btrfs_uuid}):/@/boot/initramfs-{kernel}.img + +/Omarchy Linux (fallback) + protocol: linux + path: uuid({btrfs_uuid}):/@/boot/vmlinuz-{kernel} + cmdline: {cmdline} + module_path: uuid({btrfs_uuid}):/@/boot/initramfs-{kernel}-fallback.img +""" + + +def _omarchy_esp_path(ctx: InstallContext, protected: dict) -> Path: + esp_mount = ctx.target / protected["esp_mount"].lstrip("/") + return esp_mount / protected["esp_path"].lstrip("/") + + +def _build_cmdline(protected: dict, btrfs_uuid: str) -> str: + if protected.get("luks_uuid"): + return ( + f"cryptdevice=UUID={protected['luks_uuid']}:omarchy_root " + "root=/dev/mapper/omarchy_root rw " + "rootflags=subvol=@ rootfstype=btrfs quiet splash" + ) + return ( + f"root=UUID={btrfs_uuid} rw " + "rootflags=subvol=@ rootfstype=btrfs quiet splash" + ) + + +def _install_limine(ctx: InstallContext, protected: dict) -> None: + src = ctx.target / "usr" / "share" / "limine" / "BOOTX64.EFI" + if not src.exists(): + raise RuntimeError( + f"Limine EFI binary not found at {src} — limine package missing in target" + ) + + omarchy_esp = _omarchy_esp_path(ctx, protected) + omarchy_esp.mkdir(parents=True, exist_ok=True) + + dst = omarchy_esp / "BOOTX64.EFI" + shutil.copy2(src, dst) + + btrfs_uuid = _blkid_uuid(_btrfs_root_device(protected)) + cmdline = _build_cmdline(protected, btrfs_uuid) + conf = LIMINE_CONF_TEMPLATE.format( + btrfs_uuid=btrfs_uuid, + kernel=protected["kernel"], + cmdline=cmdline, + ) + (omarchy_esp / "limine.conf").write_text(conf) + + +# ── efibootmgr ─────────────────────────────────────────────────────────────── + +_BOOT_ENTRY_RE = re.compile(r"^Boot([0-9A-Fa-f]{4})\*?\s+(.*)$") +_BOOT_ORDER_RE = re.compile(r"^BootOrder:\s*(.*)$") + + +def _read_efibootmgr() -> dict: + res = subprocess.run( + ["efibootmgr"], + capture_output=True, text=True, check=True, + ) + entries: dict[str, str] = {} + order: list[str] = [] + for line in res.stdout.splitlines(): + m = _BOOT_ENTRY_RE.match(line) + if m: + entries[m.group(1).upper()] = m.group(2).strip() + continue + m = _BOOT_ORDER_RE.match(line) + if m: + order = [n.strip().upper() for n in m.group(1).split(",") if n.strip()] + return {"entries": entries, "order": order, "raw": res.stdout} + + +def _find_label_entries(entries: dict[str, str], needle: str) -> list[str]: + return [num for num, label in entries.items() if needle.lower() in label.lower()] + + +def _split_partition_device(part_dev: str) -> tuple[str, int]: + parent = subprocess.run( + ["lsblk", "-ndo", "PKNAME", part_dev], + capture_output=True, text=True, check=True, + ).stdout.strip() + if not parent: + raise RuntimeError(f"could not find parent disk for {part_dev}") + part_num = subprocess.run( + ["lsblk", "-ndo", "PARTN", part_dev], + capture_output=True, text=True, check=True, + ).stdout.strip() + if not part_num: + raise RuntimeError(f"could not find partition number for {part_dev}") + return f"/dev/{parent}", int(part_num) + + +def _register_efibootmgr_entry( + ctx: InstallContext, protected: dict, pre_state: dict +) -> None: + esp_dev = _esp_device(ctx, protected) + disk, part_num = _split_partition_device(esp_dev) + + # Clean up any stale "Omarchy Linux" entries so we don't accumulate dupes + # across re-installs. + for num in _find_label_entries(pre_state["entries"], "Omarchy Linux"): + subprocess.run( + ["efibootmgr", "--bootnum", num, "--delete-bootnum"], + check=False, capture_output=True, + ) + + subprocess.run( + [ + "efibootmgr", + "--create", + "--disk", disk, + "--part", str(part_num), + "--label", "Omarchy Linux", + "--loader", "\\EFI\\Omarchy\\BOOTX64.EFI", + "--unicode", + ], + check=True, capture_output=True, text=True, + ) + + post = _read_efibootmgr() + new_omarchy = _find_label_entries(post["entries"], "Omarchy Linux") + if not new_omarchy: + raise RuntimeError( + "efibootmgr --create reported success but no Omarchy Linux entry found" + ) + omarchy_num = new_omarchy[0] + + # Preserve the original BootOrder, but put Omarchy first. + keep = [n for n in pre_state["order"] if n not in new_omarchy] + new_order = ",".join([omarchy_num, *keep]) + subprocess.run( + ["efibootmgr", "--bootorder", new_order], + check=True, capture_output=True, ) @@ -498,13 +799,55 @@ def validate_boot_full(ctx: InstallContext) -> None: # ───────────────────────────────────────────────────────────────────────────── # validate_boot_protected: hard checks for the dualboot/protected path. -# Implemented in Step 8. +# Mirrors validate_boot_full but checks the Omarchy ESP subdir, fstab/crypttab, +# kernel/initramfs presence, and efibootmgr entry registration. # ───────────────────────────────────────────────────────────────────────────── def validate_boot_protected(ctx: InstallContext) -> None: - raise NotImplementedError( - "Implemented in Step 8 — see REFACTOR-PLAN-C3-EXECUTION.md" - ) + protected = ctx.state["protected"] + kernel = protected["kernel"] + + omarchy_esp = _omarchy_esp_path(ctx, protected) + + bootx64 = omarchy_esp / "BOOTX64.EFI" + if not bootx64.exists() or bootx64.stat().st_size == 0: + raise RuntimeError(f"{bootx64} missing or empty") + + limine_conf = omarchy_esp / "limine.conf" + if not limine_conf.exists(): + raise RuntimeError(f"{limine_conf} missing") + if "/Omarchy Linux" not in limine_conf.read_text(): + raise RuntimeError(f"{limine_conf} has no /Omarchy Linux entry") + + fstab = ctx.target / "etc" / "fstab" + if not fstab.exists(): + raise RuntimeError(f"{fstab} missing") + fstab_text = fstab.read_text() + btrfs_uuid = _blkid_uuid(_btrfs_root_device(protected)) + esp_uuid = _blkid_uuid(_esp_device(ctx, protected)) + if btrfs_uuid not in fstab_text: + raise RuntimeError(f"{fstab} missing btrfs UUID {btrfs_uuid}") + if esp_uuid not in fstab_text: + raise RuntimeError(f"{fstab} missing ESP UUID {esp_uuid}") + + if protected.get("luks_uuid"): + crypttab = ctx.target / "etc" / "crypttab.initramfs" + if not crypttab.exists(): + raise RuntimeError(f"{crypttab} missing") + if protected["luks_uuid"] not in crypttab.read_text(): + raise RuntimeError(f"{crypttab} missing LUKS UUID {protected['luks_uuid']}") + + vmlinuz = ctx.target / "boot" / f"vmlinuz-{kernel}" + if not vmlinuz.exists(): + raise RuntimeError(f"{vmlinuz} missing") + + initramfs = ctx.target / "boot" / f"initramfs-{kernel}.img" + if not initramfs.exists(): + raise RuntimeError(f"{initramfs} missing") + + post = _read_efibootmgr() + if not _find_label_entries(post["entries"], "Omarchy Linux"): + raise RuntimeError("no 'Omarchy Linux' entry registered in efibootmgr") # ─────────────────────────────────────────────────────────────────────────────