Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 12 additions & 12 deletions archinstall/lib/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -102,15 +101,14 @@ 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,
'swap': self.swap,
'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,
}
Expand Down Expand Up @@ -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)
Expand Down
Empty file.
229 changes: 229 additions & 0 deletions archinstall/lib/bootloader/bootloader_menu.py
Original file line number Diff line number Diff line change
@@ -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')
Loading
Loading