Add dialog to install EFI bootloader to removable location (#3932)

* Add dialog to install EFI bootloader to removable location

This is just for GRUB and Limine for now.

* Move bootloader removable and UKI selections to bootloader submenu

* Update ask_for_bootloader_removable() prompt for ease of translation

* Fix issue where removable and UKI options were always enabled at first

* Minor cosmetic fixes to bootloader removable code

* Add has_removable_support to Bootloader

* Validate UKI and removable options in installer

* Use has_removable_support() where appropriate

* Fix potential AttributeError when bootloader_config is None

* Set default value for bootloader configuration menu item

* Update documentation after EFI removable/Limine changes

* Update limine.conf and non-removable location paths (as per Wiki)

* Do not create fallback boot menu entries when using UKIs on Limine

* Remove useless ask_* wrappers in bootloader_menu

* Improve bootloader menu previews

* Make bootloader menu __init__.py empty
This commit is contained in:
Mintsuki 2025-11-21 01:41:24 +01:00 committed by GitHub
parent 840c1c0f7e
commit eb815d817f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 395 additions and 165 deletions

View File

@ -15,7 +15,7 @@ from pydantic.dataclasses import dataclass as p_dataclass
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 @@ class ArchConfig:
'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 @@ class ArchConfig:
'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 @@ class ArchConfig:
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)

View File

View File

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

View File

@ -12,6 +12,7 @@ from archinstall.tui.menu_item import MenuItem, MenuItemGroup
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 @@ from .interactions.general_conf import (
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 @@ class GlobalMenu(AbstractMenu[None]):
),
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 @@ class GlobalMenu(AbstractMenu[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 @@ class GlobalMenu(AbstractMenu[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 @@ class GlobalMenu(AbstractMenu[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 @@ class GlobalMenu(AbstractMenu[None]):
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

View File

@ -1268,6 +1268,7 @@ class Installer:
boot_partition: PartitionModification,
root: PartitionModification | LvmVolume,
efi_partition: PartitionModification | None,
bootloader_removable: bool = False,
) -> None:
debug('Installing grub bootloader')
@ -1311,9 +1312,11 @@ class Installer:
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 @@ class Installer:
efi_partition: PartitionModification | None,
root: PartitionModification | LvmVolume,
uki_enabled: bool = False,
bootloader_removable: bool = False,
) -> None:
debug('Installing Limine bootloader')
@ -1376,22 +1380,22 @@ class Installer:
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 @@ class Installer:
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 @@ class Installer:
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 @@ class Installer:
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 @@ class Installer:
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 @@ class Installer:
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 @@ class Installer:
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 @@ class Installer:
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)

View File

@ -16,7 +16,7 @@ from .general_conf import (
)
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 @@ __all__ = [
'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',

View File

@ -6,9 +6,7 @@ from archinstall.tui.menu_item import MenuItem, MenuItemGroup
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.

View File

@ -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 @@ class Bootloader(Enum):
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 @@ class Bootloader(Enum):
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

View File

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

View File

@ -2,7 +2,7 @@ Key,Value(s),Description,Required
additional-repositories,[ `multilib <https://wiki.archlinux.org/title/Official_repositories#multilib>`_!, `testing <https://wiki.archlinux.org/title/Official_repositories#Testing_repositories>`_ ],Enables one or more of the testing and multilib repositories before proceeding with installation,No
archinstall-language,`lang <https://github.com/archlinux/archinstall/blob/master/archinstall/locales/languages.json>`__,Sets the TUI language used *(make sure to use the ``lang`` value not the ``abbr``)*,No
audio_config,`pipewire <https://wiki.archlinux.org/title/PipeWire>`_!, `pulseaudio <https://wiki.archlinux.org/title/PulseAudio>`_,Audioserver to be installed,No
bootloader,`Systemd-boot <https://wiki.archlinux.org/title/Systemd-boot>`_!, `grub <https://wiki.archlinux.org/title/GRUB>`_,Bootloader to be installed *(grub being mandatory on BIOS machines)*,Yes
bootloader_config,"{ bootloader: `Systemd-boot <https://wiki.archlinux.org/title/Systemd-boot>`_!, `grub <https://wiki.archlinux.org/title/GRUB>`_!, `limine <https://wiki.archlinux.org/title/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

Can't render this file because it has a wrong number of fields in line 2.

View File

@ -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": {

View File

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

View File

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

View File

@ -9,7 +9,7 @@ from archinstall.lib.args import ArchConfig, ArchConfigHandler, Arguments
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,