diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index 48a379b09a..e2ab864f89 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -15,7 +15,7 @@ from archinstall.lib.crypt import decrypt from archinstall.lib.models.application import ApplicationConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration -from archinstall.lib.models.bootloader import Bootloader +from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration @@ -64,8 +64,7 @@ class ArchConfig: profile_config: ProfileConfiguration | None = None mirror_config: MirrorConfiguration | None = None network_config: NetworkConfiguration | None = None - bootloader: Bootloader | None = None - uki: bool = False + bootloader_config: BootloaderConfiguration | None = None app_config: ApplicationConfiguration | None = None auth_config: AuthenticationConfiguration | None = None hostname: str = 'archlinux' @@ -102,7 +101,6 @@ def safe_json(self) -> dict[str, Any]: 'archinstall-language': self.archinstall_language.json(), 'hostname': self.hostname, 'kernels': self.kernels, - 'uki': self.uki, 'ntp': self.ntp, 'packages': self.packages, 'parallel_downloads': self.parallel_downloads, @@ -110,7 +108,7 @@ def safe_json(self) -> dict[str, Any]: 'timezone': self.timezone, 'services': self.services, 'custom_commands': self.custom_commands, - 'bootloader': self.bootloader.json() if self.bootloader else None, + 'bootloader_config': self.bootloader_config.json() if self.bootloader_config else None, 'app_config': self.app_config.json() if self.app_config else None, 'auth_config': self.auth_config.json() if self.auth_config else None, } @@ -179,13 +177,15 @@ def from_config(cls, args_config: dict[str, Any], args: Arguments) -> 'ArchConfi if net_config := args_config.get('network_config', None): arch_config.network_config = NetworkConfiguration.parse_arg(net_config) - if bootloader_config := args_config.get('bootloader', None): - arch_config.bootloader = Bootloader.from_arg(bootloader_config, args.skip_boot) - - arch_config.uki = args_config.get('uki', False) - - if args_config.get('uki') and (arch_config.bootloader is None or not arch_config.bootloader.has_uki_support()): - arch_config.uki = False + if bootloader_config_dict := args_config.get('bootloader_config', None): + arch_config.bootloader_config = BootloaderConfiguration.parse_arg(bootloader_config_dict, args.skip_boot) + # DEPRECATED: separate bootloader and uki fields (backward compatibility) + elif bootloader_str := args_config.get('bootloader', None): + bootloader = Bootloader.from_arg(bootloader_str, args.skip_boot) + uki = args_config.get('uki', False) + if uki and not bootloader.has_uki_support(): + uki = False + arch_config.bootloader_config = BootloaderConfiguration(bootloader=bootloader, uki=uki, removable=False) # deprecated: backwards compatibility audio_config_args = args_config.get('audio_config', None) diff --git a/archinstall/lib/bootloader/__init__.py b/archinstall/lib/bootloader/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/archinstall/lib/bootloader/bootloader_menu.py b/archinstall/lib/bootloader/bootloader_menu.py new file mode 100644 index 0000000000..c127186214 --- /dev/null +++ b/archinstall/lib/bootloader/bootloader_menu.py @@ -0,0 +1,229 @@ +import textwrap +from typing import override + +from archinstall.lib.translationhandler import tr +from archinstall.tui.curses_menu import SelectMenu +from archinstall.tui.menu_item import MenuItem, MenuItemGroup +from archinstall.tui.result import ResultType +from archinstall.tui.types import Alignment, FrameProperties, Orientation + +from ..args import arch_config_handler +from ..hardware import SysInfo +from ..menu.abstract_menu import AbstractSubMenu +from ..models.bootloader import Bootloader, BootloaderConfiguration + + +class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]): + def __init__( + self, + bootloader_conf: BootloaderConfiguration, + ): + self._bootloader_conf = bootloader_conf + menu_options = self._define_menu_options() + + self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True) + super().__init__( + self._item_group, + config=self._bootloader_conf, + allow_reset=False, + ) + + def _define_menu_options(self) -> list[MenuItem]: + bootloader = self._bootloader_conf.bootloader + has_uefi = SysInfo.has_uefi() + + # UKI availability + uki_enabled = has_uefi and bootloader.has_uki_support() + if not uki_enabled: + self._bootloader_conf.uki = False + + # Removable availability + removable_enabled = has_uefi and bootloader.has_removable_support() + if not removable_enabled: + self._bootloader_conf.removable = False + + return [ + MenuItem( + text=tr('Bootloader'), + action=self._select_bootloader, + value=self._bootloader_conf.bootloader, + preview_action=self._prev_bootloader, + mandatory=True, + key='bootloader', + ), + MenuItem( + text=tr('Unified kernel images'), + action=self._select_uki, + value=self._bootloader_conf.uki, + preview_action=self._prev_uki, + key='uki', + enabled=uki_enabled, + ), + MenuItem( + text=tr('Install to removable location'), + action=self._select_removable, + value=self._bootloader_conf.removable, + preview_action=self._prev_removable, + key='removable', + enabled=removable_enabled, + ), + ] + + def _prev_bootloader(self, item: MenuItem) -> str | None: + if item.value: + return f'{tr("Bootloader")}: {item.value.value}' + return None + + def _prev_uki(self, item: MenuItem) -> str | None: + uki_text = f'{tr("Unified kernel images")}' + if item.value: + return f'{uki_text}: {tr("Enabled")}' + else: + return f'{uki_text}: {tr("Disabled")}' + + def _prev_removable(self, item: MenuItem) -> str | None: + if item.value: + return tr('Will install to /EFI/BOOT/ (removable location)') + return tr('Will install to standard location with NVRAM entry') + + @override + def run( + self, + additional_title: str | None = None, + ) -> BootloaderConfiguration: + super().run(additional_title=additional_title) + return self._bootloader_conf + + def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None: + bootloader = ask_for_bootloader(preset) + + if bootloader: + # Update UKI option based on bootloader + uki_item = self._menu_item_group.find_by_key('uki') + if not SysInfo.has_uefi() or not bootloader.has_uki_support(): + uki_item.enabled = False + uki_item.value = False + self._bootloader_conf.uki = False + else: + uki_item.enabled = True + + # Update removable option based on bootloader + removable_item = self._menu_item_group.find_by_key('removable') + if not SysInfo.has_uefi() or not bootloader.has_removable_support(): + removable_item.enabled = False + removable_item.value = False + self._bootloader_conf.removable = False + else: + removable_item.enabled = True + + return bootloader + + def _select_uki(self, preset: bool) -> bool: + prompt = tr('Would you like to use unified kernel images?') + '\n' + + group = MenuItemGroup.yes_no() + group.set_focus_by_value(preset) + + result = SelectMenu[bool]( + group, + header=prompt, + columns=2, + orientation=Orientation.HORIZONTAL, + alignment=Alignment.CENTER, + allow_skip=True, + ).run() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Selection: + return result.item() == MenuItem.yes() + case ResultType.Reset: + raise ValueError('Unhandled result type') + + def _select_removable(self, preset: bool) -> bool: + prompt = ( + tr('Would you like to install the bootloader to the default removable media search location?') + + '\n\n' + + tr('This installs the bootloader to /EFI/BOOT/BOOTX64.EFI (or similar) which is useful for:') + + '\n\n • ' + + tr('USB drives or other portable external media.') + + '\n • ' + + tr('Systems where you want the disk to be bootable on any computer.') + + '\n • ' + + tr('Firmware that does not properly support NVRAM boot entries.') + + '\n\n' + + tr( + textwrap.dedent( + """\ + This is NOT recommended if none of the above apply, as it makes installing multiple + EFI bootloaders on the same disk more challenging, and it overwrites whatever bootloader + was previously installed on the default removable media search location, if any. + """ + ) + ) + + '\n' + ) + + group = MenuItemGroup.yes_no() + group.set_focus_by_value(preset) + + result = SelectMenu[bool]( + group, + header=prompt, + columns=2, + orientation=Orientation.HORIZONTAL, + alignment=Alignment.CENTER, + allow_skip=True, + ).run() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Selection: + return result.item() == MenuItem.yes() + case ResultType.Reset: + raise ValueError('Unhandled result type') + + +def ask_for_bootloader(preset: Bootloader | None) -> Bootloader | None: + options = [] + hidden_options = [] + default = None + header = None + + if arch_config_handler.args.skip_boot: + default = Bootloader.NO_BOOTLOADER + else: + hidden_options += [Bootloader.NO_BOOTLOADER] + + if not SysInfo.has_uefi(): + options += [Bootloader.Grub, Bootloader.Limine] + if not default: + default = Bootloader.Grub + header = tr('UEFI is not detected and some options are disabled') + else: + options += [b for b in Bootloader if b not in hidden_options] + if not default: + default = Bootloader.Systemd + + items = [MenuItem(o.value, value=o) for o in options] + group = MenuItemGroup(items) + group.set_default_by_value(default) + group.set_focus_by_value(preset) + + result = SelectMenu[Bootloader]( + group, + header=header, + alignment=Alignment.CENTER, + frame=FrameProperties.min(tr('Bootloader')), + allow_skip=True, + ).run() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Selection: + return result.get_value() + case ResultType.Reset: + raise ValueError('Unhandled result type') diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 8acafd8a4c..fa2e51764b 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -12,6 +12,7 @@ from .applications.application_menu import ApplicationMenu from .args import ArchConfig from .authentication.authentication_menu import AuthenticationMenu +from .bootloader.bootloader_menu import BootloaderMenu from .configuration import save_config from .hardware import SysInfo from .interactions.general_conf import ( @@ -22,11 +23,11 @@ ask_ntp, ) from .interactions.network_menu import ask_to_configure_network -from .interactions.system_conf import ask_for_bootloader, ask_for_swap, ask_for_uki, select_kernel +from .interactions.system_conf import ask_for_swap, select_kernel from .locale.locale_menu import LocaleMenu from .menu.abstract_menu import CONFIG_KEY, AbstractMenu from .mirrors import MirrorMenu -from .models.bootloader import Bootloader +from .models.bootloader import Bootloader, BootloaderConfiguration from .models.locale import LocaleConfiguration from .models.mirrors import MirrorConfiguration from .models.network import NetworkConfiguration, NicType @@ -86,19 +87,10 @@ def _get_menu_options(self) -> list[MenuItem]: ), MenuItem( text=tr('Bootloader'), - value=Bootloader.get_default(), - action=self._select_bootloader, - preview_action=self._prev_bootloader, - mandatory=True, - key='bootloader', - ), - MenuItem( - text=tr('Unified kernel images'), - value=False, - enabled=SysInfo.has_uefi(), - action=ask_for_uki, - preview_action=self._prev_uki, - key='uki', + value=BootloaderConfiguration.get_default(), + action=self._select_bootloader_config, + preview_action=self._prev_bootloader_config, + key='bootloader_config', ), MenuItem( text=tr('Hostname'), @@ -379,13 +371,6 @@ def _prev_swap(self, item: MenuItem) -> str | None: return output return None - def _prev_uki(self, item: MenuItem) -> str | None: - if item.value is not None: - output = f'{tr("Unified kernel images")}: ' - output += tr('Enabled') if item.value else tr('Disabled') - return output - return None - def _prev_hostname(self, item: MenuItem) -> str | None: if item.value is not None: return f'{tr("Hostname")}: {item.value}' @@ -402,9 +387,10 @@ def _prev_kernel(self, item: MenuItem) -> str | None: return f'{tr("Kernel")}: {kernel}' return None - def _prev_bootloader(self, item: MenuItem) -> str | None: - if item.value is not None: - return f'{tr("Bootloader")}: {item.value.value}' + def _prev_bootloader_config(self, item: MenuItem) -> str | None: + bootloader_config: BootloaderConfiguration | None = item.value + if bootloader_config: + return bootloader_config.preview() return None def _validate_bootloader(self) -> str | None: @@ -418,16 +404,18 @@ def _validate_bootloader(self) -> str | None: XXX: The caller is responsible for wrapping the string with the translation shim if necessary. """ - bootloader: Bootloader | None = None + bootloader_config: BootloaderConfiguration | None = None root_partition: PartitionModification | None = None boot_partition: PartitionModification | None = None efi_partition: PartitionModification | None = None - bootloader = self._item_group.find_by_key('bootloader').value + bootloader_config = self._item_group.find_by_key('bootloader_config').value - if bootloader == Bootloader.NO_BOOTLOADER: + if not bootloader_config or bootloader_config.bootloader == Bootloader.NO_BOOTLOADER: return None + bootloader = bootloader_config.bootloader + if disk_config := self._item_group.find_by_key('disk_config').value: for layout in disk_config.device_modifications: if root_partition := layout.get_root_partition(): @@ -501,18 +489,16 @@ def _select_disk_config( return disk_config - def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None: - bootloader = ask_for_bootloader(preset) + def _select_bootloader_config( + self, + preset: BootloaderConfiguration | None = None, + ) -> BootloaderConfiguration | None: + if preset is None: + preset = BootloaderConfiguration.get_default() - if bootloader: - uki = self._item_group.find_by_key('uki') - if not SysInfo.has_uefi() or not bootloader.has_uki_support(): - uki.value = False - uki.enabled = False - else: - uki.enabled = True + bootloader_config = BootloaderMenu(preset).run() - return bootloader + return bootloader_config def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None: from .profile.profile_menu import ProfileMenu diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 71e3b51712..4bc467ebf7 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1268,6 +1268,7 @@ def _add_grub_bootloader( boot_partition: PartitionModification, root: PartitionModification | LvmVolume, efi_partition: PartitionModification | None, + bootloader_removable: bool = False, ) -> None: debug('Installing grub bootloader') @@ -1311,9 +1312,11 @@ def _add_grub_bootloader( f'--efi-directory={efi_partition.mountpoint}', *boot_dir_arg, '--bootloader-id=GRUB', - '--removable', ] + if bootloader_removable: + add_options.append('--removable') + command.extend(add_options) try: @@ -1354,6 +1357,7 @@ def _add_limine_bootloader( efi_partition: PartitionModification | None, root: PartitionModification | LvmVolume, uki_enabled: bool = False, + bootloader_removable: bool = False, ) -> None: debug('Installing Limine bootloader') @@ -1376,22 +1380,22 @@ def _add_limine_bootloader( info(f'Limine EFI partition: {efi_partition.dev_path}') parent_dev_path = device_handler.get_parent_device_path(efi_partition.safe_dev_path) - is_target_usb = ( - SysCommand( - f'udevadm info --no-pager --query=property --property=ID_BUS --value --name={parent_dev_path}', - ).decode() - == 'usb' - ) try: efi_dir_path = self.target / efi_partition.mountpoint.relative_to('/') / 'EFI' efi_dir_path_target = efi_partition.mountpoint / 'EFI' - if is_target_usb: + 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 / 'limine' - efi_dir_path_target = efi_dir_path_target / 'limine' + 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' efi_dir_path.mkdir(parents=True, exist_ok=True) @@ -1400,13 +1404,11 @@ def _add_limine_bootloader( except Exception as err: raise DiskError(f'Failed to install Limine in {self.target}{efi_partition.mountpoint}: {err}') - config_path = efi_dir_path / 'limine.conf' - hook_command = ( f'/usr/bin/cp /usr/share/limine/BOOTIA32.EFI {efi_dir_path_target}/ && /usr/bin/cp /usr/share/limine/BOOTX64.EFI {efi_dir_path_target}/' ) - if not is_target_usb: + if not bootloader_removable: # Create EFI boot menu entry for Limine. try: with open('/sys/firmware/efi/fw_platform_size') as fw_platform_size: @@ -1415,9 +1417,9 @@ def _add_limine_bootloader( raise OSError(f'Could not open or read /sys/firmware/efi/fw_platform_size to determine EFI bitness: {err}') if efi_bitness == '64': - loader_path = '/EFI/limine/BOOTX64.EFI' + loader_path = '\\EFI\\arch-limine\\BOOTX64.EFI' elif efi_bitness == '32': - loader_path = '/EFI/limine/BOOTIA32.EFI' + loader_path = '\\EFI\\arch-limine\\BOOTIA32.EFI' else: raise ValueError(f'EFI bitness is neither 32 nor 64 bits. Found "{efi_bitness}".') @@ -1428,7 +1430,7 @@ def _add_limine_bootloader( f' --disk {parent_dev_path}' f' --part {efi_partition.partn}' ' --label "Arch Linux Limine Bootloader"' - f' --loader {loader_path}' + f" --loader '{loader_path}'" ' --unicode' ' --verbose', ) @@ -1485,23 +1487,24 @@ def _add_limine_bootloader( path_root = f'uuid({boot_partition.partuuid})' for kernel in self.kernels: - for variant in ('', '-fallback'): - if uki_enabled: - entry = [ - 'protocol: efi', - f'path: boot():/EFI/Linux/arch-{kernel}.efi', - f'cmdline: {kernel_params}', - ] - else: + if uki_enabled: + entry = [ + 'protocol: efi', + f'path: boot():/EFI/Linux/arch-{kernel}.efi', + f'cmdline: {kernel_params}', + ] + config_contents += f'\n/Arch Linux ({kernel})\n' + config_contents += '\n'.join([f' {it}' for it in entry]) + '\n' + else: + for variant in ('', '-fallback'): entry = [ 'protocol: linux', f'path: {path_root}:/vmlinuz-{kernel}', f'cmdline: {kernel_params}', f'module_path: {path_root}:/initramfs-{kernel}{variant}.img', ] - - config_contents += f'\n/Arch Linux ({kernel}{variant})\n' - config_contents += '\n'.join([f' {it}' for it in entry]) + '\n' + config_contents += f'\n/Arch Linux ({kernel}{variant})\n' + config_contents += '\n'.join([f' {it}' for it in entry]) + '\n' config_path.write_text(config_contents) @@ -1612,16 +1615,18 @@ def _config_uki( if not self.mkinitcpio(['-P']): error('Error generating initramfs (continuing anyway)') - def add_bootloader(self, bootloader: Bootloader, uki_enabled: bool = False) -> None: + def add_bootloader(self, bootloader: Bootloader, uki_enabled: bool = False, bootloader_removable: bool = False) -> None: """ Adds a bootloader to the installation instance. Archinstall supports one of three types: * systemd-bootctl * grub - * limine (beta) + * limine * efistub (beta) :param bootloader: Type of bootloader to be added + :param uki_enabled: Whether to use unified kernel images + :param bootloader_removable: Whether to install to removable media location (UEFI only, for GRUB and Limine) """ for plugin in plugins.values(): @@ -1643,6 +1648,20 @@ def add_bootloader(self, bootloader: Bootloader, uki_enabled: bool = False) -> N info(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}') + # validate UKI support + if uki_enabled and not bootloader.has_uki_support(): + warn(f'Bootloader {bootloader.value} does not support UKI; disabling.') + uki_enabled = False + + # validate removable bootloader option + if bootloader_removable: + if not SysInfo.has_uefi(): + warn('Removable install requested but system is not UEFI; disabling.') + bootloader_removable = False + elif not bootloader.has_removable_support(): + warn(f'Bootloader {bootloader.value} lacks removable support; disabling.') + bootloader_removable = False + if uki_enabled: self._config_uki(root, efi_partition) @@ -1650,11 +1669,11 @@ def add_bootloader(self, bootloader: Bootloader, uki_enabled: bool = False) -> N case Bootloader.Systemd: self._add_systemd_bootloader(boot_partition, root, efi_partition, uki_enabled) case Bootloader.Grub: - self._add_grub_bootloader(boot_partition, root, efi_partition) + self._add_grub_bootloader(boot_partition, root, efi_partition, bootloader_removable) case Bootloader.Efistub: self._add_efistub_bootloader(boot_partition, root, uki_enabled) case Bootloader.Limine: - self._add_limine_bootloader(boot_partition, efi_partition, root, uki_enabled) + self._add_limine_bootloader(boot_partition, efi_partition, root, uki_enabled, bootloader_removable) def add_additional_packages(self, packages: str | list[str]) -> None: return self.pacman.strap(packages) diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 09b8347835..987a1f8d00 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -16,7 +16,7 @@ ) from .manage_users_conf import UserList, ask_for_additional_users from .network_menu import ManualNetworkConfig, ask_to_configure_network -from .system_conf import ask_for_bootloader, ask_for_swap, ask_for_uki, select_driver, select_kernel +from .system_conf import ask_for_swap, select_driver, select_kernel __all__ = [ 'ManualNetworkConfig', @@ -25,9 +25,7 @@ 'ask_additional_packages_to_install', 'ask_for_a_timezone', 'ask_for_additional_users', - 'ask_for_bootloader', 'ask_for_swap', - 'ask_for_uki', 'ask_hostname', 'ask_ntp', 'ask_to_configure_network', diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py index 99a1d8bd16..e37977c7fd 100644 --- a/archinstall/lib/interactions/system_conf.py +++ b/archinstall/lib/interactions/system_conf.py @@ -6,9 +6,7 @@ from archinstall.tui.result import ResultType from archinstall.tui.types import Alignment, FrameProperties, FrameStyle, Orientation, PreviewStyle -from ..args import arch_config_handler from ..hardware import GfxDriver, SysInfo -from ..models.bootloader import Bootloader def select_kernel(preset: list[str] = []) -> list[str]: @@ -46,74 +44,6 @@ def select_kernel(preset: list[str] = []) -> list[str]: return result.get_values() -def ask_for_bootloader(preset: Bootloader | None) -> Bootloader | None: - # Systemd is UEFI only - options = [] - hidden_options = [] - default = None - header = None - - if arch_config_handler.args.skip_boot: - default = Bootloader.NO_BOOTLOADER - else: - hidden_options += [Bootloader.NO_BOOTLOADER] - - if not SysInfo.has_uefi(): - options += [Bootloader.Grub, Bootloader.Limine] - if not default: - default = Bootloader.Grub - header = tr('UEFI is not detected and some options are disabled') - else: - options += [b for b in Bootloader if b not in hidden_options] - if not default: - default = Bootloader.Systemd - - items = [MenuItem(o.value, value=o) for o in options] - group = MenuItemGroup(items) - group.set_default_by_value(default) - group.set_focus_by_value(preset) - - result = SelectMenu[Bootloader]( - group, - header=header, - alignment=Alignment.CENTER, - frame=FrameProperties.min(tr('Bootloader')), - allow_skip=True, - ).run() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Selection: - return result.get_value() - case ResultType.Reset: - raise ValueError('Unhandled result type') - - -def ask_for_uki(preset: bool = True) -> bool: - prompt = tr('Would you like to use unified kernel images?') + '\n' - - group = MenuItemGroup.yes_no() - group.set_focus_by_value(preset) - - result = SelectMenu[bool]( - group, - header=prompt, - columns=2, - orientation=Orientation.HORIZONTAL, - alignment=Alignment.CENTER, - allow_skip=True, - ).run() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Selection: - return result.item() == MenuItem.yes() - case ResultType.Reset: - raise ValueError('Unhandled result type') - - def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None) -> GfxDriver | None: """ Some what convoluted function, whose job is simple. diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py index 1eb3562cad..457d2b3b34 100644 --- a/archinstall/lib/models/bootloader.py +++ b/archinstall/lib/models/bootloader.py @@ -1,7 +1,11 @@ from __future__ import annotations import sys +from dataclasses import dataclass from enum import Enum +from typing import Any + +from archinstall.lib.translationhandler import tr from ..hardware import SysInfo from ..output import warn @@ -21,6 +25,13 @@ def has_uki_support(self) -> bool: case _: return False + def has_removable_support(self) -> bool: + match self: + case Bootloader.Grub | Bootloader.Limine: + return True + case _: + return False + def json(self) -> str: return self.value @@ -48,3 +59,43 @@ def from_arg(cls, bootloader: str, skip_boot: bool) -> Bootloader: sys.exit(1) return Bootloader(bootloader) + + +@dataclass +class BootloaderConfiguration: + bootloader: Bootloader + uki: bool = False + removable: bool = False + + def json(self) -> dict[str, Any]: + return {'bootloader': self.bootloader.json(), 'uki': self.uki, 'removable': self.removable} + + @classmethod + def parse_arg(cls, config: dict[str, Any], skip_boot: bool) -> BootloaderConfiguration: + bootloader = Bootloader.from_arg(config.get('bootloader', ''), skip_boot) + uki = config.get('uki', False) + removable = config.get('removable', False) + return cls(bootloader=bootloader, uki=uki, removable=removable) + + @classmethod + def get_default(cls) -> BootloaderConfiguration: + return cls(bootloader=Bootloader.get_default(), uki=False, removable=False) + + def preview(self) -> str: + text = f'{tr("Bootloader")}: {self.bootloader.value}' + text += '\n' + if SysInfo.has_uefi() and self.bootloader.has_uki_support(): + if self.uki: + uki_string = tr('Enabled') + else: + uki_string = tr('Disabled') + text += f'UKI: {uki_string}' + text += '\n' + if SysInfo.has_uefi() and self.bootloader.has_removable_support(): + if self.removable: + removable_string = tr('Enabled') + else: + removable_string = tr('Disabled') + text += f'{tr("Removable")}: {removable_string}' + text += '\n' + return text diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index c0a252b888..9fcb22334b 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -62,7 +62,7 @@ def perform_installation(mountpoint: Path) -> None: return disk_config = config.disk_config - run_mkinitcpio = not config.uki + run_mkinitcpio = not config.bootloader_config or not config.bootloader_config.uki locale_config = config.locale_config optional_repositories = config.mirror_config.optional_repositories if config.mirror_config else [] mountpoint = disk_config.mountpoint if disk_config.mountpoint else mountpoint @@ -99,11 +99,11 @@ def perform_installation(mountpoint: Path) -> None: if config.swap: installation.setup_swap('zram') - if config.bootloader and config.bootloader != Bootloader.NO_BOOTLOADER: - if config.bootloader == Bootloader.Grub and SysInfo.has_uefi(): + if config.bootloader_config and config.bootloader_config.bootloader != Bootloader.NO_BOOTLOADER: + if config.bootloader_config.bootloader == Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages('grub') - installation.add_bootloader(config.bootloader, config.uki) + installation.add_bootloader(config.bootloader_config.bootloader, config.bootloader_config.uki, config.bootloader_config.removable) # If user selected to copy the current ISO network configuration # Perform a copy of the config @@ -155,7 +155,8 @@ def perform_installation(mountpoint: Path) -> None: snapshot_config = btrfs_options.snapshot_config if btrfs_options else None snapshot_type = snapshot_config.snapshot_type if snapshot_config else None if snapshot_type: - installation.setup_btrfs_snapshot(snapshot_type, config.bootloader) + bootloader = config.bootloader_config.bootloader if config.bootloader_config else None + installation.setup_btrfs_snapshot(snapshot_type, bootloader) # If the user provided custom commands to be run post-installation, execute them now. if cc := config.custom_commands: diff --git a/docs/cli_parameters/config/config_options.csv b/docs/cli_parameters/config/config_options.csv index 3edfa205d1..a902d1d20e 100644 --- a/docs/cli_parameters/config/config_options.csv +++ b/docs/cli_parameters/config/config_options.csv @@ -2,7 +2,7 @@ Key,Value(s),Description,Required additional-repositories,[ `multilib `_!, `testing `_ ],Enables one or more of the testing and multilib repositories before proceeding with installation,No archinstall-language,`lang `__,Sets the TUI language used *(make sure to use the ``lang`` value not the ``abbr``)*,No audio_config,`pipewire `_!, `pulseaudio `_,Audioserver to be installed,No -bootloader,`Systemd-boot `_!, `grub `_,Bootloader to be installed *(grub being mandatory on BIOS machines)*,Yes +bootloader_config,"{ bootloader: `Systemd-boot `_!, `grub `_!, `limine `_!, uki: ``true``/``false``!, removable: ``true``/``false`` }","Bootloader configuration. ``bootloader`` selects which bootloader to install *(grub/limine mandatory on BIOS)*. ``uki`` enables unified kernel images *(UEFI only!, systemd-boot/limine only)*. ``removable`` installs to default removable media path /EFI/BOOT/ instead of NVRAM *(UEFI only!, grub/limine only)*",Yes debug,``true``!, ``false``,Enables debug output,No disk_config,*Read more under* :ref:`disk config`,Contains the desired disk setup to be used during installation,No disk_encryption,*Read more about under* :ref:`disk encryption`,Parameters for disk encryption applied on top of ``disk_config``,No diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index 24ee19a6c4..562d2b2980 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -66,6 +66,11 @@ The contents of :code:`https://domain.lan/config.json`: "additional-repositories": [], "archinstall-language": "English", "audio_config": null, + "bootloader_config": { + "bootloader": "Systemd-boot", + "uki": false, + "removable": false + }, "bootloader": "Systemd-boot", "debug": false, "disk_config": { diff --git a/examples/config-sample.json b/examples/config-sample.json index 40a349c998..30ba5b52d3 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -3,7 +3,11 @@ "audio_config": { "audio": "pipewire" }, - "bootloader": "Systemd-boot", + "bootloader_config": { + "bootloader": "Systemd-boot", + "uki": false, + "removable": false + }, "debug": false, "disk_config": { "config_type": "default_layout", diff --git a/tests/data/test_config.json b/tests/data/test_config.json index 9b0c218d32..a01c831b7f 100644 --- a/tests/data/test_config.json +++ b/tests/data/test_config.json @@ -18,7 +18,11 @@ "audio_config": { "audio": "pipewire" }, - "bootloader": "Systemd-boot", + "bootloader_config": { + "bootloader": "Systemd-boot", + "uki": false, + "removable": false + }, "services": [ "service_1", "service_2" diff --git a/tests/test_args.py b/tests/test_args.py index 28c7fc8e00..87ab1a3f47 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -9,7 +9,7 @@ from archinstall.lib.hardware import GfxDriver from archinstall.lib.models.application import ApplicationConfiguration, Audio, AudioConfiguration, BluetoothConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod -from archinstall.lib.models.bootloader import Bootloader +from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.mirrors import CustomRepository, CustomServer, MirrorConfiguration, MirrorRegion, SignCheck, SignOption @@ -214,8 +214,11 @@ def test_config_file_parsing( ), ], ), - bootloader=Bootloader.Systemd, - uki=False, + bootloader_config=BootloaderConfiguration( + bootloader=Bootloader.Systemd, + uki=False, + removable=False, + ), hostname='archy', kernels=['linux-zen'], ntp=True,