Files
miasma-installer/examples/archinstall/archinstall/lib/global_menu.py
tumillanino 33dd952ad4
Some checks failed
Build / build (push) Failing after 5m23s
updated the installer so that it should actually work
2025-11-11 18:57:02 +11:00

582 lines
17 KiB
Python

from __future__ import annotations
from typing import override
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.models.application import ApplicationConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, EncryptionType, FilesystemType, PartitionModification
from archinstall.lib.packages import list_available_packages
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 .configuration import save_config
from .hardware import SysInfo
from .interactions.general_conf import (
add_number_of_parallel_downloads,
ask_additional_packages_to_install,
ask_for_a_timezone,
ask_hostname,
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 .locale.locale_menu import LocaleMenu
from .menu.abstract_menu import CONFIG_KEY, AbstractMenu
from .mirrors import MirrorMenu
from .models.bootloader import Bootloader
from .models.locale import LocaleConfiguration
from .models.mirrors import MirrorConfiguration
from .models.network import NetworkConfiguration, NicType
from .models.packages import Repository
from .models.profile import ProfileConfiguration
from .output import FormattedOutput
from .pacman.config import PacmanConfig
from .translationhandler import Language, tr, translation_handler
class GlobalMenu(AbstractMenu[None]):
def __init__(self, arch_config: ArchConfig) -> None:
self._arch_config = arch_config
menu_optioons = self._get_menu_options()
self._item_group = MenuItemGroup(
menu_optioons,
sort_items=False,
checkmarks=True,
)
super().__init__(self._item_group, config=arch_config)
def _get_menu_options(self) -> list[MenuItem]:
menu_options = [
MenuItem(
text=tr('Archinstall language'),
action=self._select_archinstall_language,
display_action=lambda x: x.display_name if x else '',
key='archinstall_language',
),
MenuItem(
text=tr('Locales'),
action=self._locale_selection,
preview_action=self._prev_locale,
key='locale_config',
),
MenuItem(
text=tr('Mirrors and repositories'),
action=self._mirror_configuration,
preview_action=self._prev_mirror_config,
key='mirror_config',
),
MenuItem(
text=tr('Disk configuration'),
action=self._select_disk_config,
preview_action=self._prev_disk_config,
mandatory=True,
key='disk_config',
),
MenuItem(
text=tr('Swap'),
value=True,
action=ask_for_swap,
preview_action=self._prev_swap,
key='swap',
),
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',
),
MenuItem(
text=tr('Hostname'),
value='archlinux',
action=ask_hostname,
preview_action=self._prev_hostname,
key='hostname',
),
MenuItem(
text=tr('Authentication'),
action=self._select_authentication,
preview_action=self._prev_authentication,
key='auth_config',
),
MenuItem(
text=tr('Profile'),
action=self._select_profile,
preview_action=self._prev_profile,
key='profile_config',
),
MenuItem(
text=tr('Applications'),
action=self._select_applications,
value=[],
preview_action=self._prev_applications,
key='app_config',
),
MenuItem(
text=tr('Kernels'),
value=['linux'],
action=select_kernel,
preview_action=self._prev_kernel,
mandatory=True,
key='kernels',
),
MenuItem(
text=tr('Network configuration'),
action=ask_to_configure_network,
value={},
preview_action=self._prev_network_config,
key='network_config',
),
MenuItem(
text=tr('Parallel Downloads'),
action=add_number_of_parallel_downloads,
value=0,
preview_action=self._prev_parallel_dw,
key='parallel_downloads',
),
MenuItem(
text=tr('Additional packages'),
action=self._select_additional_packages,
value=[],
preview_action=self._prev_additional_pkgs,
key='packages',
),
MenuItem(
text=tr('Timezone'),
action=ask_for_a_timezone,
value='UTC',
preview_action=self._prev_tz,
key='timezone',
),
MenuItem(
text=tr('Automatic time sync (NTP)'),
action=ask_ntp,
value=True,
preview_action=self._prev_ntp,
key='ntp',
),
MenuItem(
text='',
),
MenuItem(
text=tr('Save configuration'),
action=lambda x: self._safe_config(),
key=f'{CONFIG_KEY}_save',
),
MenuItem(
text=tr('Install'),
preview_action=self._prev_install_invalid_config,
key=f'{CONFIG_KEY}_install',
),
MenuItem(
text=tr('Abort'),
action=lambda x: exit(1),
key=f'{CONFIG_KEY}_abort',
),
]
return menu_options
def _safe_config(self) -> None:
# data: dict[str, Any] = {}
# for item in self._item_group.items:
# if item.key is not None:
# data[item.key] = item.value
self.sync_all_to_config()
save_config(self._arch_config)
def _missing_configs(self) -> list[str]:
item: MenuItem = self._item_group.find_by_key('auth_config')
auth_config: AuthenticationConfiguration | None = item.value
def check(s: str) -> bool:
item = self._item_group.find_by_key(s)
return item.has_value()
def has_superuser() -> bool:
if auth_config and auth_config.users:
return any([u.sudo for u in auth_config.users])
return False
missing = set()
if (auth_config is None or auth_config.root_enc_password is None) and not has_superuser():
missing.add(
tr('Either root-password or at least 1 user with sudo privileges must be specified'),
)
for item in self._item_group.items:
if item.mandatory:
assert item.key is not None
if not check(item.key):
missing.add(item.text)
return list(missing)
@override
def _is_config_valid(self) -> bool:
"""
Checks the validity of the current configuration.
"""
if len(self._missing_configs()) != 0:
return False
return self._validate_bootloader() is None
def _select_archinstall_language(self, preset: Language) -> Language:
from .interactions.general_conf import select_archinstall_language
language = select_archinstall_language(translation_handler.translated_languages, preset)
translation_handler.activate(language)
self._update_lang_text()
return language
def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None:
app_config = ApplicationMenu(preset).run()
return app_config
def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None:
auth_config = AuthenticationMenu(preset).run()
return auth_config
def _update_lang_text(self) -> None:
"""
The options for the global menu are generated with a static text;
each entry of the menu needs to be updated with the new translation
"""
new_options = self._get_menu_options()
for o in new_options:
if o.key is not None:
self._item_group.find_by_key(o.key).text = o.text
def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration:
locale_config = LocaleMenu(preset).run()
return locale_config
def _prev_locale(self, item: MenuItem) -> str | None:
if not item.value:
return None
config: LocaleConfiguration = item.value
return config.preview()
def _prev_network_config(self, item: MenuItem) -> str | None:
if item.value:
network_config: NetworkConfiguration = item.value
if network_config.type == NicType.MANUAL:
output = FormattedOutput.as_table(network_config.nics)
else:
output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}'
return output
return None
def _prev_additional_pkgs(self, item: MenuItem) -> str | None:
if item.value:
output = '\n'.join(sorted(item.value))
return output
return None
def _prev_authentication(self, item: MenuItem) -> str | None:
if item.value:
auth_config: AuthenticationConfiguration = item.value
output = ''
if auth_config.root_enc_password:
output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'
if auth_config.users:
output += FormattedOutput.as_table(auth_config.users) + '\n'
if auth_config.u2f_config:
u2f_config = auth_config.u2f_config
login_method = u2f_config.u2f_login_method.display_value()
output = tr('U2F login method: ') + login_method
output += '\n'
output += tr('Passwordless sudo: ') + (tr('Enabled') if u2f_config.passwordless_sudo else tr('Disabled'))
return output
return None
def _prev_applications(self, item: MenuItem) -> str | None:
if item.value:
app_config: ApplicationConfiguration = item.value
output = ''
if app_config.bluetooth_config:
output += f'{tr("Bluetooth")}: '
output += tr('Enabled') if app_config.bluetooth_config.enabled else tr('Disabled')
output += '\n'
if app_config.audio_config:
audio_config = app_config.audio_config
output += f'{tr("Audio")}: {audio_config.audio.value}'
output += '\n'
return output
return None
def _prev_tz(self, item: MenuItem) -> str | None:
if item.value:
return f'{tr("Timezone")}: {item.value}'
return None
def _prev_ntp(self, item: MenuItem) -> str | None:
if item.value is not None:
output = f'{tr("NTP")}: '
output += tr('Enabled') if item.value else tr('Disabled')
return output
return None
def _prev_disk_config(self, item: MenuItem) -> str | None:
disk_layout_conf: DiskLayoutConfiguration | None = item.value
if disk_layout_conf:
output = tr('Configuration type: {}').format(disk_layout_conf.config_type.display_msg()) + '\n'
if disk_layout_conf.config_type == DiskLayoutType.Pre_mount:
output += tr('Mountpoint') + ': ' + str(disk_layout_conf.mountpoint)
if disk_layout_conf.lvm_config:
output += '{}: {}'.format(tr('LVM configuration type'), disk_layout_conf.lvm_config.config_type.display_msg()) + '\n'
if disk_layout_conf.disk_encryption:
output += tr('Disk encryption') + ': ' + EncryptionType.type_to_text(disk_layout_conf.disk_encryption.encryption_type) + '\n'
if disk_layout_conf.btrfs_options:
btrfs_options = disk_layout_conf.btrfs_options
if btrfs_options.snapshot_config:
output += tr('Btrfs snapshot type: {}').format(btrfs_options.snapshot_config.snapshot_type.value) + '\n'
return output
return None
def _prev_swap(self, item: MenuItem) -> str | None:
if item.value is not None:
output = f'{tr("Swap on zram")}: '
output += tr('Enabled') if item.value else tr('Disabled')
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}'
return None
def _prev_parallel_dw(self, item: MenuItem) -> str | None:
if item.value is not None:
return f'{tr("Parallel Downloads")}: {item.value}'
return None
def _prev_kernel(self, item: MenuItem) -> str | None:
if item.value:
kernel = ', '.join(item.value)
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}'
return None
def _validate_bootloader(self) -> str | None:
"""
Checks the selected bootloader is valid for the selected filesystem
type of the boot partition.
Returns [`None`] if the bootloader is valid, otherwise returns a
string with the error message.
XXX: The caller is responsible for wrapping the string with the translation
shim if necessary.
"""
bootloader: Bootloader | 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
if bootloader == Bootloader.NO_BOOTLOADER:
return None
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():
break
for layout in disk_config.device_modifications:
if boot_partition := layout.get_boot_partition():
break
if SysInfo.has_uefi():
for layout in disk_config.device_modifications:
if efi_partition := layout.get_efi_partition():
break
else:
return 'No disk layout selected'
if root_partition is None:
return 'Root partition not found'
if boot_partition is None:
return 'Boot partition not found'
if SysInfo.has_uefi():
if efi_partition is None:
return 'EFI system partition (ESP) not found'
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'
return None
def _prev_install_invalid_config(self, item: MenuItem) -> str | None:
if missing := self._missing_configs():
text = tr('Missing configurations:\n')
for m in missing:
text += f'- {m}\n'
return text[:-1] # remove last new line
if error := self._validate_bootloader():
return tr(f'Invalid configuration: {error}')
return None
def _prev_profile(self, item: MenuItem) -> str | None:
profile_config: ProfileConfiguration | None = item.value
if profile_config and profile_config.profile:
output = tr('Profiles') + ': '
if profile_names := profile_config.profile.current_selection_names():
output += ', '.join(profile_names) + '\n'
else:
output += profile_config.profile.name + '\n'
if profile_config.gfx_driver:
output += tr('Graphics driver') + ': ' + profile_config.gfx_driver.value + '\n'
if profile_config.greeter:
output += tr('Greeter') + ': ' + profile_config.greeter.value + '\n'
return output
return None
def _select_disk_config(
self,
preset: DiskLayoutConfiguration | None = None,
) -> DiskLayoutConfiguration | None:
disk_config = DiskLayoutConfigurationMenu(preset).run()
return disk_config
def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None:
bootloader = ask_for_bootloader(preset)
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
return bootloader
def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None:
from .profile.profile_menu import ProfileMenu
profile_config = ProfileMenu(preset=current_profile).run()
return profile_config
def _select_additional_packages(self, preset: list[str]) -> list[str]:
config: MirrorConfiguration | None = self._item_group.find_by_key('mirror_config').value
repositories: set[Repository] = set()
if config:
repositories = set(config.optional_repositories)
packages = ask_additional_packages_to_install(
preset,
repositories=repositories,
)
return packages
def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration:
mirror_configuration = MirrorMenu(preset=preset).run()
if mirror_configuration.optional_repositories:
# reset the package list cache in case the repository selection has changed
list_available_packages.cache_clear()
# enable the repositories in the config
pacman_config = PacmanConfig(None)
pacman_config.enable(mirror_configuration.optional_repositories)
pacman_config.apply()
return mirror_configuration
def _prev_mirror_config(self, item: MenuItem) -> str | None:
if not item.value:
return None
mirror_config: MirrorConfiguration = item.value
output = ''
if mirror_config.mirror_regions:
title = tr('Selected mirror regions')
divider = '-' * len(title)
regions = mirror_config.region_names
output += f'{title}\n{divider}\n{regions}\n\n'
if mirror_config.custom_servers:
title = tr('Custom servers')
divider = '-' * len(title)
servers = mirror_config.custom_server_urls
output += f'{title}\n{divider}\n{servers}\n\n'
if mirror_config.optional_repositories:
title = tr('Optional repositories')
divider = '-' * len(title)
repos = ', '.join([r.value for r in mirror_config.optional_repositories])
output += f'{title}\n{divider}\n{repos}\n\n'
if mirror_config.custom_repositories:
title = tr('Custom repositories')
table = FormattedOutput.as_table(mirror_config.custom_repositories)
output += f'{title}:\n\n{table}'
return output.strip()