Consolidate Limine layout validation in bootloader utils

Move the boot-partition FAT check from GlobalMenu into
validate_bootloader_layout so all three call sites (GlobalMenu,
guided.py, Installer._add_limine_bootloader) share one function.

Return a BootloaderValidationFailure dataclass (kind + description)
instead of str | None, so callers can match on the failure kind and
the description is built where partition context is in scope.
This commit is contained in:
Softer 2026-04-21 16:59:11 +03:00
parent d7f6b7f871
commit 70a9797774
4 changed files with 66 additions and 45 deletions

View File

@ -1,36 +1,65 @@
from dataclasses import dataclass
from enum import Enum, auto
from pathlib import Path
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration, FilesystemType
_FAT_FILESYSTEMS = (FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32)
class BootloaderValidationFailureKind(Enum):
LimineNonFatBoot = auto()
LimineLayout = auto()
@dataclass(frozen=True)
class BootloaderValidationFailure:
kind: BootloaderValidationFailureKind
description: str
def validate_bootloader_layout(
bootloader_config: BootloaderConfiguration | None,
disk_config: DiskLayoutConfiguration | None,
) -> str | None:
) -> BootloaderValidationFailure | None:
"""Validate bootloader configuration against disk layout.
Returns an error message if the configuration would produce an
unbootable system, or None if it is valid.
Returns a failure with a human-readable description if the configuration
would produce an unbootable system, or None if it is valid.
"""
# Limine can only read FAT. When the ESP is the boot partition but
# mounted outside /boot and UKI is disabled, the kernel ends up on the
# root filesystem which Limine cannot access.
if not (bootloader_config and bootloader_config.bootloader == Bootloader.Limine and not bootloader_config.uki and disk_config):
if not (bootloader_config and disk_config):
return None
efi_part = next(
(p for m in disk_config.device_modifications if (p := m.get_efi_partition())),
None,
)
boot_part = next(
(p for m in disk_config.device_modifications if (p := m.get_boot_partition())),
None,
)
if efi_part and boot_part == efi_part and efi_part.mountpoint != Path('/boot'):
return (
f'Limine requires kernels on a FAT partition. The ESP is mounted at {efi_part.mountpoint}, '
'enable UKI or add a separate /boot partition to install Limine.'
if bootloader_config.bootloader == Bootloader.Limine:
boot_part = next(
(p for m in disk_config.device_modifications if (p := m.get_boot_partition())),
None,
)
# Limine reads its config and kernels from the boot partition, which
# must be FAT.
if boot_part and boot_part.fs_type not in _FAT_FILESYSTEMS:
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.LimineNonFatBoot,
description='Limine does not support booting with a non-FAT boot partition.',
)
# When the ESP is the boot partition but mounted outside /boot and
# UKI is disabled, kernels end up on the root filesystem which
# Limine cannot access.
if not bootloader_config.uki:
efi_part = next(
(p for m in disk_config.device_modifications if (p := m.get_efi_partition())),
None,
)
if efi_part and efi_part == boot_part and efi_part.mountpoint != Path('/boot'):
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.LimineLayout,
description=(
f'Limine requires kernels on a FAT partition. The ESP is mounted at {efi_part.mountpoint}, '
'enable UKI or add a separate /boot partition to install Limine.'
),
)
return None

View File

@ -1,4 +1,3 @@
from pathlib import Path
from typing import override
from archinstall.default_profiles.profile import GreeterType
@ -6,6 +5,7 @@ from archinstall.lib.applications.application_menu import ApplicationMenu
from archinstall.lib.args import ArchConfig
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.configuration import save_config
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone
@ -490,18 +490,11 @@ class GlobalMenu(AbstractMenu[None]):
if efi_partition.fs_type not in [FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32]:
return 'ESP must be formatted as a FAT filesystem'
if bootloader == Bootloader.Limine:
if boot_partition.fs_type not in [FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32]:
return 'Limine does not support booting with a non-FAT boot partition'
if self._uefi and efi_partition and boot_partition == efi_partition and efi_partition.mountpoint != Path('/boot') and not bootloader_config.uki:
return (
f'Limine requires kernels on a FAT partition. The ESP is mounted at {efi_partition.mountpoint}, '
'enable UKI or add a separate /boot partition'
)
if bootloader == Bootloader.Refind and not self._uefi:
return 'rEFInd can only be used on UEFI systems'
elif bootloader == Bootloader.Refind:
if not self._uefi:
return 'rEFInd can only be used on UEFI systems'
if failure := validate_bootloader_layout(bootloader_config, disk_config):
return failure.description
return None

View File

@ -12,6 +12,7 @@ from types import TracebackType
from typing import Any, Self
from archinstall.lib.boot import Boot
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.command import SysCommand, run
from archinstall.lib.disk.fido import Fido2
from archinstall.lib.disk.luks import Luks2, unlock_luks2_dev
@ -30,7 +31,7 @@ from archinstall.lib.linux_path import LPath
from archinstall.lib.locale.utils import verify_keyboard_layout, verify_x11_keyboard_layout
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.models.application import ZramAlgorithm
from archinstall.lib.models.bootloader import Bootloader
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import (
DiskEncryption,
DiskLayoutConfiguration,
@ -1466,15 +1467,13 @@ class Installer:
elif not efi_partition.mountpoint:
raise ValueError('EFI partition is not mounted')
# Limine can only read FAT filesystems. When the ESP doubles as
# the boot partition but is mounted outside /boot (e.g. /efi,
# /boot/efi) and UKI is disabled, kernels end up on the root
# filesystem under /boot/ which Limine cannot access.
if boot_partition == efi_partition and efi_partition.mountpoint != Path('/boot') and not uki_enabled:
raise DiskError(
f'Limine requires kernels on a FAT partition. The ESP is mounted at {efi_partition.mountpoint}, '
'so enable UKI or add a separate /boot partition to install Limine.',
)
# Safety net for programmatic callers that bypass GlobalMenu and
# guided.py validation.
if failure := validate_bootloader_layout(
BootloaderConfiguration(bootloader=Bootloader.Limine, uki=uki_enabled),
self._disk_config,
):
raise DiskError(failure.description)
info(f'Limine EFI partition: {efi_partition.dev_path}')

View File

@ -214,11 +214,11 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None:
# Safety net for silent/config-file flow. The TUI menu blocks Install via
# GlobalMenu._validate_bootloader() before reaching this point.
if err_msg := validate_bootloader_layout(
if failure := validate_bootloader_layout(
arch_config_handler.config.bootloader_config,
arch_config_handler.config.disk_config,
):
error(err_msg)
error(failure.description)
return
if arch_config_handler.args.dry_run: