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:
Softer 2026-04-22 14:32:41 +03:00 committed by GitHub
parent d1765323d8
commit e7d38d0e82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 95 additions and 15 deletions

View File

@ -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

View File

@ -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)))

View File

@ -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

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,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)

View File

@ -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

View File

@ -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