From 17532793fdccea862b6c9a080b55b85c7a4b5321 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Sat, 1 Aug 2026 21:00:56 -0400 Subject: [PATCH] [INSTALL SPEED] Configure target keyboard without temporary boot (#93) * Configure target keyboard without temporary boot * Slim the offline keyboard configuration to what Omarchy consumes Drop the Xorg 00-keyboard.conf writer: the installed system only reads the XKB* settings in vconsole.conf (via detect-keyboard-layout.sh, which copies XKBLAYOUT/XKBVARIANT into Hyprland's kb_layout), and nothing on an Omarchy system reads xorg.conf.d. systemd-firstboot already writes those vconsole settings. Drop the set_keyboard_language monkey-patch: passing an empty kb_layout to minimal_installation takes archinstall's documented skip path (no nspawn boot), and the keymap is configured offline right after instead. Append the preserved FONT= line instead of reproducing localectl's exact file layout, and slim the tests to the behavior that matters: XKB settings present, font preserved, every configurator keymap valid, and archinstall-compatible invalid/empty handling. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: David Heinemeier Hansson Co-authored-by: Claude Fable 5 --- .../omarchy-iso/orchestrator/keyboard.py | 62 ++++++++++++++++ .../omarchy-iso/orchestrator/phases_impl.py | 16 ++++- test/__init__.py | 0 test/test_keyboard.py | 70 +++++++++++++++++++ 4 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 configs/airootfs/usr/share/omarchy-iso/orchestrator/keyboard.py create mode 100644 test/__init__.py create mode 100644 test/test_keyboard.py diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/keyboard.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/keyboard.py new file mode 100644 index 0000000..3286241 --- /dev/null +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/keyboard.py @@ -0,0 +1,62 @@ +"""Offline target keyboard configuration. + +archinstall's set_keyboard_language boots the installed system in a +systemd-nspawn container just to run localectl. systemd-firstboot --root +produces the part of that output Omarchy actually consumes — KEYMAP for the +console plus the XKB* settings in vconsole.conf that omarchy's +detect-keyboard-layout.sh copies into Hyprland's kb_layout — without booting +anything. The Xorg 00-keyboard.conf that localectl also writes is not +generated: nothing on an Omarchy system reads it. +""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + + +def configure_keyboard(target: Path, language: str) -> bool: + """Write the console keymap into a mounted target without booting it. + + Returns False for layouts localectl doesn't know (the configurator offers + two, ba and khmer), matching archinstall: warn and keep the default. + """ + if not language.strip(): + return True + + result = subprocess.run( + ["localectl", "--no-pager", "list-keymaps"], + check=False, + text=True, + capture_output=True, + ) + if result.returncode != 0: + detail = result.stderr.strip() or "localectl returned an error" + raise RuntimeError(f"Unable to list keyboard layouts: {detail}") + if language.lower() not in {layout.lower() for layout in result.stdout.splitlines()}: + return False + + # systemd-firstboot --force rewrites vconsole.conf wholesale, dropping the + # FONT= line archinstall's set_vconsole wrote earlier. + vconsole_path = target / "etc" / "vconsole.conf" + font = None + if vconsole_path.exists(): + font = next( + (line for line in vconsole_path.read_text().splitlines() if line.startswith("FONT=")), + None, + ) + + result = subprocess.run( + ["systemd-firstboot", f"--root={target}", f"--keymap={language}", "--force"], + check=False, + text=True, + capture_output=True, + ) + if result.returncode != 0: + detail = result.stderr.strip() or "systemd-firstboot returned an error" + raise RuntimeError(f"Unable to configure keyboard layout {language}: {detail}") + + if font: + vconsole_path.write_text(vconsole_path.read_text() + font + "\n") + + return True 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 d47bcdf..468636a 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -27,11 +27,13 @@ import shutil import subprocess import textwrap import time +from dataclasses import replace from pathlib import Path from . import archinstall_adapter as arch from .context import InstallContext -from .ui import info +from .keyboard import configure_keyboard +from .ui import error, info # Package targets are written by builder/build-iso.sh. Stable ISOs use the @@ -246,6 +248,10 @@ def arch_install_system(ctx: InstallContext) -> None: _mask_mkinitcpio_pacman_hooks(ctx) try: info("› installing base system (mkinitcpio deferred to final Limine UKI build)") + # An empty kb_layout makes archinstall's set_keyboard_language skip + # booting the target in a container just to run localectl; the + # keymap is configured offline right after instead. + kb_layout = config.locale_config.kb_layout if config.locale_config else "" installer.minimal_installation( optional_repositories=( config.mirror_config.optional_repositories @@ -253,10 +259,16 @@ def arch_install_system(ctx: InstallContext) -> None: ), mkinitcpio=False, hostname=config.hostname, - locale_config=config.locale_config, + locale_config=( + replace(config.locale_config, kb_layout="") + if config.locale_config else None + ), pacman_config=config.pacman_config, ) + if not configure_keyboard(installer.target, kb_layout): + error(f"Invalid keyboard language specified: {kb_layout}") + if config.mirror_config: installer.set_mirrors(mirror_handler, config.mirror_config, on_target=True) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_keyboard.py b/test/test_keyboard.py new file mode 100644 index 0000000..a0103b4 --- /dev/null +++ b/test/test_keyboard.py @@ -0,0 +1,70 @@ +#!/usr/bin/python + +import importlib.util +import re +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +MODULE_PATH = ROOT / "configs/airootfs/usr/share/omarchy-iso/orchestrator/keyboard.py" +SPEC = importlib.util.spec_from_file_location("installer_keyboard", MODULE_PATH) +KEYBOARD = importlib.util.module_from_spec(SPEC) +assert SPEC.loader +SPEC.loader.exec_module(KEYBOARD) + +CONFIGURATOR = (ROOT / "configs/airootfs/root/configurator").read_text() +KEYBOARD_BLOCK = re.search(r"keyboards=\$'(.*?)'\n choice=", CONFIGURATOR, re.DOTALL) +assert KEYBOARD_BLOCK +SUPPORTED_KEYMAPS = [ + line.split("|", 1)[1] + for line in KEYBOARD_BLOCK.group(1).splitlines() +] +UNSUPPORTED_KEYMAPS = {"ba", "khmer"} + + +class KeyboardConfigurationTest(unittest.TestCase): + def target(self, directory: str) -> Path: + target = Path(directory) + (target / "etc").mkdir() + (target / "etc/vconsole.conf").write_text( + "KEYMAP=us\nFONT=default8x16\n" + ) + return target + + def test_writes_keymap_and_xkb_settings_and_preserves_font(self): + # XKBLAYOUT is load-bearing: omarchy's detect-keyboard-layout.sh copies + # it into Hyprland's kb_layout on the installed system. + with tempfile.TemporaryDirectory() as directory: + target = self.target(directory) + self.assertTrue(KEYBOARD.configure_keyboard(target, "us")) + lines = (target / "etc/vconsole.conf").read_text().splitlines() + self.assertIn("KEYMAP=us", lines) + self.assertIn("XKBLAYOUT=us", lines) + self.assertEqual(lines.count("FONT=default8x16"), 1) + + def test_all_configurator_keymaps_are_known_to_localectl(self): + for keymap in SUPPORTED_KEYMAPS: + with self.subTest(keymap=keymap), tempfile.TemporaryDirectory() as directory: + target = self.target(directory) + configured = KEYBOARD.configure_keyboard(target, keymap) + self.assertEqual(configured, keymap not in UNSUPPORTED_KEYMAPS) + if configured: + lines = (target / "etc/vconsole.conf").read_text().splitlines() + self.assertIn(f"KEYMAP={keymap}", lines) + self.assertEqual(lines.count("FONT=default8x16"), 1) + + def test_unknown_and_empty_keymaps_match_archinstall_behavior(self): + for keymap, expected in (("definitely-not-a-keymap", False), ("", True)): + with self.subTest(keymap=keymap), tempfile.TemporaryDirectory() as directory: + target = self.target(directory) + self.assertEqual(KEYBOARD.configure_keyboard(target, keymap), expected) + self.assertEqual( + (target / "etc/vconsole.conf").read_text(), + "KEYMAP=us\nFONT=default8x16\n", + ) + + +if __name__ == "__main__": + unittest.main()