diff --git a/archinstall/lib/bootloader/utils.py b/archinstall/lib/bootloader/utils.py new file mode 100644 index 0000000000..a732f4e117 --- /dev/null +++ b/archinstall/lib/bootloader/utils.py @@ -0,0 +1,63 @@ +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 + + +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, +) -> BootloaderValidationFailure | None: + """Validate bootloader configuration against disk layout. + + Returns a failure with a human-readable description if the configuration + would produce an unbootable system, or None if it is valid. + """ + if not (bootloader_config and disk_config): + return None + + 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 is None or not boot_part.fs_type.is_fat()): + 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/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 7ac05a3120..2e51d3d226 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -250,7 +250,7 @@ def format( case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4: # Force create options.append('-F') - case FilesystemType.FAT12 | FilesystemType.FAT16 | FilesystemType.FAT32: + case _ if fs_type.is_fat(): mkfs_type = 'fat' # Set FAT size options.extend(('-F', fs_type.value.removeprefix(mkfs_type))) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 2baef93d41..3d94f31e2b 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -5,6 +5,7 @@ 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 @@ -17,7 +18,7 @@ from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration -from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, FilesystemType, PartitionModification +from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, PartitionModification from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.network import NetworkConfiguration, NicType @@ -486,16 +487,14 @@ def _validate_bootloader(self) -> str | None: if efi_partition is None: return 'EFI system partition (ESP) not found' - if efi_partition.fs_type not in [FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32]: + if efi_partition.fs_type is None or not efi_partition.fs_type.is_fat(): 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 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 9f8c51b2bd..3dbcc8f670 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -12,6 +12,7 @@ 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.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,6 +1467,14 @@ def _add_limine_bootloader( elif not efi_partition.mountpoint: raise ValueError('EFI partition is not mounted') + # 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}') parent_dev_path = get_parent_device_path(efi_partition.safe_dev_path) @@ -1476,15 +1485,11 @@ def _add_limine_bootloader( if bootloader_removable: efi_dir_path = efi_dir_path / 'BOOT' efi_dir_path_target = efi_dir_path_target / 'BOOT' - - boot_limine_path = self.target / 'boot' / 'limine' - boot_limine_path.mkdir(parents=True, exist_ok=True) - config_path = boot_limine_path / 'limine.conf' else: efi_dir_path = efi_dir_path / 'arch-limine' efi_dir_path_target = efi_dir_path_target / 'arch-limine' - config_path = efi_dir_path / 'limine.conf' + config_path = efi_dir_path / 'limine.conf' efi_dir_path.mkdir(parents=True, exist_ok=True) diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py index 3015833d80..7fa05407f9 100644 --- a/archinstall/lib/models/device.py +++ b/archinstall/lib/models/device.py @@ -802,6 +802,9 @@ class FilesystemType(StrEnum): def is_crypto(self) -> bool: return self == FilesystemType.CRYPTO_LUKS + def is_fat(self) -> bool: + return self in (FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32) + @property def parted_value(self) -> str: return self.value + '(v1)' if self == FilesystemType.LINUX_SWAP else self.value diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 3413372253..5cdcdad9d2 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -5,6 +5,7 @@ from archinstall.lib.applications.application_handler import ApplicationHandler from archinstall.lib.args import ArchConfig, ArchConfigHandler from archinstall.lib.authentication.authentication_handler import AuthenticationHandler +from archinstall.lib.bootloader.utils import validate_bootloader_layout from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.disk.filesystem import FilesystemHandler from archinstall.lib.disk.utils import disk_layouts @@ -211,6 +212,15 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None: config.write_debug() config.save() + # Safety net for silent/config-file flow. The TUI menu blocks Install via + # GlobalMenu._validate_bootloader() before reaching this point. + if failure := validate_bootloader_layout( + arch_config_handler.config.bootloader_config, + arch_config_handler.config.disk_config, + ): + error(failure.description) + return + if arch_config_handler.args.dry_run: return