Improve ISO installer flow and VM tooling

This commit is contained in:
Ryan Hughes committed 2026-05-25 19:54:12 -04:00
1 parent c50d0bc299
commit afa8293b2b
8 files changed
+807 -246

No files matched your search

@@ -34,6 +34,7 @@ from the start.
from __future__ import annotations
import importlib
from contextlib import contextmanager
from pathlib import Path
from typing import Iterator
@@ -144,6 +145,63 @@ def unique_device_path(dev_path: Path) -> Path | None:
return get_unique_path_for_device(dev_path)
def _application_handler():
"""Return archinstall's application handler across archinstall versions."""
module = importlib.import_module("archinstall.lib.applications.application_handler")
handler = getattr(module, "application_handler", None)
if handler is not None and callable(getattr(handler, "install_applications", None)):
return handler
handler_class = getattr(module, "ApplicationHandler", None)
if handler_class is None:
raise RuntimeError("archinstall application handler is unavailable")
try:
handler = handler_class()
except TypeError as exc:
raise RuntimeError("archinstall ApplicationHandler cannot be constructed") from exc
if not callable(getattr(handler, "install_applications", None)):
raise RuntimeError("archinstall ApplicationHandler has no install_applications method")
return handler
def _method_accepts_users(method) -> bool:
"""Return whether a bound archinstall method accepts the users argument.
Do not use inspect.signature here: Python 3.14 may evaluate archinstall's
lazy annotations, and some archinstall releases annotate with names that are
only imported under TYPE_CHECKING.
"""
fn = getattr(method, "__func__", method)
code = getattr(fn, "__code__", None)
if code is None:
return True
positional = code.co_varnames[:code.co_argcount]
kwonly = code.co_varnames[code.co_argcount:code.co_argcount + code.co_kwonlyargcount]
return "users" in (*positional, *kwonly)
def install_applications(installer: Installer, arch_config: ArchConfig) -> None:
"""Install archinstall application selections such as PipeWire audio.
The configurator still writes archinstall's audio_config. We own phase
ordering now, but should not silently drop archinstall's hardware-aware
application installers (SOF/ALSA firmware detection, PipeWire packages,
Bluetooth selections, etc.).
"""
app_config = arch_config.app_config
if not app_config:
return
users = arch_config.auth_config.users if arch_config.auth_config else None
handler = _application_handler()
install_applications_method = handler.install_applications
if _method_accepts_users(install_applications_method):
install_applications_method(installer, app_config, users)
else:
install_applications_method(installer, app_config)
def root_user(arch_config: ArchConfig) -> User | None:
auth = arch_config.auth_config
if not auth or not auth.root_enc_password:
@@ -36,6 +36,7 @@ def build_phases(ctx: InstallContext):
run_system_finalizer,
finalize_limine_boot,
run_chroot_finalizer,
configure_dns_resolver,
configure_login,
validate_boot,
)
@@ -49,6 +50,7 @@ def build_phases(ctx: InstallContext):
("Finalizing Limine boot", finalize_limine_boot),
("Finalizing user", run_chroot_finalizer),
("Configuring login", configure_login),
("Configuring DNS resolver", configure_dns_resolver),
("Validating boot setup", validate_boot),
]
@@ -14,7 +14,7 @@ Phase ordering (full-disk and protected/pre-mounted):
run_system_finalizer → arch-chroot root omarchy-setup-system
finalize_limine_boot → final Limine config/UKI build after hardware drop-ins
run_chroot_finalizer → arch-chroot -u user omarchy-setup-user
configure_login → sddm autologin for unencrypted installs
configure_login → sddm state + encrypted-install autologin
validate_boot → assert UKI / limine.conf / kernel cmdline are sane
"""
@@ -182,6 +182,10 @@ def arch_install_system(ctx: InstallContext) -> None:
if config.auth_config and config.auth_config.users:
installer.create_users(config.auth_config.users)
if config.app_config:
info("› installing archinstall application selections")
arch.install_applications(installer, config)
info("› installing Omarchy runtime + omarchy-base.packages")
installer.add_additional_packages(_runtime_package_list(ctx))
finally:
@@ -901,6 +905,29 @@ def _ensure_finalizer_log_started(ctx: InstallContext) -> tuple[str, int]:
return ctx.state["omarchy_start_time"], ctx.state["omarchy_start_epoch"]
def _target_user_env(ctx: InstallContext, user: str) -> list[str]:
home = f"/home/{user}"
shell = "/bin/bash"
passwd = ctx.target / "etc" / "passwd"
try:
for line in passwd.read_text(errors="ignore").splitlines():
fields = line.split(":")
if len(fields) >= 7 and fields[0] == user:
home = fields[5] or home
shell = fields[6] or shell
break
except OSError:
pass
return [
f"HOME={home}",
f"USER={user}",
f"LOGNAME={user}",
f"SHELL={shell}",
]
def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str | None = None) -> None:
_prepare_target_setup(ctx)
omarchy_start_time, omarchy_start_epoch = _ensure_finalizer_log_started(ctx)
@@ -929,6 +956,7 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str
f"OMARCHY_USER_EMAIL={ctx.email}",
f"OMARCHY_MIRROR={mirror_channel}",
"OMARCHY_INSTALL_LOG_FILE=/var/log/omarchy-install.log",
"OMARCHY_LOG_TO_STDOUT=1",
]
if _install_debug_enabled():
env_extras.append("OMARCHY_INSTALL_DEBUG=1")
@@ -937,6 +965,7 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str
chroot_cmd = ["arch-chroot"]
if user:
chroot_cmd += ["-u", user]
env_extras.extend(_target_user_env(ctx, user))
chroot_cmd += [str(ctx.target), "env", "--unset=XDG_RUNTIME_DIR", *env_extras, *cmd]
try:
@@ -1069,31 +1098,54 @@ def run_chroot_finalizer(ctx: InstallContext) -> None:
user=ctx.username,
)
def configure_dns_resolver(ctx: InstallContext) -> None:
"""Put the installed system in systemd-resolved stub mode.
Arch's systemd-resolved docs explicitly say not to create this symlink from
inside arch-chroot because /etc/resolv.conf may be a bind mount from the
live environment. Do it from the ISO against /mnt instead.
"""
resolv_conf = ctx.target / "etc" / "resolv.conf"
target = "../run/systemd/resolve/stub-resolv.conf"
if resolv_conf.is_symlink() and os.readlink(resolv_conf) == target:
return
info("› configuring /etc/resolv.conf for systemd-resolved")
resolv_conf.parent.mkdir(parents=True, exist_ok=True)
resolv_conf.unlink(missing_ok=True)
resolv_conf.symlink_to(target)
def _read_omarchy_mirror() -> str:
p = Path("/root/omarchy_mirror")
return p.read_text().strip() if p.exists() else "stable"
# ─────────────────────────────────────────────────────────────────────────────
# configure_login: sddm autologin for unencrypted installs only (encrypted
# installs already get a LUKS unlock prompt, no need for sddm autologin).
# configure_login: seed SDDM's last user/session for the password-only Omarchy
# greeter. Encrypted installs autologin because the LUKS prompt is the auth
# boundary; unencrypted installs leave SDDM as the auth screen.
# ─────────────────────────────────────────────────────────────────────────────
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 / "99-omarchy-login.conf").write_text(
"[Theme]\nCurrent=omarchy\n\n"
"[Users]\nRememberLastUser=true\nRememberLastSession=true\n"
)
(sddm_dir / "autologin.conf").write_text(
"[Autologin]\n"
f"User={ctx.username}\n"
"Session=omarchy.desktop\n"
)
autologin_conf = sddm_dir / "autologin.conf"
if ctx.encrypt:
autologin_conf.write_text(
"[Autologin]\n"
f"User={ctx.username}\n"
"Session=omarchy.desktop\n"
)
else:
autologin_conf.unlink(missing_ok=True)
state_dir = ctx.target / "var" / "lib" / "sddm"
state_dir.mkdir(parents=True, exist_ok=True)