Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions archinstall/lib/bootloader/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +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, FilesystemType

_FAT_FILESYSTEMS = (FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be a member function of the FilesystemType instead? That would it's encapsulate there and not spread across the codebase

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... Yes, you're right. Reworked it.



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 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
11 changes: 5 additions & 6 deletions archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -489,13 +490,11 @@ def _validate_bootloader(self) -> str | 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 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

Expand Down
17 changes: 11 additions & 6 deletions archinstall/lib/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down
10 changes: 10 additions & 0 deletions archinstall/scripts/guided.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down