582 lines
17 KiB
Python
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()
|