615 lines
20 KiB
Python
615 lines
20 KiB
Python
from typing import override
|
|
|
|
from archinstall.default_profiles.profile import GreeterType
|
|
from archinstall.lib.applications.application_menu import ApplicationMenu
|
|
from archinstall.lib.args import ArchConfig
|
|
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
|
|
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
|
|
from archinstall.lib.configuration import save_config
|
|
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
|
|
from archinstall.lib.general.general_menu import add_number_of_parallel_downloads, select_hostname, select_ntp, select_timezone
|
|
from archinstall.lib.general.system_menu import select_kernel, select_swap
|
|
from archinstall.lib.hardware import SysInfo
|
|
from archinstall.lib.locale.locale_menu import LocaleMenu
|
|
from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey
|
|
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
|
|
from archinstall.lib.mirror.mirror_menu import MirrorMenu
|
|
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
|
|
from archinstall.lib.models.authentication import AuthenticationConfiguration
|
|
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
|
|
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, FilesystemType, PartitionModification
|
|
from archinstall.lib.models.locale import LocaleConfiguration
|
|
from archinstall.lib.models.mirrors import MirrorConfiguration
|
|
from archinstall.lib.models.network import NetworkConfiguration, NicType
|
|
from archinstall.lib.models.packages import Repository
|
|
from archinstall.lib.models.profile import ProfileConfiguration
|
|
from archinstall.lib.network.network_menu import select_network
|
|
from archinstall.lib.output import FormattedOutput
|
|
from archinstall.lib.packages.packages import list_available_packages, select_additional_packages
|
|
from archinstall.lib.pacman.config import PacmanConfig
|
|
from archinstall.lib.translationhandler import Language, tr, translation_handler
|
|
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
|
|
|
|
|
class GlobalMenu(AbstractMenu[None]):
|
|
def __init__(
|
|
self,
|
|
arch_config: ArchConfig,
|
|
mirror_list_handler: MirrorListHandler | None = None,
|
|
skip_boot: bool = False,
|
|
title: str | None = None,
|
|
) -> None:
|
|
self._arch_config = arch_config
|
|
self._mirror_list_handler = mirror_list_handler
|
|
self._skip_boot = skip_boot
|
|
self._uefi = SysInfo.has_uefi()
|
|
menu_options = self._get_menu_options()
|
|
|
|
self._item_group = MenuItemGroup(
|
|
menu_options,
|
|
sort_items=False,
|
|
checkmarks=True,
|
|
)
|
|
|
|
super().__init__(self._item_group, config=arch_config, title=title)
|
|
|
|
def _get_menu_options(self) -> list[MenuItem]:
|
|
menu_options = [
|
|
MenuItem(
|
|
text=tr('Archinstall language'),
|
|
action=self._select_archinstall_language,
|
|
preview_action=self._prev_archinstall_language,
|
|
key='archinstall_language',
|
|
),
|
|
MenuItem(
|
|
text=tr('Locales'),
|
|
value=LocaleConfiguration.default(),
|
|
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=ZramConfiguration(enabled=True),
|
|
action=select_swap,
|
|
preview_action=self._prev_swap,
|
|
key='swap',
|
|
),
|
|
MenuItem(
|
|
text=tr('Bootloader'),
|
|
value=BootloaderConfiguration.get_default(self._uefi, self._skip_boot),
|
|
action=self._select_bootloader_config,
|
|
preview_action=self._prev_bootloader_config,
|
|
key='bootloader_config',
|
|
),
|
|
MenuItem(
|
|
text=tr('Kernels'),
|
|
value=['linux'],
|
|
action=select_kernel,
|
|
preview_action=self._prev_kernel,
|
|
mandatory=True,
|
|
key='kernels',
|
|
),
|
|
MenuItem(
|
|
text=tr('Hostname'),
|
|
value='archlinux',
|
|
action=select_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('Network configuration'),
|
|
action=select_network,
|
|
value={},
|
|
preview_action=self._prev_network_config,
|
|
key='network_config',
|
|
),
|
|
MenuItem(
|
|
text=tr('Parallel Downloads'),
|
|
action=add_number_of_parallel_downloads,
|
|
value=1,
|
|
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=select_timezone,
|
|
value='UTC',
|
|
preview_action=self._prev_tz,
|
|
key='timezone',
|
|
),
|
|
MenuItem(
|
|
text=tr('Automatic time sync (NTP)'),
|
|
action=select_ntp,
|
|
value=True,
|
|
preview_action=self._prev_ntp,
|
|
key='ntp',
|
|
),
|
|
MenuItem(
|
|
text='',
|
|
read_only=True,
|
|
),
|
|
MenuItem(
|
|
text=tr('Save configuration'),
|
|
action=lambda x: self._safe_config(),
|
|
key=SpecialMenuKey.SAVE.value,
|
|
),
|
|
MenuItem(
|
|
text=tr('Install'),
|
|
preview_action=self._prev_install_invalid_config,
|
|
key=SpecialMenuKey.INSTALL.value,
|
|
),
|
|
MenuItem(
|
|
text=tr('Abort'),
|
|
key=SpecialMenuKey.ABORT.value,
|
|
),
|
|
]
|
|
|
|
return menu_options
|
|
|
|
async 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()
|
|
await 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
|
|
|
|
def has_regular_user() -> bool:
|
|
if auth_config and auth_config.users:
|
|
return len(auth_config.users) > 0
|
|
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'),
|
|
)
|
|
|
|
# These greeters only show users with UID >= 1000 and have no manual login by default
|
|
if not has_regular_user():
|
|
profile_item: MenuItem = self._item_group.find_by_key('profile_config')
|
|
profile_config: ProfileConfiguration | None = profile_item.value
|
|
|
|
if profile_config and profile_config.profile and profile_config.profile.is_desktop_profile():
|
|
problematic_greeters = {GreeterType.Sddm}
|
|
if any(p.default_greeter_type in problematic_greeters for p in profile_config.profile.current_selection):
|
|
missing.add(
|
|
tr('The selected desktop profile requires a regular user to log in via the greeter'),
|
|
)
|
|
|
|
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
|
|
|
|
async def _select_archinstall_language(self, preset: Language) -> Language:
|
|
from archinstall.lib.general.general_menu import select_archinstall_language
|
|
|
|
language = await select_archinstall_language(translation_handler.translated_languages, preset)
|
|
translation_handler.activate(language)
|
|
|
|
self._update_lang_text()
|
|
|
|
return language
|
|
|
|
def _prev_archinstall_language(self, item: MenuItem) -> str | None:
|
|
if not item.value:
|
|
return None
|
|
|
|
lang: Language = item.value
|
|
return f'{tr("Language")}: {lang.display_name}'
|
|
|
|
async def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None:
|
|
app_config = await ApplicationMenu(preset).show()
|
|
return app_config
|
|
|
|
async def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None:
|
|
auth_config = await AuthenticationMenu(preset).show()
|
|
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
|
|
|
|
async def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration | None:
|
|
locale_config = await LocaleMenu(preset).show()
|
|
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'
|
|
|
|
if app_config.print_service_config:
|
|
output += f'{tr("Print service")}: '
|
|
output += tr('Enabled') if app_config.print_service_config.enabled else tr('Disabled')
|
|
output += '\n'
|
|
|
|
if app_config.power_management_config:
|
|
power_management_config = app_config.power_management_config
|
|
output += f'{tr("Power management")}: {power_management_config.power_management.value}'
|
|
output += '\n'
|
|
|
|
if app_config.firewall_config:
|
|
firewall_config = app_config.firewall_config
|
|
output += f'{tr("Firewall")}: {firewall_config.firewall.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') + ': ' + disk_layout_conf.disk_encryption.encryption_type.type_to_text() + '\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.enabled else tr('Disabled')
|
|
if item.value.enabled:
|
|
output += f'\n{tr("Compression algorithm")}: {item.value.algorithm.value}'
|
|
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_config(self, item: MenuItem) -> str | None:
|
|
bootloader_config: BootloaderConfiguration | None = item.value
|
|
if bootloader_config:
|
|
return bootloader_config.preview(self._uefi)
|
|
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.
|
|
"""
|
|
bootloader_config: BootloaderConfiguration | None = None
|
|
root_partition: PartitionModification | None = None
|
|
boot_partition: PartitionModification | None = None
|
|
efi_partition: PartitionModification | None = None
|
|
|
|
bootloader_config = self._item_group.find_by_key('bootloader_config').value
|
|
|
|
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():
|
|
break
|
|
for layout in disk_config.device_modifications:
|
|
if boot_partition := layout.get_boot_partition():
|
|
break
|
|
if self._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 self._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'
|
|
|
|
elif bootloader == Bootloader.Refind:
|
|
if not self._uefi:
|
|
return 'rEFInd can only be used on UEFI systems'
|
|
|
|
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
|
|
|
|
async def _select_disk_config(
|
|
self,
|
|
preset: DiskLayoutConfiguration | None = None,
|
|
) -> DiskLayoutConfiguration | None:
|
|
disk_config = await DiskLayoutConfigurationMenu(preset).show()
|
|
return disk_config
|
|
|
|
async def _select_bootloader_config(
|
|
self,
|
|
preset: BootloaderConfiguration | None = None,
|
|
) -> BootloaderConfiguration | None:
|
|
if preset is None:
|
|
preset = BootloaderConfiguration.get_default(self._uefi, self._skip_boot)
|
|
|
|
bootloader_config = await BootloaderMenu(preset, self._uefi, self._skip_boot).show()
|
|
|
|
return bootloader_config
|
|
|
|
async def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None:
|
|
from archinstall.lib.profile.profile_menu import ProfileMenu
|
|
|
|
profile_config = await ProfileMenu(preset=current_profile).show()
|
|
return profile_config
|
|
|
|
async 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 = await select_additional_packages(
|
|
preset,
|
|
repositories=repositories,
|
|
)
|
|
|
|
return packages
|
|
|
|
async def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration | None:
|
|
if self._mirror_list_handler is None:
|
|
self._mirror_list_handler = MirrorListHandler()
|
|
|
|
mirror_configuration = await MirrorMenu(self._mirror_list_handler, preset=preset).run()
|
|
|
|
if mirror_configuration and 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()
|