Fix Limine install with ESP mounted outside /boot (#4442)
* Fix Limine install with ESP mounted outside /boot Place limine.conf next to the EFI binary on the ESP so it is found regardless of ESP mountpoint, and block unbootable layouts (non-UKI Limine with ESP not at /boot and no separate /boot partition) in GlobalMenu validation, guided.main() and _add_limine_bootloader(). Fixes #4333 * Extract bootloader layout validation into lib/bootloader/utils * 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. * Encapsulate FAT filesystem detection in FilesystemType.is_fat()
This commit is contained in:
parent
d1765323d8
commit
e7d38d0e82
|
|
@ -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
|
||||
|
|
@ -250,7 +250,7 @@ class DeviceHandler:
|
|||
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)))
|
||||
|
|
|
|||
|
|
@ -5,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
|
||||
|
|
@ -17,7 +18,7 @@ from archinstall.lib.mirror.mirror_menu import MirrorMenu
|
|||
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 @@ class GlobalMenu(AbstractMenu[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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,6 +1467,14 @@ class Installer:
|
|||
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 @@ class Installer:
|
|||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import time
|
|||
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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue