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 6d486245..5b74150c 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -1288,6 +1288,8 @@ def finalize_limine_boot(ctx: InstallContext) -> None: if "root=" not in cmdline: raise RuntimeError(f"cmdline parsed from {default_limine} has no root=: {cmdline}") + _configure_initramfs_encryption_hook(ctx) + esp_path = _limine_setting(config_text, "ESP_PATH", "/boot") or "/boot" esp_root = ctx.target / esp_path.lstrip("/") if not esp_root.is_dir(): @@ -1314,6 +1316,49 @@ def finalize_limine_boot(ctx: InstallContext) -> None: raise RuntimeError(f"encrypted install but {limine_conf} has no cryptdevice=") +def _configure_initramfs_encryption_hook(ctx: InstallContext) -> None: + """Include the busybox encrypt hook exactly when the root uses LUKS. + + omarchy-settings ships a general-purpose drop-in containing ``encrypt``. + On an unencrypted installation that hook treats ``root=`` as the legacy + encrypted-device syntax, emits a scary boot error, and then falls through. + Keep the hook for encrypted installs and remove only that token otherwise. + """ + hooks_path = ctx.target / "etc" / "mkinitcpio.conf.d" / "omarchy_hooks.conf" + if not hooks_path.exists(): + raise RuntimeError(f"{hooks_path} missing") + + encrypted = _provision_install_encrypted(ctx) + output: list[str] = [] + found = False + changed = False + pattern = re.compile(r"^(\s*HOOKS=\()([^)]*)(\).*)$") + for line in hooks_path.read_text().splitlines(): + match = pattern.match(line) + if not match: + output.append(line) + continue + + found = True + hooks = match.group(2).split() + has_encrypt = "encrypt" in hooks + if encrypted and not has_encrypt: + try: + hooks.insert(hooks.index("filesystems"), "encrypt") + except ValueError: + hooks.append("encrypt") + changed = True + elif not encrypted and has_encrypt: + hooks = [hook for hook in hooks if hook != "encrypt"] + changed = True + output.append(f"{match.group(1)}{' '.join(hooks)}{match.group(3)}") + + if not found: + raise RuntimeError(f"{hooks_path} has no HOOKS=(...) assignment") + if changed: + hooks_path.write_text("\n".join(output) + "\n") + + def _strip_shell_quotes(value: str) -> str: value = value.strip() if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"): diff --git a/test/unit/test_provisioning_state.py b/test/unit/test_provisioning_state.py index 7386f001..755475e7 100644 --- a/test/unit/test_provisioning_state.py +++ b/test/unit/test_provisioning_state.py @@ -279,6 +279,45 @@ def test_deferred_provisioning_pre_encrypted_without_passphrase_fails(self): phases_impl.stage_provisioning_state(ctx) +class InitramfsEncryptionHookTest(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.target = Path(self.tmp.name) + self.hooks = self.target / "etc/mkinitcpio.conf.d/omarchy_hooks.conf" + self.hooks.parent.mkdir(parents=True) + + def test_unencrypted_install_removes_encrypt_hook(self): + self.hooks.write_text( + "HOOKS=(base udev block encrypt filesystems fsck)\n" + ) + phases_impl._configure_initramfs_encryption_hook(make_ctx(self.target)) + self.assertEqual( + self.hooks.read_text(), + "HOOKS=(base udev block filesystems fsck)\n", + ) + + def test_encrypted_install_keeps_encrypt_hook(self): + self.hooks.write_text( + "HOOKS=(base udev block encrypt filesystems fsck)\n" + ) + ctx = make_ctx( + self.target, + user_configuration={"disk_config": {"disk_encryption": { + "encryption_type": "luks", + "encryption_password": "secret", + }}}, + ) + phases_impl._configure_initramfs_encryption_hook(ctx) + self.assertIn(" block encrypt filesystems ", self.hooks.read_text()) + + def test_encrypted_install_restores_missing_encrypt_hook(self): + self.hooks.write_text("HOOKS=(base udev block filesystems fsck)\n") + ctx = make_ctx(self.target, encrypt=True) + phases_impl._configure_initramfs_encryption_hook(ctx) + self.assertIn(" block encrypt filesystems ", self.hooks.read_text()) + + class ConfigureLoginDeferProvisioningTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory()