Skip to content

Commit e7d38d0

Browse files
authored
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()
1 parent d176532 commit e7d38d0

6 files changed

Lines changed: 95 additions & 15 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from dataclasses import dataclass
2+
from enum import Enum, auto
3+
from pathlib import Path
4+
5+
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
6+
from archinstall.lib.models.device import DiskLayoutConfiguration
7+
8+
9+
class BootloaderValidationFailureKind(Enum):
10+
LimineNonFatBoot = auto()
11+
LimineLayout = auto()
12+
13+
14+
@dataclass(frozen=True)
15+
class BootloaderValidationFailure:
16+
kind: BootloaderValidationFailureKind
17+
description: str
18+
19+
20+
def validate_bootloader_layout(
21+
bootloader_config: BootloaderConfiguration | None,
22+
disk_config: DiskLayoutConfiguration | None,
23+
) -> BootloaderValidationFailure | None:
24+
"""Validate bootloader configuration against disk layout.
25+
26+
Returns a failure with a human-readable description if the configuration
27+
would produce an unbootable system, or None if it is valid.
28+
"""
29+
if not (bootloader_config and disk_config):
30+
return None
31+
32+
if bootloader_config.bootloader == Bootloader.Limine:
33+
boot_part = next(
34+
(p for m in disk_config.device_modifications if (p := m.get_boot_partition())),
35+
None,
36+
)
37+
38+
# Limine reads its config and kernels from the boot partition, which
39+
# must be FAT.
40+
if boot_part and (boot_part.fs_type is None or not boot_part.fs_type.is_fat()):
41+
return BootloaderValidationFailure(
42+
kind=BootloaderValidationFailureKind.LimineNonFatBoot,
43+
description='Limine does not support booting with a non-FAT boot partition.',
44+
)
45+
46+
# When the ESP is the boot partition but mounted outside /boot and
47+
# UKI is disabled, kernels end up on the root filesystem which
48+
# Limine cannot access.
49+
if not bootloader_config.uki:
50+
efi_part = next(
51+
(p for m in disk_config.device_modifications if (p := m.get_efi_partition())),
52+
None,
53+
)
54+
if efi_part and efi_part == boot_part and efi_part.mountpoint != Path('/boot'):
55+
return BootloaderValidationFailure(
56+
kind=BootloaderValidationFailureKind.LimineLayout,
57+
description=(
58+
f'Limine requires kernels on a FAT partition. The ESP is mounted at {efi_part.mountpoint}, '
59+
'enable UKI or add a separate /boot partition to install Limine.'
60+
),
61+
)
62+
63+
return None

archinstall/lib/disk/device_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def format(
250250
case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4:
251251
# Force create
252252
options.append('-F')
253-
case FilesystemType.FAT12 | FilesystemType.FAT16 | FilesystemType.FAT32:
253+
case _ if fs_type.is_fat():
254254
mkfs_type = 'fat'
255255
# Set FAT size
256256
options.extend(('-F', fs_type.value.removeprefix(mkfs_type)))

archinstall/lib/global_menu.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from archinstall.lib.args import ArchConfig
66
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
77
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
8+
from archinstall.lib.bootloader.utils import validate_bootloader_layout
89
from archinstall.lib.configuration import save_config
910
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
1011
from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone
@@ -17,7 +18,7 @@
1718
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
1819
from archinstall.lib.models.authentication import AuthenticationConfiguration
1920
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
20-
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, FilesystemType, PartitionModification
21+
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, PartitionModification
2122
from archinstall.lib.models.locale import LocaleConfiguration
2223
from archinstall.lib.models.mirrors import MirrorConfiguration
2324
from archinstall.lib.models.network import NetworkConfiguration, NicType
@@ -486,16 +487,14 @@ def _validate_bootloader(self) -> str | None:
486487
if efi_partition is None:
487488
return 'EFI system partition (ESP) not found'
488489

489-
if efi_partition.fs_type not in [FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32]:
490+
if efi_partition.fs_type is None or not efi_partition.fs_type.is_fat():
490491
return 'ESP must be formatted as a FAT filesystem'
491492

492-
if bootloader == Bootloader.Limine:
493-
if boot_partition.fs_type not in [FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32]:
494-
return 'Limine does not support booting with a non-FAT boot partition'
493+
if bootloader == Bootloader.Refind and not self._uefi:
494+
return 'rEFInd can only be used on UEFI systems'
495495

496-
elif bootloader == Bootloader.Refind:
497-
if not self._uefi:
498-
return 'rEFInd can only be used on UEFI systems'
496+
if failure := validate_bootloader_layout(bootloader_config, disk_config):
497+
return failure.description
499498

500499
return None
501500

archinstall/lib/installer.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import Any, Self
1313

1414
from archinstall.lib.boot import Boot
15+
from archinstall.lib.bootloader.utils import validate_bootloader_layout
1516
from archinstall.lib.command import SysCommand, run
1617
from archinstall.lib.disk.fido import Fido2
1718
from archinstall.lib.disk.luks import Luks2, unlock_luks2_dev
@@ -30,7 +31,7 @@
3031
from archinstall.lib.locale.utils import verify_keyboard_layout, verify_x11_keyboard_layout
3132
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
3233
from archinstall.lib.models.application import ZramAlgorithm
33-
from archinstall.lib.models.bootloader import Bootloader
34+
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
3435
from archinstall.lib.models.device import (
3536
DiskEncryption,
3637
DiskLayoutConfiguration,
@@ -1466,6 +1467,14 @@ def _add_limine_bootloader(
14661467
elif not efi_partition.mountpoint:
14671468
raise ValueError('EFI partition is not mounted')
14681469

1470+
# Safety net for programmatic callers that bypass GlobalMenu and
1471+
# guided.py validation.
1472+
if failure := validate_bootloader_layout(
1473+
BootloaderConfiguration(bootloader=Bootloader.Limine, uki=uki_enabled),
1474+
self._disk_config,
1475+
):
1476+
raise DiskError(failure.description)
1477+
14691478
info(f'Limine EFI partition: {efi_partition.dev_path}')
14701479

14711480
parent_dev_path = get_parent_device_path(efi_partition.safe_dev_path)
@@ -1476,15 +1485,11 @@ def _add_limine_bootloader(
14761485
if bootloader_removable:
14771486
efi_dir_path = efi_dir_path / 'BOOT'
14781487
efi_dir_path_target = efi_dir_path_target / 'BOOT'
1479-
1480-
boot_limine_path = self.target / 'boot' / 'limine'
1481-
boot_limine_path.mkdir(parents=True, exist_ok=True)
1482-
config_path = boot_limine_path / 'limine.conf'
14831488
else:
14841489
efi_dir_path = efi_dir_path / 'arch-limine'
14851490
efi_dir_path_target = efi_dir_path_target / 'arch-limine'
14861491

1487-
config_path = efi_dir_path / 'limine.conf'
1492+
config_path = efi_dir_path / 'limine.conf'
14881493

14891494
efi_dir_path.mkdir(parents=True, exist_ok=True)
14901495

archinstall/lib/models/device.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,9 @@ class FilesystemType(StrEnum):
802802
def is_crypto(self) -> bool:
803803
return self == FilesystemType.CRYPTO_LUKS
804804

805+
def is_fat(self) -> bool:
806+
return self in (FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32)
807+
805808
@property
806809
def parted_value(self) -> str:
807810
return self.value + '(v1)' if self == FilesystemType.LINUX_SWAP else self.value

archinstall/scripts/guided.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from archinstall.lib.applications.application_handler import ApplicationHandler
66
from archinstall.lib.args import ArchConfig, ArchConfigHandler
77
from archinstall.lib.authentication.authentication_handler import AuthenticationHandler
8+
from archinstall.lib.bootloader.utils import validate_bootloader_layout
89
from archinstall.lib.configuration import ConfigurationOutput
910
from archinstall.lib.disk.filesystem import FilesystemHandler
1011
from archinstall.lib.disk.utils import disk_layouts
@@ -211,6 +212,15 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None:
211212
config.write_debug()
212213
config.save()
213214

215+
# Safety net for silent/config-file flow. The TUI menu blocks Install via
216+
# GlobalMenu._validate_bootloader() before reaching this point.
217+
if failure := validate_bootloader_layout(
218+
arch_config_handler.config.bootloader_config,
219+
arch_config_handler.config.disk_config,
220+
):
221+
error(failure.description)
222+
return
223+
214224
if arch_config_handler.args.dry_run:
215225
return
216226

0 commit comments

Comments
 (0)