orchestrator: install Limine ourselves; stop creating arch-limine entries to delete later

archinstall's add_bootloader creates EFI/arch-limine/{BOOTX64.EFI, BOOTIA32.EFI}, writes a limine.conf with an Arch Linux entry, and registers an efibootmgr label "Arch Linux Limine Bootloader" — only for limine-snapper.sh to delete it all and rebuild with Omarchy branding via limine-mkinitcpio-hook.

Replace the add_bootloader call with _install_limine_omarchy, which does the same work archinstall's _add_limine_bootloader does (UEFI EFI binary copy + efibootmgr register, or BIOS limine-bios.sys + limine bios-install) but with Omarchy-owned paths and labels from the start:

  - EFI subdir:    EFI/limine          (not EFI/arch-limine)

  - efibootmgr:    label "Limine"      (not "Arch Linux Limine Bootloader")

  - loader path:   \EFI\limine\BOOTX64.EFI

Compute the kernel cmdline directly from disk_config instead of harvesting it from a file archinstall wrote (whose path keeps drifting across versions). Then write /etc/default/limine and /etc/kernel/cmdline, and stage /boot/limine.conf from our styling template — limine-mkinitcpio-hook will add the actual boot entries when it deploys.

Adds limine + efibootmgr to EARLY_PACKAGES (archinstall used to pacstrap these as a side effect of add_bootloader).

Does not modify limine-snapper.sh — its cleanup code will simply no-op because there are no arch-limine paths or "Arch Linux Limine Bootloader" entries to find. That code can be pruned in a follow-up.
This commit is contained in:
Ryan Hughes committed 2026-05-21 21:57:50 -04:00
1 parent 39113a2d2b
commit 3a91cf780c
2 files changed
+185 -58

No files matched your search

@@ -19,7 +19,6 @@ The canonical call sequence (mirrored from archinstall.scripts.guided.py) is:
inst.minimal_installation(...) # base + linux pacstrap inst.minimal_installation(...) # base + linux pacstrap
inst.set_mirrors(handler, mirror_config, on_target=True) inst.set_mirrors(handler, mirror_config, on_target=True)
inst.setup_swap(algo=...) inst.setup_swap(algo=...)
inst.add_bootloader(bootloader, uki, removable)
inst.create_users(users) inst.create_users(users)
inst.add_additional_packages(packages) inst.add_additional_packages(packages)
inst.set_timezone(tz) inst.set_timezone(tz)
@@ -28,9 +27,9 @@ The canonical call sequence (mirrored from archinstall.scripts.guided.py) is:
inst.enable_service(services) inst.enable_service(services)
inst.genfstab() inst.genfstab()
Our orchestrator interleaves `write_limine_config` between `add_bootloader` Our orchestrator installs Omarchy's Limine files directly instead of invoking
and the first `add_additional_packages` so the limine UKI hook fires once, archinstall's bootloader helper, so EFI paths and efibootmgr labels are ours
correctly, on its first install. from the start.
""" """
from __future__ import annotations from __future__ import annotations
@@ -45,6 +44,8 @@ from archinstall.lib.args import ArchConfig, ArchConfigHandler
from archinstall.lib.authentication.authentication_handler import AuthenticationHandler from archinstall.lib.authentication.authentication_handler import AuthenticationHandler
from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.disk.filesystem import FilesystemHandler from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.disk.utils import get_parent_device_path, get_unique_path_for_device
from archinstall.lib.hardware import SysInfo
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
from archinstall.lib.mirror.mirror_handler import MirrorListHandler from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.models import Bootloader from archinstall.lib.models import Bootloader
@@ -121,11 +122,28 @@ def is_pre_mount(arch_config: ArchConfig) -> bool:
) )
def bootloader_enabled(arch_config: ArchConfig) -> bool:
bl = arch_config.bootloader_config
return bool(bl and bl.bootloader != Bootloader.NO_BOOTLOADER)
def is_limine(arch_config: ArchConfig) -> bool: def is_limine(arch_config: ArchConfig) -> bool:
bl = arch_config.bootloader_config bl = arch_config.bootloader_config
return bool(bl and bl.bootloader == Bootloader.Limine) return bool(bl and bl.bootloader == Bootloader.Limine)
def has_uefi() -> bool:
return SysInfo.has_uefi()
def parent_device_path(dev_path: Path) -> Path:
return get_parent_device_path(dev_path)
def unique_device_path(dev_path: Path) -> Path | None:
return get_unique_path_for_device(dev_path)
def root_user(arch_config: ArchConfig) -> User | None: def root_user(arch_config: ArchConfig) -> User | None:
auth = arch_config.auth_config auth = arch_config.auth_config
if not auth or not auth.root_enc_password: if not auth or not auth.root_enc_password:
@@ -6,8 +6,8 @@ Phase ordering (full-disk):
handlers (archinstall patch happens in the handlers (archinstall patch happens in the
wrapper before Python imports archinstall) wrapper before Python imports archinstall)
arch_install_full → archinstall-driven install (partition, base, arch_install_full → archinstall-driven install (partition, base,
bootloader, write limine config, early omarchy early omarchy pkgs, Omarchy Limine setup,
pkgs, useradd, runtime omarchy pkgs) useradd, runtime omarchy pkgs)
run_chroot_finalizer → bind mounts + sudoers shim + arch-chroot finalize.sh run_chroot_finalizer → bind mounts + sudoers shim + arch-chroot finalize.sh
configure_login → sddm autologin for unencrypted installs configure_login → sddm autologin for unencrypted installs
validate_boot_full → assert UKI / limine.conf / kernel cmdline are sane validate_boot_full → assert UKI / limine.conf / kernel cmdline are sane
@@ -36,6 +36,7 @@ import os
import re import re
import shutil import shutil
import subprocess import subprocess
import textwrap
from pathlib import Path from pathlib import Path
from . import archinstall_adapter as arch from . import archinstall_adapter as arch
@@ -50,6 +51,8 @@ from .ui import confirm, info
EARLY_PACKAGES = [ EARLY_PACKAGES = [
"base-devel", "base-devel",
"git", "git",
"limine",
"efibootmgr",
"omarchy-keyring", "omarchy-keyring",
"omarchy-settings", "omarchy-settings",
] ]
@@ -98,9 +101,9 @@ def _install_disk(ctx: InstallContext) -> str | None:
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# arch_install_full: everything inside a single Installer context manager. # arch_install_full: everything inside a single Installer context manager.
# Reorders guided.py's perform_installation() so our limine config write # Reorders guided.py's perform_installation() so early Omarchy packages install
# lands between add_bootloader and the first add_additional_packages call, # before user creation and before our Omarchy-owned Limine setup copies files
# and user creation happens AFTER early omarchy packages populate /etc/skel. # from the target's limine package.
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
def arch_install_full(ctx: InstallContext) -> None: def arch_install_full(ctx: InstallContext) -> None:
@@ -147,20 +150,20 @@ def arch_install_full(ctx: InstallContext) -> None:
if config.swap and config.swap.enabled: if config.swap and config.swap.enabled:
installer.setup_swap(algo=config.swap.algorithm) 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)}") info(f"› installing early Omarchy packages: {', '.join(EARLY_PACKAGES)}")
installer.add_additional_packages(EARLY_PACKAGES) installer.add_additional_packages(EARLY_PACKAGES)
if arch.bootloader_enabled(config):
if not arch.is_limine(config):
raise RuntimeError(
"Omarchy full-disk installs only support Limine bootloader setup"
)
info("› installing bootloader (Limine)")
_install_limine_omarchy(ctx, installer, config)
info("› writing Limine config (so limine-mkinitcpio-hook fires correctly)")
_write_limine_defaults_from_config(ctx, installer, config)
info("› creating user (with /etc/skel populated)") info("› creating user (with /etc/skel populated)")
if config.auth_config and config.auth_config.users: if config.auth_config and config.auth_config.users:
installer.create_users(config.auth_config.users) installer.create_users(config.auth_config.users)
@@ -179,57 +182,163 @@ def arch_install_full(ctx: InstallContext) -> None:
installer.genfstab() installer.genfstab()
def _write_limine_defaults(ctx: InstallContext) -> None: def _install_limine_omarchy(ctx: InstallContext, installer, config) -> None:
if not arch.is_limine(ctx.state["arch_config_handler"].config): boot_partition = installer._get_boot_partition()
return efi_partition = installer._get_efi_partition()
root = installer._get_root()
# archinstall 4.3 writes limine.conf to one of: if boot_partition is None:
# <target>/<esp_mount>/EFI/arch-limine/limine.conf (UEFI, non-removable) raise RuntimeError(f"Could not detect boot at mountpoint {ctx.target}")
# <target>/<esp_mount>/EFI/BOOT/limine.conf (UEFI, removable) if root is None:
# <target>/boot/limine/limine.conf (BIOS or fallback) raise RuntimeError(f"Could not detect root at mountpoint {ctx.target}")
# Probe in that order and use whichever exists.
candidates = [ bootloader_config = config.bootloader_config
ctx.target / "boot" / "EFI" / "arch-limine" / "limine.conf", bootloader_removable = bool(
ctx.target / "boot" / "EFI" / "BOOT" / "limine.conf", getattr(bootloader_config, "removable", False) if bootloader_config else False
ctx.target / "boot" / "limine" / "limine.conf", )
ctx.target / "boot" / "limine.conf", # very-old archinstall path limine_path = ctx.target / "usr" / "share" / "limine"
]
limine_conf = next((p for p in candidates if p.exists()), None) if arch.has_uefi():
if limine_conf is None: if efi_partition is None:
searched = "\n ".join(str(p) for p in candidates) raise RuntimeError("Could not detect EFI partition")
raise RuntimeError( if not efi_partition.mountpoint:
"limine.conf not found after add_bootloader. Searched:\n " + searched raise RuntimeError("EFI partition is not mounted")
parent_dev_path = arch.parent_device_path(efi_partition.safe_dev_path)
efi_dir_path = ctx.target / efi_partition.mountpoint.relative_to("/") / "EFI"
efi_dir_path_target = efi_partition.mountpoint / "EFI"
if bootloader_removable:
efi_dir_path = efi_dir_path / "BOOT"
efi_dir_path_target = efi_dir_path_target / "BOOT"
else:
# Non-removable UEFI installs place the x64 binary at EFI/limine/BOOTX64.EFI.
efi_dir_path = efi_dir_path / "limine"
efi_dir_path_target = efi_dir_path_target / "limine"
efi_dir_path.mkdir(parents=True, exist_ok=True)
for filename in ("BOOTIA32.EFI", "BOOTX64.EFI"):
_copy_required(limine_path / filename, efi_dir_path / filename)
hook_command = (
f"/usr/bin/cp /usr/share/limine/BOOTIA32.EFI {efi_dir_path_target}/ && "
f"/usr/bin/cp /usr/share/limine/BOOTX64.EFI {efi_dir_path_target}/"
) )
cmdline = _extract_cmdline(limine_conf) loader_path = _limine_efi_loader_path(bootloader_removable)
subprocess.run(
[
"efibootmgr",
"--create",
"--disk", str(parent_dev_path),
"--part", str(efi_partition.partn),
"--label", "Limine",
"--loader", loader_path,
"--unicode",
"--verbose",
],
check=True,
)
else:
boot_limine_path = ctx.target / "boot" / "limine"
boot_limine_path.mkdir(parents=True, exist_ok=True)
parent_dev_path = arch.parent_device_path(boot_partition.safe_dev_path)
if unique_path := arch.unique_device_path(parent_dev_path):
parent_dev_path = unique_path
_copy_required(limine_path / "limine-bios.sys", boot_limine_path / "limine-bios.sys")
subprocess.run(
["arch-chroot", str(ctx.target), "limine", "bios-install", str(parent_dev_path)],
check=True,
)
hook_command = (
f"/usr/bin/limine bios-install {parent_dev_path} && "
"/usr/bin/cp /usr/share/limine/limine-bios.sys /boot/limine/"
)
_write_limine_pacman_hook(ctx.target, hook_command)
installer._helper_flags["bootloader"] = "limine"
def _copy_required(src: Path, dst: Path) -> None:
if not src.exists():
raise RuntimeError(f"Required Limine file missing: {src}")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def _limine_efi_loader_path(bootloader_removable: bool) -> str:
try:
efi_bitness = Path("/sys/firmware/efi/fw_platform_size").read_text().strip()
except Exception as err:
raise RuntimeError(
"Could not read /sys/firmware/efi/fw_platform_size to determine EFI bitness"
) from err
if efi_bitness == "64":
return "\\EFI\\BOOT\\BOOTX64.EFI" if bootloader_removable else "\\EFI\\limine\\BOOTX64.EFI"
if efi_bitness == "32":
return "\\EFI\\BOOT\\BOOTIA32.EFI" if bootloader_removable else "\\EFI\\limine\\BOOTIA32.EFI"
raise RuntimeError(f'EFI bitness is neither 32 nor 64 bits. Found "{efi_bitness}".')
def _write_limine_pacman_hook(target: Path, hook_command: str) -> None:
hook_contents = textwrap.dedent(
f"""\
[Trigger]
Operation = Upgrade
Type = Package
Target = limine
[Action]
Description = Deploying Omarchy Limine after upgrade...
When = PostTransaction
Exec = /bin/sh -c "{hook_command}"
"""
)
hooks_dir = target / "etc" / "pacman.d" / "hooks"
hooks_dir.mkdir(parents=True, exist_ok=True)
(hooks_dir / "99-omarchy-limine.hook").write_text(hook_contents)
def _write_limine_defaults_from_config(ctx: InstallContext, installer, config) -> None:
if not arch.is_limine(config):
return
root = installer._get_root()
if root is None:
raise RuntimeError(f"Could not detect root at mountpoint {ctx.target}")
cmdline = " ".join(installer._get_kernel_params(root))
if not cmdline.strip(): if not cmdline.strip():
raise RuntimeError("Could not extract kernel cmdline from limine.conf") raise RuntimeError("Could not compute kernel cmdline from install config")
if "root=" not in cmdline: if "root=" not in cmdline:
raise RuntimeError(f"Extracted cmdline has no root=: {cmdline!r}") raise RuntimeError(f"Computed cmdline has no root=: {cmdline!r}")
# 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():
template = ctx.omarchy_path / "default" / "limine" / "default.conf"
if not template.exists():
raise RuntimeError(f"Limine template not found at {template}")
default_template = _limine_template(ctx, "default.conf")
default_limine = ctx.target / "etc" / "default" / "limine" default_limine = ctx.target / "etc" / "default" / "limine"
default_limine.parent.mkdir(parents=True, exist_ok=True) default_limine.parent.mkdir(parents=True, exist_ok=True)
default_limine.write_text(template.read_text().replace("@@CMDLINE@@", cmdline)) default_limine.write_text(default_template.read_text().replace("@@CMDLINE@@", cmdline))
kernel_cmdline = ctx.target / "etc" / "kernel" / "cmdline" kernel_cmdline = ctx.target / "etc" / "kernel" / "cmdline"
kernel_cmdline.parent.mkdir(parents=True, exist_ok=True) kernel_cmdline.parent.mkdir(parents=True, exist_ok=True)
kernel_cmdline.write_text(cmdline + "\n") kernel_cmdline.write_text(cmdline + "\n")
limine_conf = ctx.target / "boot" / "limine.conf"
limine_conf.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(_limine_template(ctx, "limine.conf"), limine_conf)
def _extract_cmdline(limine_conf: Path) -> str:
for line in limine_conf.read_text().splitlines(): def _limine_template(ctx: InstallContext, filename: str) -> Path:
stripped = line.strip() candidates = [
if stripped.startswith("cmdline:"): ctx.omarchy_path / "install" / "assets" / "limine" / filename,
return stripped[len("cmdline:"):].strip() ctx.omarchy_path / "default" / "limine" / filename,
return "" ]
for candidate in candidates:
if candidate.exists():
return candidate
searched = "\n ".join(str(p) for p in candidates)
raise RuntimeError(f"Limine template {filename} not found. Searched:\n {searched}")
def _runtime_package_list(ctx: InstallContext) -> list[str]: def _runtime_package_list(ctx: InstallContext) -> list[str]:
@@ -345,7 +454,7 @@ def arch_install_base(ctx: InstallContext) -> None:
info("› installing Omarchy runtime + omarchy-base.packages") info("› installing Omarchy runtime + omarchy-base.packages")
installer.add_additional_packages(_runtime_package_list(ctx)) installer.add_additional_packages(_runtime_package_list(ctx))
# Protected mode never calls add_bootloader, so pacstrap limine + # Protected mode owns boot setup separately, so pacstrap limine +
# efibootmgr here while the live ISO's offline mirror is still the # efibootmgr here while the live ISO's offline mirror is still the
# active pacman source. Doing it later via arch-chroot would hit # active pacman source. Doing it later via arch-chroot would hit
# the target's network-mirror pacman.conf and fail offline. # the target's network-mirror pacman.conf and fail offline.