diff --git a/archinstall/lib/bootloader/utils.py b/archinstall/lib/bootloader/utils.py index 0f1ae9b5..3d77bafe 100644 --- a/archinstall/lib/bootloader/utils.py +++ b/archinstall/lib/bootloader/utils.py @@ -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 diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 2fe28cef..57405985 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -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 diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 9978d647..3dbcc8f6 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -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}') diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 30cb14da..5cdcdad9 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -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: