diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index efe9baa2f3..eb58ce8f59 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -21,7 +21,6 @@ from archinstall.lib.models.config import SubConfig from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration from archinstall.lib.models.locale import LocaleConfiguration -from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.network import NetworkConfiguration from archinstall.lib.models.package_types import DEFAULT_KERNEL from archinstall.lib.models.packages import Repository @@ -66,7 +65,6 @@ class ArchConfigType(StrEnum): ARCHINSTALL_LANGUAGE = 'archinstall_language' DISK_CONFIG = 'disk_config' PROFILE_CONFIG = 'profile_config' - MIRROR_CONFIG = 'mirror_config' NETWORK_CONFIG = 'network_config' BOOTLOADER_CONFIG = 'bootloader_config' APP_CONFIG = 'app_config' @@ -98,8 +96,6 @@ def text(self) -> str: return tr('Disk configuration') case ArchConfigType.PROFILE_CONFIG: return tr('Profile') - case ArchConfigType.MIRROR_CONFIG: - return tr('Mirrors and repositories') case ArchConfigType.NETWORK_CONFIG: return tr('Network') case ArchConfigType.BOOTLOADER_CONFIG: @@ -123,7 +119,7 @@ def text(self) -> str: case ArchConfigType.PACKAGES: return tr('Additional packages') case ArchConfigType.PACMAN_CONFIG: - return tr('Pacman') + return tr('Pacman configuration') case ArchConfigType.CUSTOM_COMMANDS: return tr('Custom commands') case ArchConfigType.USERS: @@ -142,7 +138,7 @@ class ArchConfig: archinstall_language: Language = field(default_factory=lambda: translation_handler.get_language_by_abbr('en')) disk_config: DiskLayoutConfiguration | None = None profile_config: ProfileConfiguration | None = None - mirror_config: MirrorConfiguration | None = None + pacman_config: PacmanConfiguration | None = None network_config: NetworkConfiguration | None = None bootloader_config: BootloaderConfiguration | None = None app_config: ApplicationConfiguration | None = None @@ -152,7 +148,6 @@ class ArchConfig: kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value]) ntp: bool = True packages: list[str] = field(default_factory=list) - pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default) timezone: str = 'UTC' services: list[str] = field(default_factory=list) custom_commands: list[str] = field(default_factory=list) @@ -203,12 +198,10 @@ def plain_cfg(self) -> dict[ArchConfigType, str | list[str] | bool]: } def sub_cfg(self) -> dict[ArchConfigType, SubConfig]: - cfg: dict[ArchConfigType, SubConfig] = { - ArchConfigType.PACMAN_CONFIG: self.pacman_config, - } + cfg: dict[ArchConfigType, SubConfig] = {} - if self.mirror_config: - cfg[ArchConfigType.MIRROR_CONFIG] = self.mirror_config + if self.pacman_config: + cfg[ArchConfigType.PACMAN_CONFIG] = self.pacman_config if self.bootloader_config: cfg[ArchConfigType.BOOTLOADER_CONFIG] = self.bootloader_config @@ -271,16 +264,21 @@ def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self: if profile_config := args_config.get('profile_config', None): arch_config.profile_config = ProfileConfiguration.parse_arg(profile_config) - if mirror_config := args_config.get('mirror_config', None): + if pacman_config := args_config.get('pacman_config', None): backwards_compatible_repo = [] if additional_repositories := args_config.get('additional-repositories', []): backwards_compatible_repo = [Repository(r) for r in additional_repositories] - arch_config.mirror_config = MirrorConfiguration.parse_args( - mirror_config, + arch_config.pacman_config = PacmanConfiguration.parse_args( + pacman_config, backwards_compatible_repo, ) + if parallel_downloads := args_config.get('parallel_downloads', 0): + if arch_config.pacman_config is None: + arch_config.pacman_config = PacmanConfiguration() + arch_config.pacman_config.parallel_downloads = int(parallel_downloads) + if net_config := args_config.get('network_config', None): arch_config.network_config = NetworkConfiguration.parse_arg(net_config) @@ -315,11 +313,6 @@ def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self: if packages := args_config.get('packages', []): arch_config.packages = packages - if pacman_config := args_config.get('pacman_config', None): - arch_config.pacman_config = PacmanConfiguration.parse_arg(pacman_config) - elif parallel_downloads := args_config.get('parallel_downloads', 0): - arch_config.pacman_config = PacmanConfiguration(parallel_downloads=int(parallel_downloads)) - swap_arg = args_config.get('swap') if swap_arg is not None: arch_config.swap = ZramConfiguration.parse_arg(swap_arg) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index eac936bdd0..c065d7a9fd 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -14,13 +14,11 @@ 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, 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.package_types import DEFAULT_KERNEL from archinstall.lib.models.packages import Repository @@ -76,10 +74,10 @@ def _get_menu_options(self) -> list[MenuItem]: key='locale_config', ), MenuItem( - text=tr('Mirrors and repositories'), - action=self._mirror_configuration, - preview_action=self._prev_mirror_config, - key='mirror_config', + text=tr('Pacman configuration'), + action=self._pacman_configuration, + preview_action=self._prev_pacman_config, + key='pacman_config', ), MenuItem( text=tr('Disk configuration'), @@ -143,13 +141,6 @@ def _get_menu_options(self) -> list[MenuItem]: preview_action=self._prev_network_config, key='network_config', ), - MenuItem( - text=tr('Pacman'), - action=self._pacman_configuration, - value=PacmanConfiguration.default(), - preview_action=self._prev_pacman_config, - key='pacman_config', - ), MenuItem( text=tr('Additional packages'), action=self._select_additional_packages, @@ -419,19 +410,6 @@ def _prev_hostname(self, item: MenuItem) -> str | None: return f'{tr("Hostname")}: {item.value}' return None - async def _pacman_configuration(self, preset: PacmanConfiguration) -> PacmanConfiguration | None: - return await PacmanMenu(preset, advanced=self._advanced).show() - - def _prev_pacman_config(self, item: MenuItem) -> str | None: - if not item.value: - return None - config: PacmanConfiguration = item.value - output = '' - if self._advanced: - output += '{}: {}\n'.format(tr('Parallel Downloads'), config.parallel_downloads) - output += '{}: {}'.format(tr('Color'), config.color) - return output - def _prev_kernel(self, item: MenuItem) -> str | None: if item.value: kernel = ', '.join(item.value) @@ -555,7 +533,7 @@ async def _select_profile(self, current_profile: ProfileConfiguration | None) -> 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 + config: PacmanConfiguration | None = self._item_group.find_by_key('pacman_config').value repositories: set[Repository] = set() if config: @@ -568,51 +546,54 @@ async def _select_additional_packages(self, preset: list[str]) -> list[str]: return packages - async def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration | None: + async def _pacman_configuration(self, preset: PacmanConfiguration | None = None) -> PacmanConfiguration | None: if self._mirror_list_handler is None: self._mirror_list_handler = MirrorListHandler() - mirror_configuration = await MirrorMenu(self._mirror_list_handler, preset=preset).run() + pacman_configuration = await PacmanMenu(self._mirror_list_handler, preset=preset).run() - if mirror_configuration and mirror_configuration.optional_repositories: + if pacman_configuration and pacman_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.enable(pacman_configuration.optional_repositories) pacman_config.apply() - return mirror_configuration + return pacman_configuration - def _prev_mirror_config(self, item: MenuItem) -> str | None: + def _prev_pacman_config(self, item: MenuItem) -> str | None: if not item.value: return None - mirror_config: MirrorConfiguration = item.value + pacman_config: PacmanConfiguration = item.value output = '' - if mirror_config.mirror_regions: + if pacman_config.mirror_regions: title = tr('Selected mirror regions') divider = '-' * len(title) - regions = mirror_config.region_names + regions = pacman_config.region_names output += f'{title}\n{divider}\n{regions}\n\n' - if mirror_config.custom_servers: + if pacman_config.custom_servers: title = tr('Custom servers') divider = '-' * len(title) - servers = mirror_config.custom_server_urls + servers = pacman_config.custom_server_urls output += f'{title}\n{divider}\n{servers}\n\n' - if mirror_config.optional_repositories: + if pacman_config.optional_repositories: title = tr('Optional repositories') divider = '-' * len(title) - repos = ', '.join(r.value for r in mirror_config.optional_repositories) + repos = ', '.join(r.value for r in pacman_config.optional_repositories) output += f'{title}\n{divider}\n{repos}\n\n' - if mirror_config.custom_repositories: + if pacman_config.custom_repositories: title = tr('Custom repositories') - table = FormattedOutput.as_table(mirror_config.custom_repositories) - output += f'{title}:\n\n{table}' + table = FormattedOutput.as_table(pacman_config.custom_repositories) + output += f'{title}:\n\n{table}\n\n' + + output += '{}: {}\n'.format(tr('Parallel Downloads'), pacman_config.parallel_downloads) + output += '{}: {}'.format(tr('Color'), pacman_config.color) return output.strip() diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 801eaee22b..2b940af0f4 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -46,7 +46,6 @@ Unit, ) from archinstall.lib.models.locale import LocaleConfiguration -from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.network import Nic from archinstall.lib.models.package_types import DEFAULT_KERNEL, Kernel from archinstall.lib.models.packages import Repository @@ -553,14 +552,14 @@ def post_install_check(self, *args: str, **kwargs: str) -> list[str]: def set_mirrors( self, mirror_list_handler: MirrorListHandler, - mirror_config: MirrorConfiguration, + pacman_config: PacmanConfiguration, on_target: bool = False, ) -> None: """ Set the mirror configuration for the installation. - :param mirror_config: The mirror configuration to use. - :type mirror_config: MirrorConfiguration + :param pacman_config: The mirror configuration to use. + :type pacman_config: PacmanConfiguration :on_target: Whether to set the mirrors on the target system or the live system. :param on_target: bool @@ -569,29 +568,29 @@ def set_mirrors( for plugin in plugins.values(): if hasattr(plugin, 'on_mirrors'): - if result := plugin.on_mirrors(mirror_config): - mirror_config = result + if result := plugin.on_mirrors(pacman_config): + pacman_config = result if on_target: mirrorlist_config = self.target / MIRRORLIST.relative_to_root() - pacman_config = self.target / PACMAN_CONF.relative_to_root() + pacman_conf_path = self.target / PACMAN_CONF.relative_to_root() else: mirrorlist_config = MIRRORLIST - pacman_config = PACMAN_CONF + pacman_conf_path = PACMAN_CONF - repositories_config = mirror_config.repositories_config() + repositories_config = pacman_config.repositories_config() if repositories_config: debug(f'Pacman config: {repositories_config}') - with open(pacman_config, 'a') as fp: + with open(pacman_conf_path, 'a') as fp: fp.write(repositories_config) - regions_config = mirror_config.regions_config(mirror_list_handler, speed_sort=True) + regions_config = pacman_config.regions_config(mirror_list_handler, speed_sort=True) if regions_config: debug(f'Mirrorlist:\n{regions_config}') mirrorlist_config.write_text(regions_config) - custom_servers = mirror_config.custom_servers_config() + custom_servers = pacman_config.custom_servers_config() if custom_servers: debug(f'Custom servers:\n{custom_servers}') diff --git a/archinstall/lib/mirror/mirror_handler.py b/archinstall/lib/mirror/mirror_handler.py index ef0ba1a9e6..c1a2ea6861 100644 --- a/archinstall/lib/mirror/mirror_handler.py +++ b/archinstall/lib/mirror/mirror_handler.py @@ -3,7 +3,7 @@ from pathlib import Path from archinstall.lib.models import MirrorRegion -from archinstall.lib.models.mirrors import MirrorStatusEntryV3, MirrorStatusListV3 +from archinstall.lib.models.pacman import MirrorStatusEntryV3, MirrorStatusListV3 from archinstall.lib.networking import fetch_data_from_url from archinstall.lib.output import debug, info from archinstall.lib.pathnames import MIRRORLIST diff --git a/archinstall/lib/mirror/mirror_menu.py b/archinstall/lib/mirror/mirror_menu.py deleted file mode 100644 index 5158bffdec..0000000000 --- a/archinstall/lib/mirror/mirror_menu.py +++ /dev/null @@ -1,390 +0,0 @@ -from typing import override - -from archinstall.lib.menu.abstract_menu import AbstractSubMenu -from archinstall.lib.menu.helpers import Input, Loading, Selection -from archinstall.lib.menu.list_manager import ListManager -from archinstall.lib.mirror.mirror_handler import MirrorListHandler -from archinstall.lib.models.mirrors import ( - CustomRepository, - CustomServer, - MirrorConfiguration, - MirrorRegion, - SignCheck, - SignOption, -) -from archinstall.lib.models.packages import Repository -from archinstall.lib.output import FormattedOutput -from archinstall.lib.translationhandler import tr -from archinstall.tui.menu_item import MenuItem, MenuItemGroup -from archinstall.tui.result import ResultType - - -class CustomMirrorRepositoriesList(ListManager[CustomRepository]): - def __init__(self, custom_repositories: list[CustomRepository]): - self._actions = [ - tr('Add a custom repository'), - tr('Change custom repository'), - tr('Delete custom repository'), - ] - - super().__init__( - custom_repositories, - [self._actions[0]], - self._actions[1:], - '', - ) - - async def show(self) -> list[CustomRepository] | None: - return await super()._run() - - @override - def selected_action_display(self, selection: CustomRepository) -> str: - return selection.name - - @override - async def handle_action( - self, - action: str, - entry: CustomRepository | None, - data: list[CustomRepository], - ) -> list[CustomRepository]: - if action == self._actions[0]: # add - new_repo = await self._add_custom_repository() - if new_repo is not None: - data = [d for d in data if d.name != new_repo.name] - data += [new_repo] - elif action == self._actions[1] and entry: # modify repo - new_repo = await self._add_custom_repository(entry) - if new_repo is not None: - data = [d for d in data if d.name != entry.name] - data += [new_repo] - elif action == self._actions[2] and entry: # delete - data = [d for d in data if d != entry] - - return data - - async def _add_custom_repository(self, preset: CustomRepository | None = None) -> CustomRepository | None: - edit_result = await Input( - header=tr('Enter a respository name'), - allow_skip=True, - default_value=preset.name if preset else None, - ).show() - - match edit_result.type_: - case ResultType.Selection: - name = edit_result.get_value() - case ResultType.Skip: - return preset - case _: - raise ValueError('Unhandled return type') - - header = f'{tr("Name")}: {name}\n' - prompt = f'{header}\n' + tr('Enter the repository url') - - edit_result = await Input( - header=prompt, - allow_skip=True, - default_value=preset.url if preset else None, - ).show() - - match edit_result.type_: - case ResultType.Selection: - url = edit_result.get_value() - case ResultType.Skip: - return preset - case _: - raise ValueError('Unhandled return type') - - header += f'{tr("Url")}: {url}\n' - prompt = f'{header}\n' + tr('Select signature check') - - sign_chk_items = [MenuItem(s.value, value=s.value) for s in SignCheck] - group = MenuItemGroup(sign_chk_items, sort_items=False) - - if preset is not None: - group.set_selected_by_value(preset.sign_check.value) - - result = await Selection[SignCheck]( - group, - header=prompt, - allow_skip=False, - ).show() - - match result.type_: - case ResultType.Selection: - sign_check = SignCheck(result.get_value()) - case _: - raise ValueError('Unhandled return type') - - header += f'{tr("Signature check")}: {sign_check.value}\n' - prompt = f'{header}\n' + tr('Select signature option') - - sign_opt_items = [MenuItem(s.value, value=s.value) for s in SignOption] - group = MenuItemGroup(sign_opt_items, sort_items=False) - - if preset is not None: - group.set_selected_by_value(preset.sign_option.value) - - result = await Selection( - group, - header=prompt, - allow_skip=False, - ).show() - - match result.type_: - case ResultType.Selection: - sign_opt = SignOption(result.get_value()) - case _: - raise ValueError('Unhandled return type') - - return CustomRepository(name, url, sign_check, sign_opt) - - -class CustomMirrorServersList(ListManager[CustomServer]): - def __init__(self, custom_servers: list[CustomServer]): - self._actions = [ - tr('Add a custom server'), - tr('Change custom server'), - tr('Delete custom server'), - ] - - super().__init__( - custom_servers, - [self._actions[0]], - self._actions[1:], - '', - ) - - async def show(self) -> list[CustomServer] | None: - return await super()._run() - - @override - def selected_action_display(self, selection: CustomServer) -> str: - return selection.url - - @override - async def handle_action( - self, - action: str, - entry: CustomServer | None, - data: list[CustomServer], - ) -> list[CustomServer]: - if action == self._actions[0]: # add - new_server = await self._add_custom_server() - if new_server is not None: - data = [d for d in data if d.url != new_server.url] - data += [new_server] - elif action == self._actions[1] and entry: # modify repo - new_server = await self._add_custom_server(entry) - if new_server is not None: - data = [d for d in data if d.url != entry.url] - data += [new_server] - elif action == self._actions[2] and entry: # delete - data = [d for d in data if d != entry] - - return data - - async def _add_custom_server(self, preset: CustomServer | None = None) -> CustomServer | None: - edit_result = await Input( - header=tr('Enter server url'), - allow_skip=True, - default_value=preset.url if preset else None, - ).show() - - match edit_result.type_: - case ResultType.Selection: - uri = edit_result.get_value() - return CustomServer(uri) - case ResultType.Skip: - return preset - case _: - return None - - -class MirrorMenu(AbstractSubMenu[MirrorConfiguration]): - def __init__( - self, - mirror_list_handler: MirrorListHandler, - preset: MirrorConfiguration | None = None, - ): - if preset: - self._mirror_config = preset - else: - self._mirror_config = MirrorConfiguration() - - self._mirror_list_handler = mirror_list_handler - - menu_options = self._define_menu_options() - self._item_group = MenuItemGroup(menu_options, checkmarks=True) - - super().__init__( - self._item_group, - config=self._mirror_config, - allow_reset=True, - ) - - def _define_menu_options(self) -> list[MenuItem]: - return [ - MenuItem( - text=tr('Select regions'), - action=lambda x: select_mirror_regions(self._mirror_list_handler, x), - value=self._mirror_config.mirror_regions, - preview_action=self._prev_regions, - key='mirror_regions', - ), - MenuItem( - text=tr('Add custom servers'), - action=add_custom_mirror_servers, - value=self._mirror_config.custom_servers, - preview_action=self._prev_custom_servers, - key='custom_servers', - ), - MenuItem( - text=tr('Optional repositories'), - action=select_optional_repositories, - value=[], - preview_action=self._prev_additional_repos, - key='optional_repositories', - ), - MenuItem( - text=tr('Add custom repository'), - action=select_custom_mirror, - value=self._mirror_config.custom_repositories, - preview_action=self._prev_custom_mirror, - key='custom_repositories', - ), - ] - - def _prev_regions(self, item: MenuItem) -> str: - regions = item.get_value() - - output = '' - for region in regions: - output += f'{region.name}\n' - - for url in region.urls: - output += f' - {url}\n' - - output += '\n' - - return output - - def _prev_additional_repos(self, item: MenuItem) -> str | None: - if item.value: - repositories: list[Repository] = item.value - repos = ', '.join(repo.value for repo in repositories) - return f'{tr("Additional repositories")}: {repos}' - return None - - def _prev_custom_mirror(self, item: MenuItem) -> str | None: - if not item.value: - return None - - custom_mirrors: list[CustomRepository] = item.value - output = FormattedOutput.as_table(custom_mirrors) - return output.strip() - - def _prev_custom_servers(self, item: MenuItem) -> str | None: - if not item.value: - return None - - custom_servers: list[CustomServer] = item.value - output = '\n'.join(server.url for server in custom_servers) - return output.strip() - - @override - async def show(self) -> MirrorConfiguration | None: - return await super().show() - - -async def select_mirror_regions( - mirror_list_handler: MirrorListHandler, - preset: list[MirrorRegion], -) -> list[MirrorRegion]: - await Loading[None]( - header=tr('Loading mirror regions...'), - data_callback=mirror_list_handler.load_mirrors, - ).show() - - available_regions = mirror_list_handler.get_mirror_regions() - - if not available_regions: - return [] - - preset_regions = [region for region in available_regions if region in preset] - - items = [MenuItem(region.name, value=region) for region in available_regions] - group = MenuItemGroup(items, sort_items=True) - - group.set_selected_by_value(preset_regions) - - result = await Selection[MirrorRegion]( - group, - header=tr('Select mirror regions to be enabled'), - allow_reset=True, - allow_skip=True, - multi=True, - enable_filter=True, - ).show() - - match result.type_: - case ResultType.Skip: - return preset_regions - case ResultType.Reset: - return [] - case ResultType.Selection: - selected_mirrors = result.get_values() - return selected_mirrors - - -async def add_custom_mirror_servers(preset: list[CustomServer] = []) -> list[CustomServer]: - custom_mirrors = await CustomMirrorServersList(preset).show() - - if not custom_mirrors: - return preset - - return custom_mirrors - - -async def select_custom_mirror(preset: list[CustomRepository] = []) -> list[CustomRepository]: - custom_mirrors = await CustomMirrorRepositoriesList(preset).show() - - if not custom_mirrors: - return preset - - return custom_mirrors - - -async def select_optional_repositories(preset: list[Repository]) -> list[Repository]: - """ - Allows the user to select additional repositories (multilib, and testing) if desired. - - :return: The string as a selected repository - :rtype: Repository - """ - - repositories = [ - Repository.Multilib, - Repository.MultilibTesting, - Repository.CoreTesting, - Repository.ExtraTesting, - ] - items = [MenuItem(r.value, value=r) for r in repositories] - group = MenuItemGroup(items, sort_items=False) - group.set_selected_by_value(preset) - - result = await Selection[Repository]( - group, - header=tr('Select optional repositories to be enabled'), - allow_reset=True, - allow_skip=True, - multi=True, - ).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Reset: - return [] - case ResultType.Selection: - return result.get_values() diff --git a/archinstall/lib/models/__init__.py b/archinstall/lib/models/__init__.py index 5f808c96f2..7e8e0f2dbf 100644 --- a/archinstall/lib/models/__init__.py +++ b/archinstall/lib/models/__init__.py @@ -27,9 +27,9 @@ _DeviceInfo, ) from archinstall.lib.models.locale import LocaleConfiguration -from archinstall.lib.models.mirrors import CustomRepository, MirrorConfiguration, MirrorRegion from archinstall.lib.models.network import NetworkConfiguration, Nic, NicType from archinstall.lib.models.packages import LocalPackage, PackageSearch, PackageSearchResult, Repository +from archinstall.lib.models.pacman import CustomRepository, MirrorRegion, PacmanConfiguration from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.users import PasswordStrength, User @@ -56,7 +56,6 @@ 'LvmLayoutType', 'LvmVolume', 'LvmVolumeGroup', - 'MirrorConfiguration', 'MirrorRegion', 'ModificationStatus', 'NetworkConfiguration', @@ -64,6 +63,7 @@ 'NicType', 'PackageSearch', 'PackageSearchResult', + 'PacmanConfiguration', 'PartitionFlag', 'PartitionModification', 'PartitionTable', diff --git a/archinstall/lib/models/mirrors.py b/archinstall/lib/models/mirrors.py deleted file mode 100644 index 3ded2a9a7d..0000000000 --- a/archinstall/lib/models/mirrors.py +++ /dev/null @@ -1,356 +0,0 @@ -import datetime -import http.client -import urllib.error -import urllib.parse -import urllib.request -from dataclasses import dataclass, field -from enum import Enum -from typing import TYPE_CHECKING, Any, Self, TypedDict, override - -from pydantic import BaseModel, ValidationInfo, field_validator, model_validator - -from archinstall.lib.models.config import SubConfig -from archinstall.lib.models.packages import Repository -from archinstall.lib.networking import DownloadTimer, ping -from archinstall.lib.output import debug -from archinstall.lib.translationhandler import tr - -if TYPE_CHECKING: - from archinstall.lib.mirror.mirror_handler import MirrorListHandler - - -class MirrorStatusEntryV3(BaseModel): - url: str - protocol: str - active: bool - country: str - country_code: str - isos: bool - ipv4: bool - ipv6: bool - details: str - delay: int | None = None - last_sync: datetime.datetime | None = None - duration_avg: float | None = None - duration_stddev: float | None = None - completion_pct: float | None = None - score: float | None = None - _latency: float | None = None - _speed: float | None = None - _hostname: str | None = None - _port: int | None = None - _speedtest_retries: int | None = None - - @property - def server_url(self) -> str: - return f'{self.url}$repo/os/$arch' - - @property - def speed(self) -> float: - if self._speed is None: - if not self._speedtest_retries: - self._speedtest_retries = 3 - elif self._speedtest_retries < 1: - self._speedtest_retries = 1 - - retry = 0 - while retry < self._speedtest_retries and self._speed is None: - debug(f'Checking download speed of {self._hostname}[{self.score}] by fetching: {self.url}core/os/x86_64/core.db') - req = urllib.request.Request(url=f'{self.url}core/os/x86_64/core.db') - - try: - with urllib.request.urlopen(req, None, 5) as handle, DownloadTimer(timeout=5) as timer: - size = len(handle.read()) - - assert timer.time is not None - self._speed = size / timer.time - debug(f' speed: {self._speed} ({int(self._speed / 1024 / 1024 * 100) / 100}MiB/s)') - # Do not retry error - except urllib.error.URLError as error: - debug(f' speed: ({error}), skip') - self._speed = 0 - # Do retry error - except (http.client.IncompleteRead, ConnectionResetError) as error: - debug(f' speed: ({error}), retry') - # Catch all - except Exception as error: - debug(f' speed: ({error}), skip') - self._speed = 0 - - retry += 1 - - if self._speed is None: - self._speed = 0 - - return self._speed - - @property - def latency(self) -> float | None: - """ - Latency measures the milliseconds between one ICMP request & response. - It only does so once because we check if self._latency is None, and a ICMP timeout result in -1 - We do this because some hosts blocks ICMP so we'll have to rely on .speed() instead which is slower. - """ - if self._latency is None: - debug(f'Checking latency for {self.url}') - assert self._hostname is not None - self._latency = ping(self._hostname, timeout=2) - debug(f' latency: {self._latency}') - - return self._latency - - @classmethod - @field_validator('score', mode='before') - def validate_score(cls, value: float) -> int | None: - if value is not None: - value = round(value) - debug(f' score: {value}') - - return value - - @model_validator(mode='after') - def debug_output(self, info: ValidationInfo) -> Self: - self._hostname, *port = urllib.parse.urlparse(self.url).netloc.split(':', 1) - self._port = int(port[0]) if port and len(port) >= 1 else None - - if (ctx := info.context) and ctx.get('verbose'): - debug(f'Loaded mirror {self._hostname}' + (f' with current score of {self.score}' if self.score else '')) - return self - - -class MirrorStatusListV3(BaseModel): - cutoff: int - last_check: datetime.datetime - num_checks: int - urls: list[MirrorStatusEntryV3] - version: int - - @model_validator(mode='before') - @classmethod - def check_model( - cls, - data: dict[str, int | datetime.datetime | list[MirrorStatusEntryV3]], - ) -> dict[str, int | datetime.datetime | list[MirrorStatusEntryV3]]: - if data.get('version') == 3: - return data - - raise ValueError('MirrorStatusListV3 only accepts version 3 data from https://archlinux.org/mirrors/status/json/') - - -@dataclass -class MirrorRegion: - name: str - urls: list[str] - - def json(self) -> dict[str, list[str]]: - return {self.name: self.urls} - - @override - def __eq__(self, other: object) -> bool: - if not isinstance(other, MirrorRegion): - return NotImplemented - return self.name == other.name - - -class SignCheck(Enum): - Never = 'Never' - Optional = 'Optional' - Required = 'Required' - - -class SignOption(Enum): - TrustedOnly = 'TrustedOnly' - TrustAll = 'TrustAll' - - -class _CustomRepositorySerialization(TypedDict): - name: str - url: str - sign_check: str - sign_option: str - - -@dataclass -class CustomRepository: - name: str - url: str - sign_check: SignCheck - sign_option: SignOption - - def table_data(self) -> dict[str, str]: - return { - 'Name': self.name, - 'Url': self.url, - 'Sign check': self.sign_check.value, - 'Sign options': self.sign_option.value, - } - - def json(self) -> _CustomRepositorySerialization: - return { - 'name': self.name, - 'url': self.url, - 'sign_check': self.sign_check.value, - 'sign_option': self.sign_option.value, - } - - @classmethod - def parse_args(cls, args: list[dict[str, str]]) -> list[Self]: - configs = [] - for arg in args: - configs.append( - cls( - arg['name'], - arg['url'], - SignCheck(arg['sign_check']), - SignOption(arg['sign_option']), - ), - ) - - return configs - - -@dataclass -class CustomServer: - url: str - - def table_data(self) -> dict[str, str]: - return {'Url': self.url} - - def json(self) -> dict[str, str]: - return {'url': self.url} - - @classmethod - def parse_args(cls, args: list[dict[str, str]]) -> list[Self]: - configs = [] - for arg in args: - configs.append( - cls(arg['url']), - ) - - return configs - - -class _MirrorConfigurationSerialization(TypedDict): - mirror_regions: dict[str, list[str]] - custom_servers: list[CustomServer] - optional_repositories: list[str] - custom_repositories: list[_CustomRepositorySerialization] - - -@dataclass -class MirrorConfiguration(SubConfig): - mirror_regions: list[MirrorRegion] = field(default_factory=list) - custom_servers: list[CustomServer] = field(default_factory=list) - optional_repositories: list[Repository] = field(default_factory=list) - custom_repositories: list[CustomRepository] = field(default_factory=list) - - @property - def region_names(self) -> str: - return '\n'.join(m.name for m in self.mirror_regions) - - @property - def custom_server_urls(self) -> str: - return '\n'.join(s.url for s in self.custom_servers) - - @override - def json(self) -> _MirrorConfigurationSerialization: - regions = {} - for m in self.mirror_regions: - regions.update(m.json()) - - return { - 'mirror_regions': regions, - 'custom_servers': self.custom_servers, - 'optional_repositories': [r.value for r in self.optional_repositories], - 'custom_repositories': [c.json() for c in self.custom_repositories], - } - - @override - def summary(self) -> list[str]: - out: list[str] = [] - - if self.mirror_regions: - out.append(tr('Mirror regions "{}"').format(', '.join(m.name for m in self.mirror_regions))) - - if self.optional_repositories: - out.append(tr('Optional repositories "{}"').format(', '.join(r.value for r in self.optional_repositories))) - - if self.custom_servers: - out.append(tr('Custom servers set up')) - - if self.custom_repositories: - out.append(tr('Custom repositories set up')) - - return out - - def custom_servers_config(self) -> str: - config = '' - - if self.custom_servers: - config += '## Custom Servers\n' - for server in self.custom_servers: - config += f'Server = {server.url}\n' - - return config.strip() - - def regions_config( - self, - mirror_list_handler: MirrorListHandler, - speed_sort: bool = True, - ) -> str: - config = '' - - for mirror_region in self.mirror_regions: - sorted_stati = mirror_list_handler.get_status_by_region( - mirror_region.name, - speed_sort=speed_sort, - ) - - config += f'\n\n## {mirror_region.name}\n' - - for status in sorted_stati: - config += f'Server = {status.server_url}\n' - - return config - - def repositories_config(self) -> str: - config = '' - - for repo in self.custom_repositories: - config += f'\n\n[{repo.name}]\n' - config += f'SigLevel = {repo.sign_check.value} {repo.sign_option.value}\n' - config += f'Server = {repo.url}\n' - - return config - - @classmethod - def parse_args( - cls, - args: dict[str, Any], - backwards_compatible_repo: list[Repository] = [], - ) -> Self: - config = cls() - - mirror_regions = args.get('mirror_regions', []) - if mirror_regions: - for region, urls in mirror_regions.items(): - config.mirror_regions.append(MirrorRegion(region, urls)) - - if args.get('custom_servers'): - config.custom_servers = CustomServer.parse_args(args['custom_servers']) - - # backwards compatibility with the new custom_repository - if 'custom_mirrors' in args: - config.custom_repositories = CustomRepository.parse_args(args['custom_mirrors']) - if 'custom_repositories' in args: - config.custom_repositories = CustomRepository.parse_args(args['custom_repositories']) - - if 'optional_repositories' in args: - config.optional_repositories = [Repository(r) for r in args['optional_repositories']] - - if backwards_compatible_repo: - for r in backwards_compatible_repo: - if r not in config.optional_repositories: - config.optional_repositories.append(r) - - return config diff --git a/archinstall/lib/models/pacman.py b/archinstall/lib/models/pacman.py index 004dfbc3f6..de359df813 100644 --- a/archinstall/lib/models/pacman.py +++ b/archinstall/lib/models/pacman.py @@ -1,46 +1,368 @@ -from dataclasses import dataclass -from typing import Self, TypedDict, override +import datetime +import http.client +import urllib.error +import urllib.parse +import urllib.request +from dataclasses import dataclass, field +from enum import Enum +from typing import TYPE_CHECKING, Any, Self, TypedDict, override + +from pydantic import BaseModel, ValidationInfo, field_validator, model_validator from archinstall.lib.models.config import SubConfig +from archinstall.lib.models.packages import Repository +from archinstall.lib.networking import DownloadTimer, ping +from archinstall.lib.output import debug from archinstall.lib.translationhandler import tr +if TYPE_CHECKING: + from archinstall.lib.mirror.mirror_handler import MirrorListHandler + + +class MirrorStatusEntryV3(BaseModel): + url: str + protocol: str + active: bool + country: str + country_code: str + isos: bool + ipv4: bool + ipv6: bool + details: str + delay: int | None = None + last_sync: datetime.datetime | None = None + duration_avg: float | None = None + duration_stddev: float | None = None + completion_pct: float | None = None + score: float | None = None + _latency: float | None = None + _speed: float | None = None + _hostname: str | None = None + _port: int | None = None + _speedtest_retries: int | None = None + + @property + def server_url(self) -> str: + return f'{self.url}$repo/os/$arch' + + @property + def speed(self) -> float: + if self._speed is None: + if not self._speedtest_retries: + self._speedtest_retries = 3 + elif self._speedtest_retries < 1: + self._speedtest_retries = 1 + + retry = 0 + while retry < self._speedtest_retries and self._speed is None: + debug(f'Checking download speed of {self._hostname}[{self.score}] by fetching: {self.url}core/os/x86_64/core.db') + req = urllib.request.Request(url=f'{self.url}core/os/x86_64/core.db') + + try: + with urllib.request.urlopen(req, None, 5) as handle, DownloadTimer(timeout=5) as timer: + size = len(handle.read()) + + assert timer.time is not None + self._speed = size / timer.time + debug(f' speed: {self._speed} ({int(self._speed / 1024 / 1024 * 100) / 100}MiB/s)') + # Do not retry error + except urllib.error.URLError as error: + debug(f' speed: ({error}), skip') + self._speed = 0 + # Do retry error + except (http.client.IncompleteRead, ConnectionResetError) as error: + debug(f' speed: ({error}), retry') + # Catch all + except Exception as error: + debug(f' speed: ({error}), skip') + self._speed = 0 + + retry += 1 + + if self._speed is None: + self._speed = 0 + + return self._speed + + @property + def latency(self) -> float | None: + """ + Latency measures the milliseconds between one ICMP request & response. + It only does so once because we check if self._latency is None, and a ICMP timeout result in -1 + We do this because some hosts blocks ICMP so we'll have to rely on .speed() instead which is slower. + """ + if self._latency is None: + debug(f'Checking latency for {self.url}') + assert self._hostname is not None + self._latency = ping(self._hostname, timeout=2) + debug(f' latency: {self._latency}') + + return self._latency + + @classmethod + @field_validator('score', mode='before') + def validate_score(cls, value: float) -> int | None: + if value is not None: + value = round(value) + debug(f' score: {value}') + + return value + + @model_validator(mode='after') + def debug_output(self, info: ValidationInfo) -> Self: + self._hostname, *port = urllib.parse.urlparse(self.url).netloc.split(':', 1) + self._port = int(port[0]) if port and len(port) >= 1 else None + + if (ctx := info.context) and ctx.get('verbose'): + debug(f'Loaded mirror {self._hostname}' + (f' with current score of {self.score}' if self.score else '')) + return self + + +class MirrorStatusListV3(BaseModel): + cutoff: int + last_check: datetime.datetime + num_checks: int + urls: list[MirrorStatusEntryV3] + version: int + + @model_validator(mode='before') + @classmethod + def check_model( + cls, + data: dict[str, int | datetime.datetime | list[MirrorStatusEntryV3]], + ) -> dict[str, int | datetime.datetime | list[MirrorStatusEntryV3]]: + if data.get('version') == 3: + return data + + raise ValueError('MirrorStatusListV3 only accepts version 3 data from https://archlinux.org/mirrors/status/json/') + + +@dataclass +class MirrorRegion: + name: str + urls: list[str] + + def json(self) -> dict[str, list[str]]: + return {self.name: self.urls} + + @override + def __eq__(self, other: object) -> bool: + if not isinstance(other, MirrorRegion): + return NotImplemented + return self.name == other.name + + +class SignCheck(Enum): + Never = 'Never' + Optional = 'Optional' + Required = 'Required' + -class PacmanConfigSerialization(TypedDict): +class SignOption(Enum): + TrustedOnly = 'TrustedOnly' + TrustAll = 'TrustAll' + + +class _CustomRepositorySerialization(TypedDict): + name: str + url: str + sign_check: str + sign_option: str + + +@dataclass +class CustomRepository: + name: str + url: str + sign_check: SignCheck + sign_option: SignOption + + def table_data(self) -> dict[str, str]: + return { + 'Name': self.name, + 'Url': self.url, + 'Sign check': self.sign_check.value, + 'Sign options': self.sign_option.value, + } + + def json(self) -> _CustomRepositorySerialization: + return { + 'name': self.name, + 'url': self.url, + 'sign_check': self.sign_check.value, + 'sign_option': self.sign_option.value, + } + + @classmethod + def parse_args(cls, args: list[dict[str, str]]) -> list[Self]: + configs = [] + for arg in args: + configs.append( + cls( + arg['name'], + arg['url'], + SignCheck(arg['sign_check']), + SignOption(arg['sign_option']), + ), + ) + + return configs + + +@dataclass +class CustomServer: + url: str + + def table_data(self) -> dict[str, str]: + return {'Url': self.url} + + def json(self) -> dict[str, str]: + return {'url': self.url} + + @classmethod + def parse_args(cls, args: list[dict[str, str]]) -> list[Self]: + configs = [] + for arg in args: + configs.append( + cls(arg['url']), + ) + + return configs + + +class _PacmanConfigurationSerialization(TypedDict): + mirror_regions: dict[str, list[str]] + custom_servers: list[CustomServer] + optional_repositories: list[str] + custom_repositories: list[_CustomRepositorySerialization] parallel_downloads: int color: bool @dataclass class PacmanConfiguration(SubConfig): + mirror_regions: list[MirrorRegion] = field(default_factory=list) + custom_servers: list[CustomServer] = field(default_factory=list) + optional_repositories: list[Repository] = field(default_factory=list) + custom_repositories: list[CustomRepository] = field(default_factory=list) parallel_downloads: int = 5 color: bool = True - @classmethod - def default(cls) -> Self: - return cls() + @property + def region_names(self) -> str: + return '\n'.join(m.name for m in self.mirror_regions) + + @property + def custom_server_urls(self) -> str: + return '\n'.join(s.url for s in self.custom_servers) @override - def json(self) -> PacmanConfigSerialization: + def json(self) -> _PacmanConfigurationSerialization: + regions = {} + for m in self.mirror_regions: + regions.update(m.json()) + return { + 'mirror_regions': regions, + 'custom_servers': self.custom_servers, + 'optional_repositories': [r.value for r in self.optional_repositories], + 'custom_repositories': [c.json() for c in self.custom_repositories], 'parallel_downloads': self.parallel_downloads, 'color': self.color, } @override - def summary(self) -> str | None: + def summary(self) -> list[str]: + out: list[str] = [] + + if self.mirror_regions: + out.append(tr('Mirror regions "{}"').format(', '.join(m.name for m in self.mirror_regions))) + + if self.optional_repositories: + out.append(tr('Optional repositories "{}"').format(', '.join(r.value for r in self.optional_repositories))) + + if self.custom_servers: + out.append(tr('Custom servers set up')) + + if self.custom_repositories: + out.append(tr('Custom repositories set up')) + + out.append(tr('Parallel downloads: {}').format(self.parallel_downloads)) + if self.color: - return tr('Color enabled') - return None + out.append(tr('Color enabled')) + + return out + + def custom_servers_config(self) -> str: + config = '' + + if self.custom_servers: + config += '## Custom Servers\n' + for server in self.custom_servers: + config += f'Server = {server.url}\n' + + return config.strip() + + def regions_config( + self, + mirror_list_handler: MirrorListHandler, + speed_sort: bool = True, + ) -> str: + config = '' + + for mirror_region in self.mirror_regions: + sorted_stati = mirror_list_handler.get_status_by_region( + mirror_region.name, + speed_sort=speed_sort, + ) + + config += f'\n\n## {mirror_region.name}\n' + + for status in sorted_stati: + config += f'Server = {status.server_url}\n' - def preview(self) -> str: - color_str = str(self.color) - output = '{}: {}\n'.format(tr('Parallel Downloads'), self.parallel_downloads) - output += '{}: {}'.format(tr('Color'), color_str) - return output + return config + + def repositories_config(self) -> str: + config = '' + + for repo in self.custom_repositories: + config += f'\n\n[{repo.name}]\n' + config += f'SigLevel = {repo.sign_check.value} {repo.sign_option.value}\n' + config += f'Server = {repo.url}\n' + + return config @classmethod - def parse_arg(cls, args: PacmanConfigSerialization) -> Self: - config = cls.default() + def parse_args( + cls, + args: dict[str, Any], + backwards_compatible_repo: list[Repository] = [], + ) -> Self: + config = cls() + + mirror_regions = args.get('mirror_regions', []) + if mirror_regions: + for region, urls in mirror_regions.items(): + config.mirror_regions.append(MirrorRegion(region, urls)) + + if args.get('custom_servers'): + config.custom_servers = CustomServer.parse_args(args['custom_servers']) + + # backwards compatibility with the new custom_repository + if 'custom_mirrors' in args: + config.custom_repositories = CustomRepository.parse_args(args['custom_mirrors']) + if 'custom_repositories' in args: + config.custom_repositories = CustomRepository.parse_args(args['custom_repositories']) + + if 'optional_repositories' in args: + config.optional_repositories = [Repository(r) for r in args['optional_repositories']] + + if backwards_compatible_repo: + for r in backwards_compatible_repo: + if r not in config.optional_repositories: + config.optional_repositories.append(r) if 'parallel_downloads' in args: config.parallel_downloads = int(args['parallel_downloads']) diff --git a/archinstall/lib/pacman/pacman_menu.py b/archinstall/lib/pacman/pacman_menu.py index 16ad6d2b58..6939aa0210 100644 --- a/archinstall/lib/pacman/pacman_menu.py +++ b/archinstall/lib/pacman/pacman_menu.py @@ -1,58 +1,318 @@ from typing import override from archinstall.lib.menu.abstract_menu import AbstractSubMenu -from archinstall.lib.menu.helpers import Confirmation, Input -from archinstall.lib.models.pacman import PacmanConfiguration +from archinstall.lib.menu.helpers import Confirmation, Input, Loading, Selection +from archinstall.lib.menu.list_manager import ListManager +from archinstall.lib.mirror.mirror_handler import MirrorListHandler +from archinstall.lib.models.packages import Repository +from archinstall.lib.models.pacman import ( + CustomRepository, + CustomServer, + MirrorRegion, + PacmanConfiguration, + SignCheck, + SignOption, +) +from archinstall.lib.output import FormattedOutput from archinstall.lib.pathnames import PACMAN_CONF from archinstall.lib.translationhandler import tr from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType +class CustomMirrorRepositoriesList(ListManager[CustomRepository]): + def __init__(self, custom_repositories: list[CustomRepository]): + self._actions = [ + tr('Add a custom repository'), + tr('Change custom repository'), + tr('Delete custom repository'), + ] + + super().__init__( + custom_repositories, + [self._actions[0]], + self._actions[1:], + '', + ) + + async def show(self) -> list[CustomRepository] | None: + return await super()._run() + + @override + def selected_action_display(self, selection: CustomRepository) -> str: + return selection.name + + @override + async def handle_action( + self, + action: str, + entry: CustomRepository | None, + data: list[CustomRepository], + ) -> list[CustomRepository]: + if action == self._actions[0]: # add + new_repo = await self._add_custom_repository() + if new_repo is not None: + data = [d for d in data if d.name != new_repo.name] + data += [new_repo] + elif action == self._actions[1] and entry: # modify repo + new_repo = await self._add_custom_repository(entry) + if new_repo is not None: + data = [d for d in data if d.name != entry.name] + data += [new_repo] + elif action == self._actions[2] and entry: # delete + data = [d for d in data if d != entry] + + return data + + async def _add_custom_repository(self, preset: CustomRepository | None = None) -> CustomRepository | None: + edit_result = await Input( + header=tr('Enter a respository name'), + allow_skip=True, + default_value=preset.name if preset else None, + ).show() + + match edit_result.type_: + case ResultType.Selection: + name = edit_result.get_value() + case ResultType.Skip: + return preset + case _: + raise ValueError('Unhandled return type') + + header = f'{tr("Name")}: {name}\n' + prompt = f'{header}\n' + tr('Enter the repository url') + + edit_result = await Input( + header=prompt, + allow_skip=True, + default_value=preset.url if preset else None, + ).show() + + match edit_result.type_: + case ResultType.Selection: + url = edit_result.get_value() + case ResultType.Skip: + return preset + case _: + raise ValueError('Unhandled return type') + + header += f'{tr("Url")}: {url}\n' + prompt = f'{header}\n' + tr('Select signature check') + + sign_chk_items = [MenuItem(s.value, value=s.value) for s in SignCheck] + group = MenuItemGroup(sign_chk_items, sort_items=False) + + if preset is not None: + group.set_selected_by_value(preset.sign_check.value) + + result = await Selection[SignCheck]( + group, + header=prompt, + allow_skip=False, + ).show() + + match result.type_: + case ResultType.Selection: + sign_check = SignCheck(result.get_value()) + case _: + raise ValueError('Unhandled return type') + + header += f'{tr("Signature check")}: {sign_check.value}\n' + prompt = f'{header}\n' + tr('Select signature option') + + sign_opt_items = [MenuItem(s.value, value=s.value) for s in SignOption] + group = MenuItemGroup(sign_opt_items, sort_items=False) + + if preset is not None: + group.set_selected_by_value(preset.sign_option.value) + + result = await Selection( + group, + header=prompt, + allow_skip=False, + ).show() + + match result.type_: + case ResultType.Selection: + sign_opt = SignOption(result.get_value()) + case _: + raise ValueError('Unhandled return type') + + return CustomRepository(name, url, sign_check, sign_opt) + + +class CustomMirrorServersList(ListManager[CustomServer]): + def __init__(self, custom_servers: list[CustomServer]): + self._actions = [ + tr('Add a custom server'), + tr('Change custom server'), + tr('Delete custom server'), + ] + + super().__init__( + custom_servers, + [self._actions[0]], + self._actions[1:], + '', + ) + + async def show(self) -> list[CustomServer] | None: + return await super()._run() + + @override + def selected_action_display(self, selection: CustomServer) -> str: + return selection.url + + @override + async def handle_action( + self, + action: str, + entry: CustomServer | None, + data: list[CustomServer], + ) -> list[CustomServer]: + if action == self._actions[0]: # add + new_server = await self._add_custom_server() + if new_server is not None: + data = [d for d in data if d.url != new_server.url] + data += [new_server] + elif action == self._actions[1] and entry: # modify repo + new_server = await self._add_custom_server(entry) + if new_server is not None: + data = [d for d in data if d.url != entry.url] + data += [new_server] + elif action == self._actions[2] and entry: # delete + data = [d for d in data if d != entry] + + return data + + async def _add_custom_server(self, preset: CustomServer | None = None) -> CustomServer | None: + edit_result = await Input( + header=tr('Enter server url'), + allow_skip=True, + default_value=preset.url if preset else None, + ).show() + + match edit_result.type_: + case ResultType.Selection: + uri = edit_result.get_value() + return CustomServer(uri) + case ResultType.Skip: + return preset + case _: + return None + + class PacmanMenu(AbstractSubMenu[PacmanConfiguration]): def __init__( self, - pacman_conf: PacmanConfiguration, - advanced: bool = False, + mirror_list_handler: MirrorListHandler, + preset: PacmanConfiguration | None = None, ): - self._pacman_conf = pacman_conf - self._advanced = advanced + if preset: + self._pacman_config = preset + else: + self._pacman_config = PacmanConfiguration() + + self._mirror_list_handler = mirror_list_handler + menu_options = self._define_menu_options() + self._item_group = MenuItemGroup(menu_options, checkmarks=True) - self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True) super().__init__( self._item_group, - config=self._pacman_conf, + config=self._pacman_config, allow_reset=True, ) def _define_menu_options(self) -> list[MenuItem]: return [ + MenuItem( + text=tr('Select regions'), + action=lambda x: select_mirror_regions(self._mirror_list_handler, x), + value=self._pacman_config.mirror_regions, + preview_action=self._prev_regions, + key='mirror_regions', + ), + MenuItem( + text=tr('Add custom servers'), + action=add_custom_mirror_servers, + value=self._pacman_config.custom_servers, + preview_action=self._prev_custom_servers, + key='custom_servers', + ), + MenuItem( + text=tr('Optional repositories'), + action=select_optional_repositories, + value=[], + preview_action=self._prev_additional_repos, + key='optional_repositories', + ), + MenuItem( + text=tr('Add custom repository'), + action=select_custom_mirror, + value=self._pacman_config.custom_repositories, + preview_action=self._prev_custom_mirror, + key='custom_repositories', + ), MenuItem( text=tr('Parallel Downloads'), action=select_parallel_downloads, - value=self._pacman_conf.parallel_downloads, + value=self._pacman_config.parallel_downloads, preview_action=lambda item: str(item.get_value()), key='parallel_downloads', - enabled=self._advanced, ), MenuItem( text=tr('Color'), action=select_color, - value=self._pacman_conf.color, + value=self._pacman_config.color, preview_action=lambda item: str(item.get_value()), key='color', ), ] + def _prev_regions(self, item: MenuItem) -> str: + regions = item.get_value() + + output = '' + for region in regions: + output += f'{region.name}\n' + + for url in region.urls: + output += f' - {url}\n' + + output += '\n' + + return output + + def _prev_additional_repos(self, item: MenuItem) -> str | None: + if item.value: + repositories: list[Repository] = item.value + repos = ', '.join(repo.value for repo in repositories) + return f'{tr("Additional repositories")}: {repos}' + return None + + def _prev_custom_mirror(self, item: MenuItem) -> str | None: + if not item.value: + return None + + custom_mirrors: list[CustomRepository] = item.value + output = FormattedOutput.as_table(custom_mirrors) + return output.strip() + + def _prev_custom_servers(self, item: MenuItem) -> str | None: + if not item.value: + return None + + custom_servers: list[CustomServer] = item.value + output = '\n'.join(server.url for server in custom_servers) + return output.strip() + @override async def show(self) -> PacmanConfiguration | None: config = await super().show() - if config is None: - return PacmanConfiguration.default() - - _apply_to_live(config.parallel_downloads) + if config is not None: + _apply_to_live(config.parallel_downloads) return config @@ -115,3 +375,96 @@ async def select_color(preset: bool = True) -> bool | None: return True case ResultType.Selection: return result.get_value() + + +async def select_mirror_regions( + mirror_list_handler: MirrorListHandler, + preset: list[MirrorRegion], +) -> list[MirrorRegion]: + await Loading[None]( + header=tr('Loading mirror regions...'), + data_callback=mirror_list_handler.load_mirrors, + ).show() + + available_regions = mirror_list_handler.get_mirror_regions() + + if not available_regions: + return [] + + preset_regions = [region for region in available_regions if region in preset] + + items = [MenuItem(region.name, value=region) for region in available_regions] + group = MenuItemGroup(items, sort_items=True) + + group.set_selected_by_value(preset_regions) + + result = await Selection[MirrorRegion]( + group, + header=tr('Select mirror regions to be enabled'), + allow_reset=True, + allow_skip=True, + multi=True, + enable_filter=True, + ).show() + + match result.type_: + case ResultType.Skip: + return preset_regions + case ResultType.Reset: + return [] + case ResultType.Selection: + selected_mirrors = result.get_values() + return selected_mirrors + + +async def add_custom_mirror_servers(preset: list[CustomServer] = []) -> list[CustomServer]: + custom_mirrors = await CustomMirrorServersList(preset).show() + + if not custom_mirrors: + return preset + + return custom_mirrors + + +async def select_custom_mirror(preset: list[CustomRepository] = []) -> list[CustomRepository]: + custom_mirrors = await CustomMirrorRepositoriesList(preset).show() + + if not custom_mirrors: + return preset + + return custom_mirrors + + +async def select_optional_repositories(preset: list[Repository]) -> list[Repository]: + """ + Allows the user to select additional repositories (multilib, and testing) if desired. + + :return: The string as a selected repository + :rtype: Repository + """ + + repositories = [ + Repository.Multilib, + Repository.MultilibTesting, + Repository.CoreTesting, + Repository.ExtraTesting, + ] + items = [MenuItem(r.value, value=r) for r in repositories] + group = MenuItemGroup(items, sort_items=False) + group.set_selected_by_value(preset) + + result = await Selection[Repository]( + group, + header=tr('Select optional repositories to be enabled'), + allow_reset=True, + allow_skip=True, + multi=True, + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Reset: + return [] + case ResultType.Selection: + return result.get_values() diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index aab02cfd4d..8c506a8992 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -73,7 +73,7 @@ def perform_installation( disk_config = config.disk_config 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 [] + optional_repositories = config.pacman_config.optional_repositories if config.pacman_config else [] mountpoint = disk_config.mountpoint if disk_config.mountpoint else mountpoint with Installer( @@ -97,8 +97,8 @@ def perform_installation( # generate encryption key files for the mounted luks devices installation.generate_key_files() - if mirror_config := config.mirror_config: - installation.set_mirrors(mirror_list_handler, mirror_config, on_target=False) + if pacman_config := config.pacman_config: + installation.set_mirrors(mirror_list_handler, pacman_config, on_target=False) installation.minimal_installation( optional_repositories=optional_repositories, @@ -108,8 +108,8 @@ def perform_installation( pacman_config=config.pacman_config, ) - if mirror_config := config.mirror_config: - installation.set_mirrors(mirror_list_handler, mirror_config, on_target=True) + if pacman_config := config.pacman_config: + installation.set_mirrors(mirror_list_handler, pacman_config, on_target=True) if config.swap and config.swap.enabled: installation.setup_swap(algo=config.swap.algorithm) diff --git a/docs/cli_parameters/config/config_options.csv b/docs/cli_parameters/config/config_options.csv index a902d1d20e..1e05cbce5a 100644 --- a/docs/cli_parameters/config/config_options.csv +++ b/docs/cli_parameters/config/config_options.csv @@ -10,7 +10,7 @@ hostname,``str``,A string defining your machines hostname on the network *(defau kernels,[ `linux `_!, `linux-hardened `_!, `linux-lts `_!, `linux-rt `_!, `linux-rt-lts `_!, `linux-zen `_ ],Defines which kernels should be installed and setup in the boot loader options,Yes custom_commands,*Read more under* :ref:`custom commands`,Custom commands that will be run post-install chrooted inside the installed system,No locale_config,{kb_layout: `lang `__!, sys_enc: `Character encoding `_!, sys_lang: `locale `_},Defines the keyboard key map!, system encoding and system locale,No -mirror_config,{custom_mirrors: [ https://... ]!, mirror_regions: { "Worldwide": [ "https://geo.mirror.pkgbuild.com/$repo/os/$arch" ] } },Sets various mirrors *(defaults to ISO's ``/etc/pacman.d/mirrors`` if not defined)*,No +pacman_config,{custom_mirrors: [ https://... ]!, mirror_regions: { "Worldwide": [ "https://geo.mirror.pkgbuild.com/$repo/os/$arch" ] } },Sets various mirrors *(defaults to ISO's ``/etc/pacman.d/mirrors`` if not defined)*,No network_config,*`see options under Network Configuration`*,Sets which type of *(if any)* network configuration should be used,No no_pkg_lookups,``true``!, ``false``,Disabled package checking against https://archlinux.org/packages/,No ntp,``true``!, ``false``,enables or disables `NTP `_ during installation,No diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index 562d2b2980..517d3c01ab 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -192,7 +192,7 @@ The contents of :code:`https://domain.lan/config.json`: "sys_enc": "UTF-8", "sys_lang": "en_US" }, - "mirror_config": { + "pacman_config": { "custom_servers": [ { "url": "https://mymirror.com/$repo/os/$arch" diff --git a/examples/config-sample.json b/examples/config-sample.json index bc4cd2d924..19da96f486 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -91,7 +91,7 @@ "sys_enc": "UTF-8", "sys_lang": "en_US" }, - "mirror_config": { + "pacman_config": { "custom_servers": [ { "url": "https://mymirror.com/$repo/os/$arch" diff --git a/tests/conftest.py b/tests/conftest.py index 819c839716..85958fa127 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,8 +29,8 @@ def deprecated_creds_config() -> Path: @pytest.fixture(scope='session') -def deprecated_mirror_config() -> Path: - return Path(__file__).parent / 'data' / 'test_deprecated_mirror_config.json' +def deprecated_pacman_config() -> Path: + return Path(__file__).parent / 'data' / 'test_deprecated_pacman_config.json' @pytest.fixture(scope='session') diff --git a/tests/data/test_config.json b/tests/data/test_config.json index 618bd8e9c7..bdac0aebcf 100644 --- a/tests/data/test_config.json +++ b/tests/data/test_config.json @@ -138,7 +138,7 @@ "sys_enc": "UTF-8", "sys_lang": "en_US" }, - "mirror_config": { + "pacman_config": { "custom_servers": [ { "url": "https://mymirror.com/$repo/os/$arch" @@ -159,7 +159,9 @@ "sign_check": "Required", "sign_option": "TrustAll" } - ] + ], + "parallel_downloads": 66, + "color": true }, "network_config": { "type": "manual", @@ -180,7 +182,6 @@ "packages": [ "firefox" ], - "parallel_downloads": 66, "profile_config": { "gfx_driver": "All open-source", "greeter": "lightdm-gtk-greeter", diff --git a/tests/data/test_deprecated_mirror_config.json b/tests/data/test_deprecated_pacman_config.json similarity index 94% rename from tests/data/test_deprecated_mirror_config.json rename to tests/data/test_deprecated_pacman_config.json index ee82a97404..b0e17135d0 100644 --- a/tests/data/test_deprecated_mirror_config.json +++ b/tests/data/test_deprecated_pacman_config.json @@ -1,6 +1,6 @@ { "additional-repositories": ["testing"], - "mirror_config": { + "pacman_config": { "custom_mirrors": [ { "name": "my_mirror", diff --git a/tests/test_args.py b/tests/test_args.py index 4cbad41bae..f8d4b4a2ea 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -19,10 +19,9 @@ 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 from archinstall.lib.models.network import NetworkConfiguration, Nic, NicType from archinstall.lib.models.packages import Repository -from archinstall.lib.models.pacman import PacmanConfiguration +from archinstall.lib.models.pacman import CustomRepository, CustomServer, MirrorRegion, PacmanConfiguration, SignCheck, SignOption from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.users import Password, User from archinstall.lib.profile.profiles_handler import profile_handler @@ -193,7 +192,7 @@ def test_config_file_parsing( gfx_driver=GfxDriver.AllOpenSource, greeter=GreeterType.Lightdm, ), - mirror_config=MirrorConfiguration( + pacman_config=PacmanConfiguration( mirror_regions=[ MirrorRegion( name='Australia', @@ -210,6 +209,7 @@ def test_config_file_parsing( sign_option=SignOption.TrustAll, ), ], + parallel_downloads=66, ), network_config=NetworkConfiguration( type=NicType.MANUAL, @@ -235,7 +235,6 @@ def test_config_file_parsing( kernels=['linux-zen'], ntp=True, packages=['firefox'], - pacman_config=PacmanConfiguration(parallel_downloads=66), swap=ZramConfiguration(enabled=False), timezone='UTC', services=['service_1', 'service_2'], @@ -243,23 +242,23 @@ def test_config_file_parsing( ) -def test_deprecated_mirror_config_parsing( +def test_deprecated_pacman_config_parsing( monkeypatch: MonkeyPatch, - deprecated_mirror_config: Path, + deprecated_pacman_config: Path, ) -> None: monkeypatch.setattr( 'sys.argv', [ 'archinstall', '--config', - str(deprecated_mirror_config), + str(deprecated_pacman_config), ], ) handler = ArchConfigHandler() arch_config = handler.config - assert arch_config.mirror_config == MirrorConfiguration( + assert arch_config.pacman_config == PacmanConfiguration( mirror_regions=[ MirrorRegion( name='Australia', diff --git a/tests/test_configuration_output.py b/tests/test_configuration_output.py index 7d9f1f97cf..d1a9af2266 100644 --- a/tests/test_configuration_output.py +++ b/tests/test_configuration_output.py @@ -38,10 +38,10 @@ def test_user_config_roundtrip( result['disk_config']['device_modifications'] = expected['disk_config']['device_modifications'] assert json.dumps( - result['mirror_config'], + result['pacman_config'], sort_keys=True, ) == json.dumps( - expected['mirror_config'], + expected['pacman_config'], sort_keys=True, )