From 89cefb9a1c7d4c4968e7d8645149078e601c9d1c Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 12 May 2023 02:30:09 +1000 Subject: Cleanup imports and unused code (#1801) * Cleanup imports and unused code * Update build check * Keep deprecation exception * Simplify logging --------- Co-authored-by: Daniel Girtler --- archinstall/lib/interactions/__init__.py | 20 ++ archinstall/lib/interactions/disk_conf.py | 393 ++++++++++++++++++++++ archinstall/lib/interactions/general_conf.py | 243 +++++++++++++ archinstall/lib/interactions/locale_conf.py | 43 +++ archinstall/lib/interactions/manage_users_conf.py | 106 ++++++ archinstall/lib/interactions/network_conf.py | 172 ++++++++++ archinstall/lib/interactions/system_conf.py | 117 +++++++ archinstall/lib/interactions/utils.py | 34 ++ 8 files changed, 1128 insertions(+) create mode 100644 archinstall/lib/interactions/__init__.py create mode 100644 archinstall/lib/interactions/disk_conf.py create mode 100644 archinstall/lib/interactions/general_conf.py create mode 100644 archinstall/lib/interactions/locale_conf.py create mode 100644 archinstall/lib/interactions/manage_users_conf.py create mode 100644 archinstall/lib/interactions/network_conf.py create mode 100644 archinstall/lib/interactions/system_conf.py create mode 100644 archinstall/lib/interactions/utils.py (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py new file mode 100644 index 00000000..b5691a10 --- /dev/null +++ b/archinstall/lib/interactions/__init__.py @@ -0,0 +1,20 @@ +from .locale_conf import select_locale_lang, select_locale_enc +from .manage_users_conf import UserList, ask_for_additional_users +from .network_conf import ManualNetworkConfig, ask_to_configure_network +from .utils import get_password + +from .disk_conf import ( + select_devices, select_disk_config, get_default_partition_layout, + select_main_filesystem_format, suggest_single_disk_layout, + suggest_multi_disk_layout +) + +from .general_conf import ( + ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_language, + select_mirror_regions, select_archinstall_language, ask_additional_packages_to_install, + add_number_of_parrallel_downloads, select_additional_repositories +) + +from .system_conf import ( + select_kernel, ask_for_bootloader, select_driver, ask_for_swap +) diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py new file mode 100644 index 00000000..78e4cff4 --- /dev/null +++ b/archinstall/lib/interactions/disk_conf.py @@ -0,0 +1,393 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, TYPE_CHECKING +from typing import Optional, List, Tuple + +from .. import disk +from ..hardware import SysInfo +from ..menu import Menu +from ..menu import TableMenu +from ..menu.menu import MenuSelectionType +from ..output import FormattedOutput, debug +from ..utils.util import prompt_dir + +if TYPE_CHECKING: + _: Any + + +def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]: + """ + Asks the user to select one or multiple devices + + :return: List of selected devices + :rtype: list + """ + + def _preview_device_selection(selection: disk._DeviceInfo) -> Optional[str]: + dev = disk.device_handler.get_device(selection.path) + if dev and dev.partition_infos: + return FormattedOutput.as_table(dev.partition_infos) + return None + + if preset is None: + preset = [] + + title = str(_('Select one or more devices to use and configure')) + warning = str(_('If you reset the device selection this will also reset the current disk layout. Are you sure?')) + + devices = disk.device_handler.devices + options = [d.device_info for d in devices] + preset_value = [p.device_info for p in preset] + + choice = TableMenu( + title, + data=options, + multi=True, + preset=preset_value, + preview_command=_preview_device_selection, + preview_title=str(_('Existing Partitions')), + preview_size=0.2, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: + selected_device_info: List[disk._DeviceInfo] = choice.value # type: ignore + selected_devices = [] + + for device in devices: + if device.device_info in selected_device_info: + selected_devices.append(device) + + return selected_devices + + +def get_default_partition_layout( + devices: List[disk.BDevice], + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_option: bool = False +) -> List[disk.DeviceModification]: + + if len(devices) == 1: + device_modification = suggest_single_disk_layout( + devices[0], + filesystem_type=filesystem_type, + advanced_options=advanced_option + ) + return [device_modification] + else: + return suggest_multi_disk_layout( + devices, + filesystem_type=filesystem_type, + advanced_options=advanced_option + ) + + +def _manual_partitioning( + preset: List[disk.DeviceModification], + devices: List[disk.BDevice] +) -> List[disk.DeviceModification]: + modifications = [] + for device in devices: + mod = next(filter(lambda x: x.device == device, preset), None) + if not mod: + mod = disk.DeviceModification(device, wipe=False) + + if partitions := disk.manual_partitioning(device, preset=mod.partitions): + mod.partitions = partitions + modifications.append(mod) + + return modifications + + +def select_disk_config( + preset: Optional[disk.DiskLayoutConfiguration] = None, + advanced_option: bool = False +) -> Optional[disk.DiskLayoutConfiguration]: + default_layout = disk.DiskLayoutType.Default.display_msg() + manual_mode = disk.DiskLayoutType.Manual.display_msg() + pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg() + + options = [default_layout, manual_mode, pre_mount_mode] + preset_value = preset.config_type.display_msg() if preset else None + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select a partitioning option'), + options, + allow_reset=True, + allow_reset_warning_msg=warning, + sort=False, + preview_size=0.2, + preset_values=preset_value + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + case MenuSelectionType.Selection: + if choice.single_value == pre_mount_mode: + output = "You will use whatever drive-setup is mounted at the specified directory\n" + output += "WARNING: Archinstall won't check the suitability of this setup\n" + + path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output) + mods = disk.device_handler.detect_pre_mounted_mods(path) + + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Pre_mount, + relative_mountpoint=path, + device_modifications=mods + ) + + preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] + + devices = select_devices(preset_devices) + + if not devices: + return None + + if choice.value == default_layout: + modifications = get_default_partition_layout(devices, advanced_option=advanced_option) + if modifications: + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Default, + device_modifications=modifications + ) + elif choice.value == manual_mode: + preset_mods = preset.device_modifications if preset else [] + modifications = _manual_partitioning(preset_mods, devices) + + if modifications: + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Manual, + device_modifications=modifications + ) + + return None + + +def _boot_partition() -> disk.PartitionModification: + if SysInfo.has_uefi(): + start = disk.Size(1, disk.Unit.MiB) + size = disk.Size(512, disk.Unit.MiB) + else: + start = disk.Size(3, disk.Unit.MiB) + size = disk.Size(203, disk.Unit.MiB) + + # boot partition + return disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=start, + length=size, + mountpoint=Path('/boot'), + fs_type=disk.FilesystemType.Fat32, + flags=[disk.PartitionFlag.Boot] + ) + + +def select_main_filesystem_format(advanced_options=False) -> disk.FilesystemType: + options = { + 'btrfs': disk.FilesystemType.Btrfs, + 'ext4': disk.FilesystemType.Ext4, + 'xfs': disk.FilesystemType.Xfs, + 'f2fs': disk.FilesystemType.F2fs + } + + if advanced_options: + options.update({'ntfs': disk.FilesystemType.Ntfs}) + + prompt = _('Select which filesystem your main partition should use') + choice = Menu(prompt, options, skip=False, sort=False).run() + return options[choice.single_value] + + +def suggest_single_disk_layout( + device: disk.BDevice, + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_options: bool = False, + separate_home: Optional[bool] = None +) -> disk.DeviceModification: + if not filesystem_type: + filesystem_type = select_main_filesystem_format(advanced_options) + + min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB) + root_partition_size = disk.Size(20, disk.Unit.GiB) + using_subvolumes = False + using_home_partition = False + compression = False + device_size_gib = device.device_info.total_size + + if filesystem_type == disk.FilesystemType.Btrfs: + prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + using_subvolumes = choice.value == Menu.yes() + + prompt = str(_('Would you like to use BTRFS compression?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + compression = choice.value == Menu.yes() + + device_modification = disk.DeviceModification(device, wipe=True) + + # Used for reference: https://wiki.archlinux.org/title/partitioning + # 2 MiB is unallocated for GRUB on BIOS. Potentially unneeded for other bootloaders? + + # TODO: On BIOS, /boot partition is only needed if the drive will + # be encrypted, otherwise it is not recommended. We should probably + # add a check for whether the drive will be encrypted or not. + + # Increase the UEFI partition if UEFI is detected. + # Also re-align the start to 1MiB since we don't need the first sectors + # like we do in MBR layouts where the boot loader is installed traditionally. + + boot_partition = _boot_partition() + device_modification.add_partition(boot_partition) + + if not using_subvolumes: + if device_size_gib >= min_size_to_allow_home_part: + if separate_home is None: + prompt = str(_('Would you like to create a separate partition for /home?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + using_home_partition = choice.value == Menu.yes() + elif separate_home is True: + using_home_partition = True + else: + using_home_partition = False + + # root partition + start = disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB) + + # Set a size for / (/root) + if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: + length = disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size) + else: + length = min(device.device_info.total_size, root_partition_size) + + root_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=start, + length=length, + mountpoint=Path('/') if not using_subvolumes else None, + fs_type=filesystem_type, + mount_options=['compress=zstd'] if compression else [], + ) + device_modification.add_partition(root_partition) + + if using_subvolumes: + # https://btrfs.wiki.kernel.org/index.php/FAQ + # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash + # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh + subvolumes = [ + disk.SubvolumeModification(Path('@'), Path('/')), + disk.SubvolumeModification(Path('@home'), Path('/home')), + disk.SubvolumeModification(Path('@log'), Path('/var/log')), + disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), + disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')) + ] + root_partition.btrfs_subvols = subvolumes + elif using_home_partition: + # If we don't want to use subvolumes, + # But we want to be able to re-use data between re-installs.. + # A second partition for /home would be nice if we have the space for it + home_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=root_partition.length, + length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + mountpoint=Path('/home'), + fs_type=filesystem_type, + mount_options=['compress=zstd'] if compression else [] + ) + device_modification.add_partition(home_partition) + + return device_modification + + +def suggest_multi_disk_layout( + devices: List[disk.BDevice], + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_options: bool = False +) -> List[disk.DeviceModification]: + if not devices: + return [] + + # Not really a rock solid foundation of information to stand on, but it's a start: + # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ + # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ + min_home_partition_size = disk.Size(40, disk.Unit.GiB) + # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? + desired_root_partition_size = disk.Size(20, disk.Unit.GiB) + compression = False + + if not filesystem_type: + filesystem_type = select_main_filesystem_format(advanced_options) + + # find proper disk for /home + possible_devices = list(filter(lambda x: x.device_info.total_size >= min_home_partition_size, devices)) + home_device = max(possible_devices, key=lambda d: d.device_info.total_size) if possible_devices else None + + # find proper device for /root + devices_delta = {} + for device in devices: + if device is not home_device: + delta = device.device_info.total_size - desired_root_partition_size + devices_delta[device] = delta + + sorted_delta: List[Tuple[disk.BDevice, Any]] = sorted(devices_delta.items(), key=lambda x: x[1]) + root_device: Optional[disk.BDevice] = sorted_delta[0][0] + + if home_device is None or root_device is None: + text = _('The selected drives do not have the minimum capacity required for an automatic suggestion\n') + text += _('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB)) + text += _('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB)) + Menu(str(text), [str(_('Continue'))], skip=False).run() + return [] + + if filesystem_type == disk.FilesystemType.Btrfs: + prompt = str(_('Would you like to use BTRFS compression?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + compression = choice.value == Menu.yes() + + device_paths = ', '.join([str(d.device_info.path) for d in devices]) + + debug(f"Suggesting multi-disk-layout for devices: {device_paths}") + debug(f"/root: {root_device.device_info.path}") + debug(f"/home: {home_device.device_info.path}") + + root_device_modification = disk.DeviceModification(root_device, wipe=True) + home_device_modification = disk.DeviceModification(home_device, wipe=True) + + # add boot partition to the root device + boot_partition = _boot_partition() + root_device_modification.add_partition(boot_partition) + + # add root partition to the root device + root_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB), + length=disk.Size(100, disk.Unit.Percent, total_size=root_device.device_info.total_size), + mountpoint=Path('/'), + mount_options=['compress=zstd'] if compression else [], + fs_type=filesystem_type + ) + root_device_modification.add_partition(root_partition) + + # add home partition to home device + home_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=disk.Size(1, disk.Unit.MiB), + length=disk.Size(100, disk.Unit.Percent, total_size=home_device.device_info.total_size), + mountpoint=Path('/home'), + mount_options=['compress=zstd'] if compression else [], + fs_type=filesystem_type, + ) + home_device_modification.add_partition(home_partition) + + return [root_device_modification, home_device_modification] diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py new file mode 100644 index 00000000..5fcfa633 --- /dev/null +++ b/archinstall/lib/interactions/general_conf.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +import pathlib +from typing import List, Any, Optional, Dict, TYPE_CHECKING + +from ..locale import list_keyboard_languages, list_timezones +from ..menu import MenuSelectionType, Menu, TextInput +from ..mirrors import list_mirrors +from ..output import warn +from ..packages.packages import validate_package_list +from ..storage import storage +from ..translationhandler import Language + +if TYPE_CHECKING: + _: Any + + +def ask_ntp(preset: bool = True) -> bool: + prompt = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n')) + prompt += str(_('Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki')) + if preset: + preset_val = Menu.yes() + else: + preset_val = Menu.no() + choice = Menu(prompt, Menu.yes_no(), skip=False, preset_values=preset_val, default_option=Menu.yes()).run() + + return False if choice.value == Menu.no() else True + + +def ask_hostname(preset: str = '') -> str: + while True: + hostname = TextInput( + str(_('Desired hostname for the installation: ')), + preset + ).run().strip() + + if hostname: + return hostname + + +def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: + timezones = list_timezones() + default = 'UTC' + + choice = Menu( + _('Select a timezone'), + list(timezones), + preset_values=preset, + default_option=default + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None + + +def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]: + no_audio = str(_('No audio server')) + choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] + default = 'pipewire' if desktop else no_audio + + choice = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=default).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None + + +def select_language(preset: Optional[str] = None) -> Optional[str]: + """ + Asks the user to select a language + Usually this is combined with :ref:`archinstall.list_keyboard_languages`. + + :return: The language/dictionary key of the selected language + :rtype: str + """ + kb_lang = list_keyboard_languages() + # sort alphabetically and then by length + sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) + + choice = Menu( + _('Select keyboard layout'), + sorted_kb_lang, + preset_values=preset, + sort=False + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None + + +def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: + """ + Asks the user to select a mirror or region + Usually this is combined with :ref:`archinstall.list_mirrors`. + + :return: The dictionary information about a mirror/region. + :rtype: dict + """ + if preset_values is None: + preselected = None + else: + preselected = list(preset_values.keys()) + + mirrors = list_mirrors() + + choice = Menu( + _('Select one of the regions to download packages from'), + list(mirrors.keys()), + preset_values=preselected, + multi=True, + allow_reset=True + ).run() + + match choice.type_: + case MenuSelectionType.Reset: + return {} + case MenuSelectionType.Skip: + return preset_values + case MenuSelectionType.Selection: + return {selected: mirrors[selected] for selected in choice.multi_value} + + return {} + + +def select_archinstall_language(languages: List[Language], preset: Language) -> Language: + # these are the displayed language names which can either be + # the english name of a language or, if present, the + # name of the language in its own language + options = {lang.display_name: lang for lang in languages} + + title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n' + title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n' + title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n' + + choice = Menu( + title, + list(options.keys()), + default_option=preset.display_name, + preview_size=0.5 + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return options[choice.single_value] + + raise ValueError('Language selection not handled') + + +def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: + # Additional packages (with some light weight error handling for invalid package names) + print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) + print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) + + def read_packages(already_defined: list = []) -> list: + display = ' '.join(already_defined) + input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip() + return input_packages.split() if input_packages else [] + + pre_set_packages = pre_set_packages if pre_set_packages else [] + packages = read_packages(pre_set_packages) + + if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']: + while True: + if len(packages): + # Verify packages that were given + print(_("Verifying that additional packages exist (this might take a few seconds)")) + valid, invalid = validate_package_list(packages) + + if invalid: + warn(f"Some packages could not be found in the repository: {invalid}") + packages = read_packages(valid) + continue + break + + return packages + + +def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Optional[int]: + max_downloads = 5 + print(_(f"This option enables the number of parallel downloads that can occur during installation")) + print(_(f"Enter the number of parallel downloads to be enabled.\n (Enter a value between 1 to {max_downloads})\nNote:")) + print(_(f" - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )")) + print(_(f" - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )")) + print(_(f" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )")) + + while True: + try: + input_number = int(TextInput(_("[Default value: 0] > ")).run().strip() or 0) + if input_number <= 0: + input_number = 0 + elif input_number > max_downloads: + input_number = max_downloads + break + except: + print(_(f"Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]")) + + pacman_conf_path = pathlib.Path("/etc/pacman.conf") + with pacman_conf_path.open() as f: + pacman_conf = f.read().split("\n") + + with pacman_conf_path.open("w") as fwrite: + for line in pacman_conf: + if "ParallelDownloads" in line: + fwrite.write(f"ParallelDownloads = {input_number+1}\n") if not input_number == 0 else fwrite.write("#ParallelDownloads = 0\n") + else: + fwrite.write(f"{line}\n") + + return input_number + + +def select_additional_repositories(preset: List[str]) -> List[str]: + """ + Allows the user to select additional repositories (multilib, and testing) if desired. + + :return: The string as a selected repository + :rtype: string + """ + + repositories = ["multilib", "testing"] + + choice = Menu( + _('Choose which optional additional repositories to enable'), + repositories, + sort=False, + multi=True, + preset_values=preset, + allow_reset=True + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Selection: return choice.single_value + + return [] diff --git a/archinstall/lib/interactions/locale_conf.py b/archinstall/lib/interactions/locale_conf.py new file mode 100644 index 00000000..de115202 --- /dev/null +++ b/archinstall/lib/interactions/locale_conf.py @@ -0,0 +1,43 @@ +from typing import Any, TYPE_CHECKING, Optional + +from ..locale import list_locales +from ..menu import Menu, MenuSelectionType + +if TYPE_CHECKING: + _: Any + + +def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: + locales = list_locales() + locale_lang = set([locale.split()[0] for locale in locales]) + + choice = Menu( + _('Choose which locale language to use'), + list(locale_lang), + sort=True, + preset_values=preset + ).run() + + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return preset + + return None + + +def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: + locales = list_locales() + locale_enc = set([locale.split()[1] for locale in locales]) + + choice = Menu( + _('Choose which locale encoding to use'), + list(locale_enc), + sort=True, + preset_values=preset + ).run() + + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return preset + + return None diff --git a/archinstall/lib/interactions/manage_users_conf.py b/archinstall/lib/interactions/manage_users_conf.py new file mode 100644 index 00000000..879578da --- /dev/null +++ b/archinstall/lib/interactions/manage_users_conf.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import re +from typing import Any, Dict, TYPE_CHECKING, List, Optional + +from .utils import get_password +from ..menu import Menu, ListManager +from ..models.users import User +from ..output import FormattedOutput + +if TYPE_CHECKING: + _: Any + + +class UserList(ListManager): + """ + subclass of ListManager for the managing of user accounts + """ + + def __init__(self, prompt: str, lusers: List[User]): + self._actions = [ + str(_('Add a user')), + str(_('Change password')), + str(_('Promote/Demote user')), + str(_('Delete User')) + ] + super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[User]) -> Dict[str, Any]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[User]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, user in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = user + + return display_data + + def selected_action_display(self, user: User) -> str: + return user.username + + def handle_action(self, action: str, entry: Optional[User], data: List[User]) -> List[User]: + if action == self._actions[0]: # add + new_user = self._add_user() + if new_user is not None: + # in case a user with the same username as an existing user + # was created we'll replace the existing one + data = [d for d in data if d.username != new_user.username] + data += [new_user] + elif action == self._actions[1] and entry: # change password + prompt = str(_('Password for user "{}": ').format(entry.username)) + new_password = get_password(prompt=prompt) + if new_password: + user = next(filter(lambda x: x == entry, data)) + user.password = new_password + elif action == self._actions[2] and entry: # promote/demote + user = next(filter(lambda x: x == entry, data)) + user.sudo = False if user.sudo else True + elif action == self._actions[3] and entry: # delete + data = [d for d in data if d != entry] + + return data + + def _check_for_correct_username(self, username: str) -> bool: + if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: + return True + return False + + def _add_user(self) -> Optional[User]: + prompt = '\n\n' + str(_('Enter username (leave blank to skip): ')) + + while True: + username = input(prompt).strip(' ') + if not username: + return None + if not self._check_for_correct_username(username): + error_prompt = str(_("The username you entered is invalid. Try again")) + print(error_prompt) + else: + break + + password = get_password(prompt=str(_('Password for user "{}": ').format(username))) + + if not password: + return None + + choice = Menu( + str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(), + skip=False, + default_option=Menu.yes(), + clear_screen=False, + show_search_hint=False + ).run() + + sudo = True if choice.value == Menu.yes() else False + return User(username, password, sudo) + + +def ask_for_additional_users(prompt: str = '', defined_users: List[User] = []) -> List[User]: + users = UserList(prompt, defined_users).run() + return users diff --git a/archinstall/lib/interactions/network_conf.py b/archinstall/lib/interactions/network_conf.py new file mode 100644 index 00000000..18a834a1 --- /dev/null +++ b/archinstall/lib/interactions/network_conf.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import ipaddress +from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict + +from ..menu import MenuSelectionType, TextInput +from ..models.network_configuration import NetworkConfiguration, NicType + +from ..networking import list_interfaces +from ..output import FormattedOutput, warn +from ..menu import ListManager, Menu + +if TYPE_CHECKING: + _: Any + + +class ManualNetworkConfig(ListManager): + """ + subclass of ListManager for the managing of network configurations + """ + + def __init__(self, prompt: str, ifaces: List[NetworkConfiguration]): + self._actions = [ + str(_('Add interface')), + str(_('Edit interface')), + str(_('Delete interface')) + ] + + super().__init__(prompt, ifaces, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[NetworkConfiguration]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, iface in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = iface + + return display_data + + def selected_action_display(self, iface: NetworkConfiguration) -> str: + return iface.iface if iface.iface else '' + + def handle_action(self, action: str, entry: Optional[NetworkConfiguration], data: List[NetworkConfiguration]): + if action == self._actions[0]: # add + iface_name = self._select_iface(data) + if iface_name: + iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) + iface = self._edit_iface(iface) + data += [iface] + elif entry: + if action == self._actions[1]: # edit interface + data = [d for d in data if d.iface != entry.iface] + data.append(self._edit_iface(entry)) + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] + + return data + + def _select_iface(self, data: List[NetworkConfiguration]) -> Optional[Any]: + all_ifaces = list_interfaces().values() + existing_ifaces = [d.iface for d in data] + available = set(all_ifaces) - set(existing_ifaces) + choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() + + if choice.type_ == MenuSelectionType.Skip: + return None + + return choice.value + + def _edit_iface(self, edit_iface: NetworkConfiguration): + iface_name = edit_iface.iface + modes = ['DHCP (auto detect)', 'IP (static)'] + default_mode = 'DHCP (auto detect)' + + prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) + mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() + + if mode.value == 'IP (static)': + while 1: + prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) + ip = TextInput(prompt, edit_iface.ip).run().strip() + # Implemented new check for correct IP/subnet input + try: + ipaddress.ip_interface(ip) + break + except ValueError: + warn("You need to enter a valid IP in IP-config mode") + + # Implemented new check for correct gateway IP address + gateway = None + + while 1: + gateway = TextInput( + _('Enter your gateway (router) IP address or leave blank for none: '), + edit_iface.gateway + ).run().strip() + try: + if len(gateway) > 0: + ipaddress.ip_address(gateway) + break + except ValueError: + warn("You need to enter a valid gateway (router) IP address") + + if edit_iface.dns: + display_dns = ' '.join(edit_iface.dns) + else: + display_dns = None + dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() + + dns = [] + if len(dns_input): + dns = dns_input.split(' ') + + return NetworkConfiguration(NicType.MANUAL, iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) + else: + # this will contain network iface names + return NetworkConfiguration(NicType.MANUAL, iface=iface_name) + + +def ask_to_configure_network( + preset: Union[NetworkConfiguration, List[NetworkConfiguration]] +) -> Optional[NetworkConfiguration | List[NetworkConfiguration]]: + """ + Configure the network on the newly installed system + """ + network_options = { + 'none': str(_('No network configuration')), + 'iso_config': str(_('Copy ISO network configuration to installation')), + 'network_manager': str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')), + 'manual': str(_('Manual configuration')) + } + # for this routine it's easier to set the cursor position rather than a preset value + cursor_idx = None + + if preset and not isinstance(preset, list): + if preset.type == 'iso_config': + cursor_idx = 0 + elif preset.type == 'network_manager': + cursor_idx = 1 + + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select one network interface to configure'), + list(network_options.values()), + cursor_index=cursor_idx, + sort=False, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + + if choice.value == network_options['none']: + return None + elif choice.value == network_options['iso_config']: + return NetworkConfiguration(NicType.ISO) + elif choice.value == network_options['network_manager']: + return NetworkConfiguration(NicType.NM) + elif choice.value == network_options['manual']: + preset_ifaces = preset if isinstance(preset, list) else [] + return ManualNetworkConfig('Configure interfaces', preset_ifaces).run() + + return preset diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py new file mode 100644 index 00000000..bbcb5b23 --- /dev/null +++ b/archinstall/lib/interactions/system_conf.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from typing import List, Any, Dict, TYPE_CHECKING, Optional + +from ..hardware import AVAILABLE_GFX_DRIVERS, SysInfo +from ..menu import MenuSelectionType, Menu +from ..models.bootloader import Bootloader + +if TYPE_CHECKING: + _: Any + + +def select_kernel(preset: List[str] = []) -> List[str]: + """ + Asks the user to select a kernel for system. + + :return: The string as a selected kernel + :rtype: string + """ + + kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] + default_kernel = "linux" + + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Choose which kernels to use or leave blank for default "{}"').format(default_kernel), + kernels, + sort=True, + multi=True, + preset_values=preset, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Selection: return choice.value # type: ignore + + +def ask_for_bootloader(preset: Bootloader) -> Bootloader: + # when the system only supports grub + if not SysInfo.has_uefi(): + options = [Bootloader.Grub.value] + default = Bootloader.Grub.value + else: + options = Bootloader.values() + default = Bootloader.Systemd.value + + preset_value = preset.value if preset else None + + choice = Menu( + _('Choose a bootloader'), + options, + preset_values=preset_value, + sort=False, + default_option=default + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return Bootloader(choice.value) + + return preset + + +def select_driver(options: Dict[str, Any] = {}, current_value: Optional[str] = None) -> Optional[str]: + """ + Some what convoluted function, whose job is simple. + Select a graphics driver from a pre-defined set of popular options. + + (The template xorg is for beginner users, not advanced, and should + there for appeal to the general public first and edge cases later) + """ + + if not options: + options = AVAILABLE_GFX_DRIVERS + + drivers = sorted(list(options.keys())) + + if drivers: + title = '' + if SysInfo.has_amd_graphics(): + title += str(_('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.')) + '\n' + if SysInfo.has_intel_graphics(): + title += str(_('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n')) + if SysInfo.has_nvidia_graphics(): + title += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')) + + title += str(_('\nSelect a graphics driver or leave blank to install all open-source drivers')) + + preset = current_value if current_value else None + choice = Menu(title, drivers, preset_values=preset).run() + + if choice.type_ != MenuSelectionType.Selection: + return None + + return choice.value # type: ignore + + return current_value + + +def ask_for_swap(preset: bool = True) -> bool: + if preset: + preset_val = Menu.yes() + else: + preset_val = Menu.no() + + prompt = _('Would you like to use swap on zram?') + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True + + return preset diff --git a/archinstall/lib/interactions/utils.py b/archinstall/lib/interactions/utils.py new file mode 100644 index 00000000..f6b5b2d3 --- /dev/null +++ b/archinstall/lib/interactions/utils.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import getpass +from typing import Any, Optional, TYPE_CHECKING + +from ..models import PasswordStrength +from ..output import log, error + +if TYPE_CHECKING: + _: Any + +# used for signal handler +SIG_TRIGGER = None + + +def get_password(prompt: str = '') -> Optional[str]: + if not prompt: + prompt = _("Enter a password: ") + + while password := getpass.getpass(prompt): + if len(password.strip()) <= 0: + break + + strength = PasswordStrength.strength(password) + log(f'Password strength: {strength.value}', fg=strength.color()) + + passwd_verification = getpass.getpass(prompt=_('And one more time for verification: ')) + if password != passwd_verification: + error(' * Passwords did not match * ') + continue + + return password + + return None -- cgit v1.2.3-70-g09d2 From 8a292a163ea2e643a8ac5d4cfada8a27076de630 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 15 May 2023 17:16:18 +1000 Subject: Add custom mirror support (#1816) Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 8 +- archinstall/lib/global_menu.py | 34 ++- archinstall/lib/installer.py | 15 +- archinstall/lib/interactions/__init__.py | 2 +- archinstall/lib/interactions/general_conf.py | 37 +-- archinstall/lib/mirrors.py | 442 ++++++++++++++++++--------- archinstall/scripts/guided.py | 16 +- archinstall/scripts/swiss.py | 19 +- examples/interactive_installation.py | 14 +- 9 files changed, 358 insertions(+), 229 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 992bd9fa..e6fcb267 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -225,12 +225,8 @@ def load_config(): if profile_config := arguments.get('profile_config', None): arguments['profile_config'] = profile.ProfileConfiguration.parse_arg(profile_config) - if arguments.get('mirror-region', None) is not None: - if type(arguments.get('mirror-region', None)) is dict: - arguments['mirror-region'] = arguments.get('mirror-region', None) - else: - selected_region = arguments.get('mirror-region', None) - arguments['mirror-region'] = {selected_region: mirrors.list_mirrors()[selected_region]} + if mirror_config := arguments.get('mirror_config', None): + arguments['mirror_config'] = mirrors.MirrorConfiguration.parse_args(mirror_config) if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 13595132..fc58a653 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -5,6 +5,7 @@ from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING from . import disk from .general import secret from .menu import Selector, AbstractMenu +from .mirrors import MirrorConfiguration, MirrorMenu from .models import NetworkConfiguration from .models.bootloader import Bootloader from .models.users import User @@ -26,7 +27,6 @@ from .interactions import select_kernel from .interactions import select_language from .interactions import select_locale_enc from .interactions import select_locale_lang -from .interactions import select_mirror_regions from .interactions import ask_ntp from .interactions.disk_conf import select_disk_config @@ -51,12 +51,13 @@ class GlobalMenu(AbstractMenu): _('Keyboard layout'), lambda preset: select_language(preset), default='us') - self._menu_options['mirror-region'] = \ + self._menu_options['mirror_config'] = \ Selector( - _('Mirror region'), - lambda preset: select_mirror_regions(preset), - display_func=lambda x: list(x.keys()) if x else '[]', - default={}) + _('Mirrors'), + lambda preset: self._mirror_configuration(preset), + display_func=lambda x: str(_('Defined')) if x else '', + preview_func=self._prev_mirror_config + ) self._menu_options['sys-language'] = \ Selector( _('Locale language'), @@ -354,3 +355,24 @@ class GlobalMenu(AbstractMenu): def _create_user_account(self, defined_users: List[User]) -> List[User]: users = ask_for_additional_users(defined_users=defined_users) return users + + def _mirror_configuration(self, preset: Optional[MirrorConfiguration] = None) -> Optional[MirrorConfiguration]: + data_store: Dict[str, Any] = {} + mirror_configuration = MirrorMenu(data_store, preset=preset).run() + return mirror_configuration + + def _prev_mirror_config(self) -> Optional[str]: + selector = self._menu_options['mirror_config'] + + if selector.has_selection(): + mirror_config: MirrorConfiguration = selector.current_selection # type: ignore + output = '' + if mirror_config.regions: + output += '{}: {}\n\n'.format(str(_('Mirror regions')), mirror_config.regions) + if mirror_config.custom_mirrors: + table = FormattedOutput.as_table(mirror_config.custom_mirrors) + output += '{}\n{}'.format(str(_('Custom mirrors')), table) + + return output.strip() + + return None diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 3c427ab2..30442774 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -6,7 +6,7 @@ import shutil import subprocess import time from pathlib import Path -from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable, Iterable +from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable from . import disk from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError @@ -14,7 +14,7 @@ from .general import SysCommand from .hardware import SysInfo from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .luks import Luks2 -from .mirrors import use_mirrors +from .mirrors import use_mirrors, MirrorConfiguration, add_custom_mirrors from .models.bootloader import Bootloader from .models.network_configuration import NetworkConfiguration from .models.users import User @@ -383,14 +383,17 @@ class Installer: raise RequirementError("Pacstrap failed. See /var/log/archinstall/install.log or above message for error details.") - def set_mirrors(self, mirrors: Dict[str, Iterable[str]]): + def set_mirrors(self, mirror_config: MirrorConfiguration): for plugin in plugins.values(): if hasattr(plugin, 'on_mirrors'): - if result := plugin.on_mirrors(mirrors): - mirrors = result + if result := plugin.on_mirrors(mirror_config): + mirror_config = result destination = f'{self.target}/etc/pacman.d/mirrorlist' - use_mirrors(mirrors, destination=destination) + if mirror_config.mirror_regions: + use_mirrors(mirror_config.mirror_regions, destination) + if mirror_config.custom_mirrors: + add_custom_mirrors(mirror_config.custom_mirrors) def genfstab(self, flags :str = '-pU'): info(f"Updating {self.target}/etc/fstab") diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index b5691a10..158750cc 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -11,7 +11,7 @@ from .disk_conf import ( from .general_conf import ( ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_language, - select_mirror_regions, select_archinstall_language, ask_additional_packages_to_install, + select_archinstall_language, ask_additional_packages_to_install, add_number_of_parrallel_downloads, select_additional_repositories ) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 5fcfa633..0338c61e 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -1,11 +1,10 @@ from __future__ import annotations import pathlib -from typing import List, Any, Optional, Dict, TYPE_CHECKING +from typing import List, Any, Optional, TYPE_CHECKING from ..locale import list_keyboard_languages, list_timezones from ..menu import MenuSelectionType, Menu, TextInput -from ..mirrors import list_mirrors from ..output import warn from ..packages.packages import validate_package_list from ..storage import storage @@ -96,40 +95,6 @@ def select_language(preset: Optional[str] = None) -> Optional[str]: return None -def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: - """ - Asks the user to select a mirror or region - Usually this is combined with :ref:`archinstall.list_mirrors`. - - :return: The dictionary information about a mirror/region. - :rtype: dict - """ - if preset_values is None: - preselected = None - else: - preselected = list(preset_values.keys()) - - mirrors = list_mirrors() - - choice = Menu( - _('Select one of the regions to download packages from'), - list(mirrors.keys()), - preset_values=preselected, - multi=True, - allow_reset=True - ).run() - - match choice.type_: - case MenuSelectionType.Reset: - return {} - case MenuSelectionType.Skip: - return preset_values - case MenuSelectionType.Selection: - return {selected: mirrors[selected] for selected in choice.multi_value} - - return {} - - def select_archinstall_language(languages: List[Language], preset: Language) -> Language: # these are the displayed language names which can either be # the english name of a language or, if present, the diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 62a0b081..521a8e5b 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -1,99 +1,279 @@ import pathlib -import urllib.error -import urllib.request -from typing import Union, Iterable, Dict, Any, List -from dataclasses import dataclass - -from .general import SysCommand -from .output import info, warn -from .exceptions import SysCallError +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, Any, List, Optional, TYPE_CHECKING + +from .menu import AbstractSubMenu, Selector, MenuSelectionType, Menu, ListManager, TextInput +from .networking import fetch_data_from_url +from .output import info, warn, FormattedOutput from .storage import storage +if TYPE_CHECKING: + _: Any -@dataclass -class CustomMirror: - url: str - signcheck: str - signoptions: str - name: str +class SignCheck(Enum): + Never = 'Never' + Optional = 'Optional' + Required = 'Required' -def sort_mirrorlist(raw_data :bytes, sort_order: List[str] = ['https', 'http']) -> bytes: - """ - This function can sort /etc/pacman.d/mirrorlist according to the - mirror's URL prefix. By default places HTTPS before HTTP but it also - preserves the country/rank-order. - This assumes /etc/pacman.d/mirrorlist looks like the following: +class SignOption(Enum): + TrustedOnly = 'TrustedOnly' + TrustAll = 'TrustAll' - ## Comment - Server = url - or +@dataclass +class CustomMirror: + name: str + url: str + sign_check: SignCheck + sign_option: SignOption + + def as_json(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) -> Dict[str, str]: + 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['CustomMirror']: + configs = [] + for arg in args: + configs.append( + CustomMirror( + arg['name'], + arg['url'], + SignCheck(arg['sign_check']), + SignOption(arg['sign_option']) + ) + ) + + return configs - ## Comment - #Server = url - But the Comments need to start with double-hashmarks to be distringuished - from server url definitions (commented or uncommented). - """ - comments_and_whitespaces = b"" - sort_order += ['Unknown'] - categories: Dict[str, List] = {key: [] for key in sort_order} - - for line in raw_data.split(b"\n"): - if line[0:2] in (b'##', b''): - comments_and_whitespaces += line + b'\n' - elif line[:6].lower() == b'server' or line[:7].lower() == b'#server': - opening, url = line.split(b'=', 1) - opening, url = opening.strip(), url.strip() - if (category := url.split(b'://',1)[0].decode('UTF-8')) in categories: - categories[category].append(comments_and_whitespaces) - categories[category].append(opening + b' = ' + url + b'\n') - else: - categories["Unknown"].append(comments_and_whitespaces) - categories["Unknown"].append(opening + b' = ' + url + b'\n') - - comments_and_whitespaces = b"" - - new_raw_data = b'' - for category in sort_order + ["Unknown"]: - for line in categories[category]: - new_raw_data += line - - return new_raw_data - - -def filter_mirrors_by_region(regions :str, - destination :str = '/etc/pacman.d/mirrorlist', - sort_order :List[str] = ["https", "http"], - *args :str, - **kwargs :str -) -> Union[bool, bytes]: +@dataclass +class MirrorConfiguration: + mirror_regions: Dict[str, List[str]] = field(default_factory=dict) + custom_mirrors: List[CustomMirror] = field(default_factory=list) + + @property + def regions(self) -> str: + return ', '.join(self.mirror_regions.keys()) + + def json(self) -> Dict[str, Any]: + return { + 'mirror_regions': self.mirror_regions, + 'custom_mirrors': [c.json() for c in self.custom_mirrors] + } + + @classmethod + def parse_args(cls, args: Dict[str, Any]) -> 'MirrorConfiguration': + config = MirrorConfiguration() + + if 'mirror_regions' in args: + config.mirror_regions = args['mirror_regions'] + + if 'custom_mirrors' in args: + config.custom_mirrors = CustomMirror.parse_args(args['custom_mirrors']) + + return config + + +class CustomMirrorList(ListManager): + def __init__(self, prompt: str, custom_mirrors: List[CustomMirror]): + self._actions = [ + str(_('Add a custom mirror')), + str(_('Change custom mirror')), + str(_('Delete custom mirror')) + ] + super().__init__(prompt, custom_mirrors, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[CustomMirror]) -> Dict[str, Any]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[CustomMirror]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, user in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = user + + return display_data + + def selected_action_display(self, mirror: CustomMirror) -> str: + return mirror.name + + def handle_action( + self, + action: str, + entry: Optional[CustomMirror], + data: List[CustomMirror] + ) -> List[CustomMirror]: + if action == self._actions[0]: # add + new_mirror = self._add_custom_mirror() + if new_mirror is not None: + data = [d for d in data if d.name != new_mirror.name] + data += [new_mirror] + elif action == self._actions[1] and entry: # modify mirror + new_mirror = self._add_custom_mirror(entry) + if new_mirror is not None: + data = [d for d in data if d.name != entry.name] + data += [new_mirror] + elif action == self._actions[2] and entry: # delete + data = [d for d in data if d != entry] + + return data + + def _add_custom_mirror(self, mirror: Optional[CustomMirror] = None) -> Optional[CustomMirror]: + prompt = '\n\n' + str(_('Enter name (leave blank to skip): ')) + existing_name = mirror.name if mirror else '' + + while True: + name = TextInput(prompt, existing_name).run() + if not name: + return mirror + break + + prompt = '\n' + str(_('Enter url (leave blank to skip): ')) + existing_url = mirror.url if mirror else '' + + while True: + url = TextInput(prompt, existing_url).run() + if not url: + return mirror + break + + sign_check_choice = Menu( + str(_('Select signature check option')), + [s.value for s in SignCheck], + skip=False, + clear_screen=False, + preset_values=mirror.sign_check.value if mirror else None + ).run() + + sign_option_choice = Menu( + str(_('Select signature option')), + [s.value for s in SignOption], + skip=False, + clear_screen=False, + preset_values=mirror.sign_option.value if mirror else None + ).run() + + return CustomMirror( + name, + url, + SignCheck(sign_check_choice.single_value), + SignOption(sign_option_choice.single_value) + ) + + +class MirrorMenu(AbstractSubMenu): + def __init__( + self, + data_store: Dict[str, Any], + preset: Optional[MirrorConfiguration] = None + ): + if preset: + self._preset = preset + else: + self._preset = MirrorConfiguration() + + super().__init__(data_store=data_store) + + def setup_selection_menu_options(self): + self._menu_options['mirror_regions'] = \ + Selector( + _('Mirror region'), + lambda preset: select_mirror_regions(preset), + display_func=lambda x: ', '.join(x.keys()) if x else '', + default=self._preset.mirror_regions, + enabled=True) + self._menu_options['custom_mirrors'] = \ + Selector( + _('Custom mirrors'), + lambda preset: select_custom_mirror(preset=preset), + display_func=lambda x: str(_('Defined')) if x else '', + preview_func=self._prev_custom_mirror, + default=self._preset.custom_mirrors, + enabled=True + ) + + def _prev_custom_mirror(self) -> Optional[str]: + selector = self._menu_options['custom_mirrors'] + + if selector.has_selection(): + custom_mirrors: List[CustomMirror] = selector.current_selection # type: ignore + output = FormattedOutput.as_table(custom_mirrors) + return output.strip() + + return None + + def run(self, allow_reset: bool = True) -> Optional[MirrorConfiguration]: + super().run(allow_reset=allow_reset) + + if self._data_store.get('mirror_regions', None) or self._data_store.get('custom_mirrors', None): + return MirrorConfiguration( + mirror_regions=self._data_store['mirror_regions'], + custom_mirrors=self._data_store['custom_mirrors'], + ) + + return None + + +def select_mirror_regions(preset_values: Dict[str, List[str]] = {}) -> Dict[str, List[str]]: """ - This function will change the active mirrors on the live medium by - filtering which regions are active based on `regions`. + Asks the user to select a mirror or region + Usually this is combined with :ref:`archinstall.list_mirrors`. - :param regions: A series of country codes separated by `,`. For instance `SE,US` for sweden and United States. - :type regions: str + :return: The dictionary information about a mirror/region. + :rtype: dict """ - region_list = [f'country={region}' for region in regions.split(',')] - response = urllib.request.urlopen(urllib.request.Request(f"https://archlinux.org/mirrorlist/?{'&'.join(region_list)}&protocol=https&protocol=http&ip_version=4&ip_version=6&use_mirror_status=on'", headers={'User-Agent': 'ArchInstall'})) - new_list = response.read().replace(b"#Server", b"Server") + if preset_values is None: + preselected = None + else: + preselected = list(preset_values.keys()) - if sort_order: - new_list = sort_mirrorlist(new_list, sort_order=sort_order) + mirrors = list_mirrors() - if destination: - with open(destination, "wb") as mirrorlist: - mirrorlist.write(new_list) + choice = Menu( + _('Select one of the regions to download packages from'), + list(mirrors.keys()), + preset_values=preselected, + multi=True, + allow_reset=True + ).run() - return True - else: - return new_list.decode('UTF-8') + match choice.type_: + case MenuSelectionType.Reset: + return {} + case MenuSelectionType.Skip: + return preset_values + case MenuSelectionType.Selection: + return {selected: mirrors[selected] for selected in choice.multi_value} + + return {} -def add_custom_mirrors(mirrors: List[CustomMirror]) -> bool: +def select_custom_mirror(prompt: str = '', preset: List[CustomMirror] = []): + custom_mirrors = CustomMirrorList(prompt, preset).run() + return custom_mirrors + + +def add_custom_mirrors(mirrors: List[CustomMirror]): """ This will append custom mirror definitions in pacman.conf @@ -102,99 +282,57 @@ def add_custom_mirrors(mirrors: List[CustomMirror]) -> bool: """ with open('/etc/pacman.conf', 'a') as pacman: for mirror in mirrors: - pacman.write(f"[{mirror.name}]\n") - pacman.write(f"SigLevel = {mirror.signcheck} {mirror.signoptions}\n") + pacman.write(f"\n\n[{mirror.name}]\n") + pacman.write(f"SigLevel = {mirror.sign_check.value} {mirror.sign_option.value}\n") pacman.write(f"Server = {mirror.url}\n") - return True - - -def insert_mirrors(mirrors :Dict[str, Any], *args :str, **kwargs :str) -> bool: - """ - This function will insert a given mirror-list at the top of `/etc/pacman.d/mirrorlist`. - It will not flush any other mirrors, just insert new ones. - - :param mirrors: A dictionary of `{'url' : 'country', 'url2' : 'country'}` - :type mirrors: dict - """ - original_mirrorlist = '' - with open('/etc/pacman.d/mirrorlist', 'r') as original: - original_mirrorlist = original.read() - - with open('/etc/pacman.d/mirrorlist', 'w') as new_mirrorlist: - for mirror, country in mirrors.items(): - new_mirrorlist.write(f'## {country}\n') - new_mirrorlist.write(f'Server = {mirror}\n') - new_mirrorlist.write('\n') - new_mirrorlist.write(original_mirrorlist) - - return True - def use_mirrors( - regions: Dict[str, Iterable[str]], + regions: Dict[str, List[str]], destination: str = '/etc/pacman.d/mirrorlist' ): - info(f'A new package mirror-list has been created: {destination}') - with open(destination, 'w') as mirrorlist: + with open(destination, 'w') as fp: for region, mirrors in regions.items(): for mirror in mirrors: - mirrorlist.write(f'## {region}\n') - mirrorlist.write(f'Server = {mirror}\n') + fp.write(f'## {region}\n') + fp.write(f'Server = {mirror}\n') + + info(f'A new package mirror-list has been created: {destination}') + +def _parse_mirror_list(mirrorlist: str) -> Dict[str, List[str]]: + file_content = mirrorlist.split('\n') + file_content = list(filter(lambda x: x, file_content)) # filter out empty lines + first_srv_idx = [idx for idx, line in enumerate(file_content) if 'server' in line.lower()][0] + mirrors = file_content[first_srv_idx - 1:] -def re_rank_mirrors( - top: int = 10, - src: str = '/etc/pacman.d/mirrorlist', - dst: str = '/etc/pacman.d/mirrorlist', -) -> bool: - try: - cmd = SysCommand(f"/usr/bin/rankmirrors -n {top} {src}") - except SysCallError: - return False - with open(dst, 'w') as f: - f.write(str(cmd)) - return True + mirror_list: Dict[str, List[str]] = {} + for idx in range(0, len(mirrors), 2): + region = mirrors[idx].removeprefix('## ') + url = mirrors[idx + 1].removeprefix('#').removeprefix('Server = ') + mirror_list.setdefault(region, []).append(url) -def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]: - regions: Dict[str, Dict[str, Any]] = {} + return mirror_list + + +def list_mirrors() -> Dict[str, List[str]]: + regions: Dict[str, List[str]] = {} if storage['arguments']['offline']: - with pathlib.Path('/etc/pacman.d/mirrorlist').open('rb') as fh: - mirrorlist = fh.read() + with pathlib.Path('/etc/pacman.d/mirrorlist').open('r') as fp: + mirrorlist = fp.read() else: url = "https://archlinux.org/mirrorlist/?protocol=https&protocol=http&ip_version=4&ip_version=6&use_mirror_status=on" - try: - response = urllib.request.urlopen(url) - except urllib.error.URLError as err: + mirrorlist = fetch_data_from_url(url) + except ValueError as err: warn(f'Could not fetch an active mirror-list: {err}') return regions - mirrorlist = response.read() - - if sort_order: - mirrorlist = sort_mirrorlist(mirrorlist, sort_order=sort_order) - - region = 'Unknown region' - for line in mirrorlist.split(b'\n'): - if len(line.strip()) == 0: - continue - - clean_line = line.decode('UTF-8').strip('\n').strip('\r') - - if clean_line[:3] == '## ': - region = clean_line[3:] - elif clean_line[:10] == '#Server = ': - regions.setdefault(region, {}) - - url = clean_line.lstrip('#Server = ') - regions[region][url] = True - elif clean_line.startswith('Server = '): - regions.setdefault(region, {}) - - url = clean_line.lstrip('Server = ') - regions[region][url] = True + regions = _parse_mirror_list(mirrorlist) + sorted_regions = {} + for region, urls in regions.items(): + sorted_regions[region] = sorted(urls, reverse=True) - return regions + return sorted_regions diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 1e19c9a3..1aecc1cd 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -11,7 +11,7 @@ from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.installer import Installer from archinstall.lib.menu import Menu -from archinstall.lib.mirrors import use_mirrors +from archinstall.lib.mirrors import use_mirrors, add_custom_mirrors from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.network_configuration import NetworkConfigurationHandler from archinstall.lib.networking import check_mirror_reachable @@ -45,7 +45,7 @@ def ask_user_questions(): global_menu.enable('keyboard-layout') # Set which region to download packages from during the installation - global_menu.enable('mirror-region') + global_menu.enable('mirror_config') global_menu.enable('sys-language') @@ -137,8 +137,11 @@ def perform_installation(mountpoint: Path): installation.generate_key_files() # Set mirrors used by pacstrap (outside of installation) - if archinstall.arguments.get('mirror-region', None): - use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + if mirror_config.mirror_regions: + use_mirrors(mirror_config.mirror_regions) + if mirror_config.custom_mirrors: + add_custom_mirrors(mirror_config.custom_mirrors) installation.minimal_installation( testing=enable_testing, @@ -147,9 +150,8 @@ def perform_installation(mountpoint: Path): locales=[locale] ) - if archinstall.arguments.get('mirror-region') is not None: - if archinstall.arguments.get("mirrors", None) is not None: - installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + installation.set_mirrors(mirror_config) # Set the mirrors in the installation medium if archinstall.arguments.get('swap'): installation.setup_swap('zram') diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index a49f568d..1998f073 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any, Dict import archinstall from archinstall import SysInfo, info, debug -from archinstall.lib.mirrors import use_mirrors +from archinstall.lib import mirrors from archinstall.lib import models from archinstall.lib import disk from archinstall.lib.networking import check_mirror_reachable @@ -92,7 +92,7 @@ class SwissMainMenu(GlobalMenu): match self._execution_mode: case ExecutionMode.Full | ExecutionMode.Lineal: options_list = [ - 'keyboard-layout', 'mirror-region', 'disk_config', + 'keyboard-layout', 'mirror_config', 'disk_config', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' @@ -107,7 +107,7 @@ class SwissMainMenu(GlobalMenu): mandatory_list = ['disk_config'] case ExecutionMode.Only_OS: options_list = [ - 'keyboard-layout', 'mirror-region','bootloader', 'hostname', + 'keyboard-layout', 'mirror_config','bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' ] @@ -196,8 +196,11 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): installation.generate_key_files() # Set mirrors used by pacstrap (outside of installation) - if archinstall.arguments.get('mirror-region', None): - use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + if mirror_config.mirror_regions: + mirrors.use_mirrors(mirror_config.mirror_regions) + if mirror_config.custom_mirrors: + mirrors.add_custom_mirrors(mirror_config.custom_mirrors) installation.minimal_installation( testing=enable_testing, @@ -206,10 +209,8 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): locales=[locale] ) - if archinstall.arguments.get('mirror-region') is not None: - if archinstall.arguments.get("mirrors", None) is not None: - installation.set_mirrors( - archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + installation.set_mirrors(mirror_config) # Set the mirrors in the installation medium if archinstall.arguments.get('swap'): installation.setup_swap('zram') diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 487db4dd..7c4ffed7 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -24,7 +24,7 @@ def ask_user_questions(): global_menu.enable('keyboard-layout') # Set which region to download packages from during the installation - global_menu.enable('mirror-region') + global_menu.enable('mirror_config') global_menu.enable('sys-language') @@ -116,8 +116,11 @@ def perform_installation(mountpoint: Path): installation.generate_key_files() # Set mirrors used by pacstrap (outside of installation) - if archinstall.arguments.get('mirror-region', None): - mirrors.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + if mirror_config.mirror_regions: + mirrors.use_mirrors(mirror_config.mirror_regions) + if mirror_config.custom_mirrors: + mirrors.add_custom_mirrors(mirror_config.custom_mirrors) installation.minimal_installation( testing=enable_testing, @@ -126,9 +129,8 @@ def perform_installation(mountpoint: Path): locales=[locale] ) - if archinstall.arguments.get('mirror-region') is not None: - if archinstall.arguments.get("mirrors", None) is not None: - installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + installation.set_mirrors(mirror_config) # Set the mirrors in the installation medium if archinstall.arguments.get('swap'): installation.setup_swap('zram') -- cgit v1.2.3-70-g09d2 From 06eadb31d4f0bca0c8cb95b6a9eb62ddd2d7cff2 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 5 Jun 2023 18:02:49 +1000 Subject: Move locales and cleanup menu (#1814) * Cleanup imports and unused code * Cleanup imports and unused code * Update build check * Keep deprecation exception * Simplify logging * Move locale into new sub-menu --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 3 +- archinstall/default_profiles/profile.py | 19 +--- archinstall/lib/global_menu.py | 95 +++++++++++----- archinstall/lib/hardware.py | 2 +- archinstall/lib/installer.py | 38 +++---- archinstall/lib/interactions/__init__.py | 3 +- archinstall/lib/interactions/general_conf.py | 12 +-- archinstall/lib/interactions/locale_conf.py | 43 -------- archinstall/lib/interactions/system_conf.py | 6 +- archinstall/lib/locale/__init__.py | 6 ++ archinstall/lib/locale/locale.py | 68 ++++++++++++ archinstall/lib/locale/locale_menu.py | 155 +++++++++++++++++++++++++++ archinstall/lib/menu/abstract_menu.py | 38 +------ archinstall/lib/utils/util.py | 23 +++- archinstall/scripts/guided.py | 17 ++- archinstall/scripts/swiss.py | 16 +-- examples/interactive_installation.py | 17 ++- 17 files changed, 374 insertions(+), 187 deletions(-) delete mode 100644 archinstall/lib/interactions/locale_conf.py create mode 100644 archinstall/lib/locale/__init__.py create mode 100644 archinstall/lib/locale/locale.py create mode 100644 archinstall/lib/locale/locale_menu.py (limited to 'archinstall/lib/interactions') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index e6fcb267..ce58e255 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -213,8 +213,7 @@ def load_config(): """ from .lib.models import NetworkConfiguration - arguments.setdefault('sys-language', 'en_US') - arguments.setdefault('sys-encoding', 'utf-8') + arguments['locale_config'] = locale.LocaleConfiguration.parse_arg(arguments) if (archinstall_lang := arguments.get('archinstall-language', None)) is not None: arguments['archinstall-language'] = TranslationHandler().get_language_by_name(archinstall_lang) diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index b1ad1f50..ce07c286 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -3,7 +3,7 @@ from __future__ import annotations from enum import Enum, auto from typing import List, Optional, Any, Dict, TYPE_CHECKING, TypeVar -from archinstall.lib.output import FormattedOutput +from archinstall.lib.utils.util import format_cols if TYPE_CHECKING: from archinstall.lib.installer import Installer @@ -185,17 +185,6 @@ class Profile: return None def packages_text(self) -> str: - text = str(_('Installed packages')) + ':\n' - - nr_packages = len(self.packages) - if nr_packages <= 5: - col = 1 - elif nr_packages <= 10: - col = 2 - elif nr_packages <= 15: - col = 3 - else: - col = 4 - - text += FormattedOutput.as_columns(self.packages, col) - return text + header = str(_('Installed packages')) + output = format_cols(self.packages, header) + return output diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index fc58a653..91ebc6a0 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -4,6 +4,7 @@ from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING from . import disk from .general import secret +from .locale.locale_menu import LocaleConfiguration, LocaleMenu from .menu import Selector, AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu from .models import NetworkConfiguration @@ -24,9 +25,7 @@ from .interactions import ask_to_configure_network from .interactions import get_password, ask_for_a_timezone from .interactions import select_additional_repositories from .interactions import select_kernel -from .interactions import select_language -from .interactions import select_locale_enc -from .interactions import select_locale_lang +from .utils.util import format_cols from .interactions import ask_ntp from .interactions.disk_conf import select_disk_config @@ -36,6 +35,7 @@ if TYPE_CHECKING: class GlobalMenu(AbstractMenu): def __init__(self, data_store: Dict[str, Any]): + self._defined_text = str(_('Defined')) super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3) def setup_selection_menu_options(self): @@ -46,28 +46,19 @@ class GlobalMenu(AbstractMenu): lambda x: self._select_archinstall_language(x), display_func=lambda x: x.display_name, default=self.translation_handler.get_language_by_abbr('en')) - self._menu_options['keyboard-layout'] = \ + self._menu_options['locale_config'] = \ Selector( - _('Keyboard layout'), - lambda preset: select_language(preset), - default='us') + _('Locales'), + lambda preset: self._locale_selection(preset), + preview_func=self._prev_locale, + display_func=lambda x: self._defined_text if x else '') self._menu_options['mirror_config'] = \ Selector( _('Mirrors'), lambda preset: self._mirror_configuration(preset), - display_func=lambda x: str(_('Defined')) if x else '', + display_func=lambda x: self._defined_text if x else '', preview_func=self._prev_mirror_config ) - self._menu_options['sys-language'] = \ - Selector( - _('Locale language'), - lambda preset: select_locale_lang(preset), - default='en_US') - self._menu_options['sys-encoding'] = \ - Selector( - _('Locale encoding'), - lambda preset: select_locale_enc(preset), - default='UTF-8') self._menu_options['disk_config'] = \ Selector( _('Disk configuration'), @@ -103,32 +94,32 @@ class GlobalMenu(AbstractMenu): Selector( _('Root password'), lambda preset:self._set_root_password(), - display_func=lambda x: secret(x) if x else 'None') + display_func=lambda x: secret(x) if x else '') self._menu_options['!users'] = \ Selector( _('User account'), lambda x: self._create_user_account(x), default=[], - display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else None, + display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else '', preview_func=self._prev_users) self._menu_options['profile_config'] = \ Selector( _('Profile'), lambda preset: self._select_profile(preset), - display_func=lambda x: x.profile.name if x else 'None', + display_func=lambda x: x.profile.name if x else '', preview_func=self._prev_profile ) self._menu_options['audio'] = \ Selector( _('Audio'), lambda preset: self._select_audio(preset), - display_func=lambda x: x if x else 'None', + display_func=lambda x: x if x else '', default=None ) self._menu_options['parallel downloads'] = \ Selector( _('Parallel Downloads'), - add_number_of_parrallel_downloads, + lambda preset: add_number_of_parrallel_downloads(preset), display_func=lambda x: x if x else '0', default=0 ) @@ -141,19 +132,20 @@ class GlobalMenu(AbstractMenu): self._menu_options['packages'] = \ Selector( _('Additional packages'), - # lambda x: ask_additional_packages_to_install(storage['arguments'].get('packages', None)), - ask_additional_packages_to_install, + lambda preset: ask_additional_packages_to_install(preset), + display_func=lambda x: self._defined_text if x else '', + preview_func=self._prev_additional_pkgs, default=[]) self._menu_options['additional-repositories'] = \ Selector( _('Optional repositories'), - select_additional_repositories, + lambda preset: select_additional_repositories(preset), display_func=lambda x: ', '.join(x) if x else None, default=[]) self._menu_options['nic'] = \ Selector( _('Network configuration'), - ask_to_configure_network, + lambda preset: ask_to_configure_network(preset), display_func=lambda x: self._display_network_conf(x), preview_func=self._prev_network_config, default={}) @@ -177,12 +169,37 @@ class GlobalMenu(AbstractMenu): self._menu_options['install'] = \ Selector( self._install_text(), - exec_func=lambda n,v: True if len(self._missing_configs()) == 0 else False, + exec_func=lambda n, v: True if len(self._missing_configs()) == 0 else False, preview_func=self._prev_install_missing_config, no_store=True) self._menu_options['abort'] = Selector(_('Abort'), exec_func=lambda n,v:exit(1)) + def _missing_configs(self) -> List[str]: + def check(s): + return self._menu_options.get(s).has_selection() + + def has_superuser() -> bool: + sel = self._menu_options['!users'] + if sel.current_selection: + return any([u.sudo for u in sel.current_selection]) + return False + + mandatory_fields = dict(filter(lambda x: x[1].is_mandatory(), self._menu_options.items())) + missing = set() + + for key, selector in mandatory_fields.items(): + if key in ['!root-password', '!users']: + if not check('!root-password') and not has_superuser(): + missing.add( + str(_('Either root-password or at least 1 user with sudo privileges must be specified')) + ) + elif key == 'disk_config': + if not check('disk_config'): + missing.add(self._menu_options['disk_config'].description) + + return list(missing) + def _update_install_text(self, name: str, value: str): text = self._install_text() self._menu_options['install'].update_description(text) @@ -216,6 +233,21 @@ class GlobalMenu(AbstractMenu): disk_encryption = disk.DiskEncryptionMenu(mods, data_store, preset=preset).run() return disk_encryption + def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration: + data_store: Dict[str, Any] = {} + locale_config = LocaleMenu(data_store, preset).run() + return locale_config + + def _prev_locale(self) -> Optional[str]: + selector = self._menu_options['locale_config'] + if selector.has_selection(): + config: LocaleConfiguration = selector.current_selection # type: ignore + output = '{}: {}\n'.format(str(_('Keyboard layout')), config.kb_layout) + output += '{}: {}\n'.format(str(_('Locale language')), config.sys_lang) + output += '{}: {}'.format(str(_('Locale encoding')), config.sys_enc) + return output + return None + def _prev_network_config(self) -> Optional[str]: selector = self._menu_options['nic'] if selector.has_selection(): @@ -224,6 +256,13 @@ class GlobalMenu(AbstractMenu): return FormattedOutput.as_table(ifaces) return None + def _prev_additional_pkgs(self): + selector = self._menu_options['packages'] + if selector.has_selection(): + packages: List[str] = selector.current_selection + return format_cols(packages, None) + return None + def _prev_disk_layouts(self) -> Optional[str]: selector = self._menu_options['disk_config'] disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = selector.current_selection diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 220d3d37..2b65e07c 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -3,9 +3,9 @@ from functools import cached_property from pathlib import Path from typing import Optional, Dict, List +from .exceptions import SysCallError from .general import SysCommand from .networking import list_interfaces, enrich_iface_types -from .exceptions import SysCallError from .output import debug AVAILABLE_GFX_DRIVERS = { diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 30442774..6eac85fc 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -12,6 +12,7 @@ from . import disk from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .general import SysCommand from .hardware import SysInfo +from .locale import LocaleConfiguration from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .luks import Luks2 from .mirrors import use_mirrors, MirrorConfiguration, add_custom_mirrors @@ -457,37 +458,36 @@ class Installer: with open(f'{self.target}/etc/hostname', 'w') as fh: fh.write(hostname + '\n') - def set_locale(self, locale :str, encoding :str = 'UTF-8', *args :str, **kwargs :str) -> bool: - if not len(locale): - return True - + def set_locale(self, locale_config: LocaleConfiguration): modifier = '' + lang = locale_config.sys_lang + encoding = locale_config.sys_enc # This is a temporary patch to fix #1200 - if '.' in locale: - locale, potential_encoding = locale.split('.', 1) + if '.' in locale_config.sys_lang: + lang, potential_encoding = locale_config.sys_lang.split('.', 1) # Override encoding if encoding is set to the default parameter # and the "found" encoding differs. - if encoding == 'UTF-8' and encoding != potential_encoding: + if locale_config.sys_enc == 'UTF-8' and locale_config.sys_enc != potential_encoding: encoding = potential_encoding # Make sure we extract the modifier, that way we can put it in if needed. - if '@' in locale: - locale, modifier = locale.split('@', 1) + if '@' in locale_config.sys_lang: + lang, modifier = locale_config.sys_lang.split('@', 1) modifier = f"@{modifier}" # - End patch with open(f'{self.target}/etc/locale.gen', 'a') as fh: - fh.write(f'{locale}.{encoding}{modifier} {encoding}\n') + fh.write(f'{lang}.{encoding}{modifier} {encoding}\n') + with open(f'{self.target}/etc/locale.conf', 'w') as fh: - fh.write(f'LANG={locale}.{encoding}{modifier}\n') + fh.write(f'LANG={lang}.{encoding}{modifier}\n') try: SysCommand(f'/usr/bin/arch-chroot {self.target} locale-gen') - return True - except SysCallError: - return False + except SysCallError as e: + error(f'Failed to run locale-gen on target: {e}') def set_timezone(self, zone :str, *args :str, **kwargs :str) -> bool: if not zone: @@ -620,7 +620,7 @@ class Installer: return True - def mkinitcpio(self, *flags :str) -> bool: + def mkinitcpio(self, flags: List[str], locale_config: LocaleConfiguration) -> bool: for plugin in plugins.values(): if hasattr(plugin, 'on_mkinitcpio'): # Allow plugins to override the usage of mkinitcpio altogether. @@ -630,7 +630,7 @@ class Installer: # mkinitcpio will error out if there's no vconsole. if (vconsole := Path(f"{self.target}/etc/vconsole.conf")).exists() is False: with vconsole.open('w') as fh: - fh.write(f"KEYMAP={storage['arguments']['keyboard-layout']}\n") + fh.write(f"KEYMAP={locale_config.kb_layout}\n") with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit: mkinit.write(f"MODULES=({' '.join(self.modules)})\n") @@ -658,7 +658,7 @@ class Installer: testing: bool = False, multilib: bool = False, hostname: str = 'archinstall', - locales: List[str] = ['en_US.UTF-8 UTF-8'] + locale_config: LocaleConfiguration = LocaleConfiguration.default() ): for mod in self._disk_config.device_modifications: for part in mod.partitions: @@ -734,12 +734,12 @@ class Installer: # sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{localtime} /etc/localtime') # sys_command('/usr/bin/arch-chroot /mnt hwclock --hctosys --localtime') self.set_hostname(hostname) - self.set_locale(*locales[0].split()) + self.set_locale(locale_config) # TODO: Use python functions for this SysCommand(f'/usr/bin/arch-chroot {self.target} chmod 700 /root') - self.mkinitcpio('-P') + self.mkinitcpio(['-P'], locale_config) self.helper_flags['base'] = True diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 158750cc..466cfa0b 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -1,4 +1,3 @@ -from .locale_conf import select_locale_lang, select_locale_enc from .manage_users_conf import UserList, ask_for_additional_users from .network_conf import ManualNetworkConfig, ask_to_configure_network from .utils import get_password @@ -10,7 +9,7 @@ from .disk_conf import ( ) from .general_conf import ( - ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_language, + ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_archinstall_language, ask_additional_packages_to_install, add_number_of_parrallel_downloads, select_additional_repositories ) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 0338c61e..3b78847b 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -3,7 +3,7 @@ from __future__ import annotations import pathlib from typing import List, Any, Optional, TYPE_CHECKING -from ..locale import list_keyboard_languages, list_timezones +from ..locale import list_timezones, list_keyboard_languages from ..menu import MenuSelectionType, Menu, TextInput from ..output import warn from ..packages.packages import validate_package_list @@ -119,18 +119,18 @@ def select_archinstall_language(languages: List[Language], preset: Language) -> raise ValueError('Language selection not handled') -def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: +def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]: # Additional packages (with some light weight error handling for invalid package names) print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) - def read_packages(already_defined: list = []) -> list: - display = ' '.join(already_defined) + def read_packages(p: List = []) -> list: + display = ' '.join(p) input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip() return input_packages.split() if input_packages else [] - pre_set_packages = pre_set_packages if pre_set_packages else [] - packages = read_packages(pre_set_packages) + preset = preset if preset else [] + packages = read_packages(preset) if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']: while True: diff --git a/archinstall/lib/interactions/locale_conf.py b/archinstall/lib/interactions/locale_conf.py deleted file mode 100644 index de115202..00000000 --- a/archinstall/lib/interactions/locale_conf.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Any, TYPE_CHECKING, Optional - -from ..locale import list_locales -from ..menu import Menu, MenuSelectionType - -if TYPE_CHECKING: - _: Any - - -def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: - locales = list_locales() - locale_lang = set([locale.split()[0] for locale in locales]) - - choice = Menu( - _('Choose which locale language to use'), - list(locale_lang), - sort=True, - preset_values=preset - ).run() - - match choice.type_: - case MenuSelectionType.Selection: return choice.single_value - case MenuSelectionType.Skip: return preset - - return None - - -def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: - locales = list_locales() - locale_enc = set([locale.split()[1] for locale in locales]) - - choice = Menu( - _('Choose which locale encoding to use'), - list(locale_enc), - sort=True, - preset_values=preset - ).run() - - match choice.type_: - case MenuSelectionType.Selection: return choice.single_value - case MenuSelectionType.Skip: return preset - - return None diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py index bbcb5b23..ea7e5989 100644 --- a/archinstall/lib/interactions/system_conf.py +++ b/archinstall/lib/interactions/system_conf.py @@ -29,14 +29,14 @@ def select_kernel(preset: List[str] = []) -> List[str]: sort=True, multi=True, preset_values=preset, - allow_reset=True, allow_reset_warning_msg=warning ).run() match choice.type_: case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return [] - case MenuSelectionType.Selection: return choice.value # type: ignore + case MenuSelectionType.Selection: return choice.single_value + + return [] def ask_for_bootloader(preset: Bootloader) -> Bootloader: diff --git a/archinstall/lib/locale/__init__.py b/archinstall/lib/locale/__init__.py new file mode 100644 index 00000000..6c32d6f3 --- /dev/null +++ b/archinstall/lib/locale/__init__.py @@ -0,0 +1,6 @@ +from .locale_menu import LocaleConfiguration +from .locale import ( + list_keyboard_languages, list_locales, list_x11_keyboard_languages, + verify_keyboard_layout, verify_x11_keyboard_layout, set_kb_layout, + list_timezones +) diff --git a/archinstall/lib/locale/locale.py b/archinstall/lib/locale/locale.py new file mode 100644 index 00000000..c3294e83 --- /dev/null +++ b/archinstall/lib/locale/locale.py @@ -0,0 +1,68 @@ +from typing import Iterator, List + +from ..exceptions import ServiceException, SysCallError +from ..general import SysCommand +from ..output import error + + +def list_keyboard_languages() -> Iterator[str]: + for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() + + +def list_locales() -> List[str]: + with open('/etc/locale.gen', 'r') as fp: + locales = [] + # before the list of locales begins there's an empty line with a '#' in front + # so we'll collect the localels from bottom up and halt when we're donw + entries = fp.readlines() + entries.reverse() + + for entry in entries: + text = entry.replace('#', '').strip() + if text == '': + break + locales.append(text) + + locales.reverse() + return locales + + +def list_x11_keyboard_languages() -> Iterator[str]: + for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() + + +def verify_keyboard_layout(layout :str) -> bool: + for language in list_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + + +def verify_x11_keyboard_layout(layout :str) -> bool: + for language in list_x11_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + + +def set_kb_layout(locale :str) -> bool: + if len(locale.strip()): + if not verify_keyboard_layout(locale): + error(f"Invalid keyboard locale specified: {locale}") + return False + + try: + SysCommand(f'localectl set-keymap {locale}') + except SysCallError as err: + raise ServiceException(f"Unable to set locale '{locale}' for console: {err}") + + return True + + return False + + +def list_timezones() -> Iterator[str]: + for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() diff --git a/archinstall/lib/locale/locale_menu.py b/archinstall/lib/locale/locale_menu.py new file mode 100644 index 00000000..29dd775d --- /dev/null +++ b/archinstall/lib/locale/locale_menu.py @@ -0,0 +1,155 @@ +from dataclasses import dataclass +from typing import Dict, Any, TYPE_CHECKING, Optional + +from .locale import set_kb_layout, list_keyboard_languages, list_locales +from ..menu import Selector, AbstractSubMenu, MenuSelectionType, Menu + +if TYPE_CHECKING: + _: Any + + +@dataclass +class LocaleConfiguration: + kb_layout: str + sys_lang: str + sys_enc: str + + @staticmethod + def default() -> 'LocaleConfiguration': + return LocaleConfiguration('us', 'en_US', 'UTF-8') + + def json(self) -> Dict[str, str]: + return { + 'kb_layout': self.kb_layout, + 'sys_lang': self.sys_lang, + 'sys_enc': self.sys_enc + } + + @classmethod + def _load_config(cls, config: 'LocaleConfiguration', args: Dict[str, Any]) -> 'LocaleConfiguration': + if 'sys_lang' in args: + config.sys_lang = args['sys_lang'] + if 'sys_enc' in args: + config.sys_enc = args['sys_enc'] + if 'kb_layout' in args: + config.kb_layout = args['kb_layout'] + + return config + + @classmethod + def parse_arg(cls, args: Dict[str, Any]) -> 'LocaleConfiguration': + default = cls.default() + + if 'locale_config' in args: + default = cls._load_config(default, args['locale_config']) + else: + default = cls._load_config(default, args) + + return default + + +class LocaleMenu(AbstractSubMenu): + def __init__( + self, + data_store: Dict[str, Any], + locele_conf: LocaleConfiguration + ): + self._preset = locele_conf + super().__init__(data_store=data_store) + + def setup_selection_menu_options(self): + self._menu_options['keyboard-layout'] = \ + Selector( + _('Keyboard layout'), + lambda preset: self._select_kb_layout(preset), + default='us', + enabled=True) + self._menu_options['sys-language'] = \ + Selector( + _('Locale language'), + lambda preset: select_locale_lang(preset), + default='en_US', + enabled=True) + self._menu_options['sys-encoding'] = \ + Selector( + _('Locale encoding'), + lambda preset: select_locale_enc(preset), + default='UTF-8', + enabled=True) + + def run(self, allow_reset: bool = True) -> LocaleConfiguration: + super().run(allow_reset=allow_reset) + + return LocaleConfiguration( + self._data_store['keyboard-layout'], + self._data_store['sys-language'], + self._data_store['sys-encoding'] + ) + + def _select_kb_layout(self, preset: Optional[str]) -> Optional[str]: + kb_lang = select_kb_layout(preset) + if kb_lang: + set_kb_layout(kb_lang) + return kb_lang + + +def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: + locales = list_locales() + locale_lang = set([locale.split()[0] for locale in locales]) + + choice = Menu( + _('Choose which locale language to use'), + list(locale_lang), + sort=True, + preset_values=preset + ).run() + + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return preset + + return None + + +def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: + locales = list_locales() + locale_enc = set([locale.split()[1] for locale in locales]) + + choice = Menu( + _('Choose which locale encoding to use'), + list(locale_enc), + sort=True, + preset_values=preset + ).run() + + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return preset + + return None + + +def select_kb_layout(preset: Optional[str] = None) -> Optional[str]: + """ + Asks the user to select a language + Usually this is combined with :ref:`archinstall.list_keyboard_languages`. + + :return: The language/dictionary key of the selected language + :rtype: str + """ + kb_lang = list_keyboard_languages() + # sort alphabetically and then by length + sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) + + choice = Menu( + _('Select keyboard layout'), + sorted_kb_lang, + preset_values=preset, + sort=False + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 2bd56374..eee99747 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING from .menu import Menu, MenuSelectionType -from ..locale import set_keyboard_language from ..output import error from ..translationhandler import TranslationHandler, Language @@ -130,7 +129,7 @@ class Selector: if current: padding += 5 description = str(self._description).ljust(padding, ' ') - current = str(_('set: {}').format(current)) + current = current else: description = self._description current = '' @@ -243,31 +242,6 @@ class AbstractMenu: elif selector is not None and selector.has_selection(): self._data_store[selector_name] = selector.current_selection - def _missing_configs(self) -> List[str]: - def check(s): - return self._menu_options.get(s).has_selection() - - def has_superuser() -> bool: - sel = self._menu_options['!users'] - if sel.current_selection: - return any([u.sudo for u in sel.current_selection]) - return False - - mandatory_fields = dict(filter(lambda x: x[1].is_mandatory(), self._menu_options.items())) - missing = set() - - for key, selector in mandatory_fields.items(): - if key in ['!root-password', '!users']: - if not check('!root-password') and not has_superuser(): - missing.add( - str(_('Either root-password or at least 1 user with sudo privileges must be specified')) - ) - elif key == 'disk_config': - if not check('disk_config'): - missing.add(self._menu_options['disk_config'].description) - - return list(missing) - def setup_selection_menu_options(self): """ Define the menu options. Menu options can be defined here in a subclass or done per program calling self.set_option() @@ -328,9 +302,6 @@ class AbstractMenu: cursor_pos = None while True: - # Before continuing, set the preferred keyboard layout/language in the current terminal. - # This will just help the user with the next following questions. - self._set_kb_language() enabled_menus = self._menus_to_enable() padding = self._get_menu_text_padding(list(enabled_menus.values())) @@ -425,13 +396,6 @@ class AbstractMenu: return True - def _set_kb_language(self): - """ general for ArchInstall""" - # Before continuing, set the preferred keyboard layout/language in the current terminal. - # This will just help the user with the next following questions. - if self._data_store.get('keyboard-layout', None) and len(self._data_store['keyboard-layout']): - set_keyboard_language(self._data_store['keyboard-layout']) - def _verify_selection_enabled(self, selection_name: str) -> bool: """ general """ if selection := self._menu_options.get(selection_name, None): diff --git a/archinstall/lib/utils/util.py b/archinstall/lib/utils/util.py index 34716f4a..8df75ab1 100644 --- a/archinstall/lib/utils/util.py +++ b/archinstall/lib/utils/util.py @@ -1,6 +1,7 @@ from pathlib import Path -from typing import Any, TYPE_CHECKING, Optional +from typing import Any, TYPE_CHECKING, Optional, List +from ..output import FormattedOutput from ..output import info if TYPE_CHECKING: @@ -28,3 +29,23 @@ def is_subpath(first: Path, second: Path): return True except ValueError: return False + + +def format_cols(items: List[str], header: Optional[str]) -> str: + if header: + text = f'{header}:\n' + else: + text = '' + + nr_items = len(items) + if nr_items <= 5: + col = 1 + elif nr_items <= 10: + col = 2 + elif nr_items <= 15: + col = 3 + else: + col = 4 + + text += FormattedOutput.as_columns(items, col) + return text diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 1aecc1cd..7f9b9fd6 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -5,6 +5,7 @@ from typing import Any, TYPE_CHECKING import archinstall from archinstall import info, debug from archinstall import SysInfo +from archinstall.lib import locale from archinstall.lib import disk from archinstall.lib.global_menu import GlobalMenu from archinstall.default_profiles.applications.pipewire import PipewireProfile @@ -42,14 +43,10 @@ def ask_user_questions(): global_menu.enable('archinstall-language') - global_menu.enable('keyboard-layout') - # Set which region to download packages from during the installation global_menu.enable('mirror_config') - global_menu.enable('sys-language') - - global_menu.enable('sys-encoding') + global_menu.enable('locale_config') global_menu.enable('disk_config', mandatory=True) @@ -76,7 +73,7 @@ def ask_user_questions(): global_menu.enable('audio') # Ask for preferred kernel: - global_menu.enable('kernels') + global_menu.enable('kernels', mandatory=True) global_menu.enable('packages') @@ -114,9 +111,7 @@ def perform_installation(mountpoint: Path): # Retrieve list of additional repositories and set boolean values appropriately enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) - - locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" - + locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config'] disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) with Installer( @@ -147,7 +142,7 @@ def perform_installation(mountpoint: Path): testing=enable_testing, multilib=enable_multilib, hostname=archinstall.arguments.get('hostname', 'archlinux'), - locales=[locale] + locale_config=locale_config ) if mirror_config := archinstall.arguments.get('mirror_config', None): @@ -210,7 +205,7 @@ def perform_installation(mountpoint: Path): # This step must be after profile installs to allow profiles_bck to install language pre-requisits. # After which, this step will set the language both for console and x11 if x11 was installed for instance. - installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + installation.set_keyboard_language(locale_config.kb_layout) if profile_config := archinstall.arguments.get('profile_config', None): profile_config.profile.post_install(installation) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 1998f073..375458a1 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -8,6 +8,7 @@ from archinstall import SysInfo, info, debug from archinstall.lib import mirrors from archinstall.lib import models from archinstall.lib import disk +from archinstall.lib import locale from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib import menu @@ -92,14 +93,14 @@ class SwissMainMenu(GlobalMenu): match self._execution_mode: case ExecutionMode.Full | ExecutionMode.Lineal: options_list = [ - 'keyboard-layout', 'mirror_config', 'disk_config', + 'mirror_config', 'disk_config', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' ] if archinstall.arguments.get('advanced', False): - options_list.extend(['sys-language', 'sys-encoding']) + options_list.extend(['locale_config']) mandatory_list = ['disk_config', 'bootloader', 'hostname'] case ExecutionMode.Only_HD: @@ -107,7 +108,7 @@ class SwissMainMenu(GlobalMenu): mandatory_list = ['disk_config'] case ExecutionMode.Only_OS: options_list = [ - 'keyboard-layout', 'mirror_config','bootloader', 'hostname', + 'mirror_config','bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' ] @@ -115,7 +116,7 @@ class SwissMainMenu(GlobalMenu): mandatory_list = ['hostname'] if archinstall.arguments.get('advanced', False): - options_list += ['sys-language','sys-encoding'] + options_list += ['locale_config'] case ExecutionMode.Minimal: pass case _: @@ -176,8 +177,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) - - locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" + locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config'] with Installer( mountpoint, @@ -206,7 +206,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): testing=enable_testing, multilib=enable_multilib, hostname=archinstall.arguments.get('hostname', 'archlinux'), - locales=[locale] + locale_config=locale_config ) if mirror_config := archinstall.arguments.get('mirror_config', None): @@ -263,7 +263,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): # This step must be after profile installs to allow profiles_bck to install language pre-requisits. # After which, this step will set the language both for console and x11 if x11 was installed for instance. - installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + installation.set_keyboard_language(locale_config.kb_layout) if profile_config := archinstall.arguments.get('profile_config', None): profile_config.profile.post_install(installation) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 7c4ffed7..ce1a80ec 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -10,6 +10,7 @@ from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall import disk from archinstall import menu from archinstall import models +from archinstall import locale from archinstall import info, debug if TYPE_CHECKING: @@ -21,14 +22,10 @@ def ask_user_questions(): global_menu.enable('archinstall-language') - global_menu.enable('keyboard-layout') - # Set which region to download packages from during the installation global_menu.enable('mirror_config') - global_menu.enable('sys-language') - - global_menu.enable('sys-encoding') + global_menu.enable('locale_config') global_menu.enable('disk_config', mandatory=True) @@ -55,7 +52,7 @@ def ask_user_questions(): global_menu.enable('audio') # Ask for preferred kernel: - global_menu.enable('kernels') + global_menu.enable('kernels', mandatory=True) global_menu.enable('packages') @@ -93,9 +90,7 @@ def perform_installation(mountpoint: Path): # Retrieve list of additional repositories and set boolean values appropriately enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) - - locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" - + locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config'] disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) with Installer( @@ -126,7 +121,7 @@ def perform_installation(mountpoint: Path): testing=enable_testing, multilib=enable_multilib, hostname=archinstall.arguments.get('hostname', 'archlinux'), - locales=[locale] + locale_config=locale_config ) if mirror_config := archinstall.arguments.get('mirror_config', None): @@ -189,7 +184,7 @@ def perform_installation(mountpoint: Path): # This step must be after profile installs to allow profiles_bck to install language pre-requisits. # After which, this step will set the language both for console and x11 if x11 was installed for instance. - installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + installation.set_keyboard_language(locale_config.kb_layout) if profile_config := archinstall.arguments.get('profile_config', None): profile_config.profile.post_install(installation) -- cgit v1.2.3-70-g09d2 From 91ee3575d369becdf1f0b93a259929b6662170c7 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 5 Jun 2023 18:47:14 +1000 Subject: Fix 1749 (#1840) Co-authored-by: Daniel Girtler Co-authored-by: Anton Hvornum --- archinstall/lib/interactions/general_conf.py | 10 ++-- archinstall/locales/ar/LC_MESSAGES/base.po | 23 ++++---- archinstall/locales/base.pot | 15 ++++-- archinstall/locales/cs/LC_MESSAGES/base.mo | Bin 26115 -> 25981 bytes archinstall/locales/cs/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/de/LC_MESSAGES/base.po | 73 ++++++++++++++++++++++++-- archinstall/locales/el/LC_MESSAGES/base.mo | Bin 35562 -> 35428 bytes archinstall/locales/el/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/en/LC_MESSAGES/base.po | 11 ++-- archinstall/locales/es/LC_MESSAGES/base.po | 11 ++-- archinstall/locales/fr/LC_MESSAGES/base.mo | Bin 27562 -> 27428 bytes archinstall/locales/fr/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/id/LC_MESSAGES/base.mo | Bin 26392 -> 26258 bytes archinstall/locales/id/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/it/LC_MESSAGES/base.mo | Bin 26675 -> 26541 bytes archinstall/locales/it/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/ka/LC_MESSAGES/base.mo | Bin 44957 -> 44823 bytes archinstall/locales/ka/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/ko/LC_MESSAGES/base.mo | Bin 27355 -> 27221 bytes archinstall/locales/ko/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/nl/LC_MESSAGES/base.po | 11 ++-- archinstall/locales/pl/LC_MESSAGES/base.mo | Bin 25444 -> 25126 bytes archinstall/locales/pl/LC_MESSAGES/base.po | 21 +++++--- archinstall/locales/pt/LC_MESSAGES/base.po | 11 ++-- archinstall/locales/pt_BR/LC_MESSAGES/base.mo | Bin 27157 -> 27023 bytes archinstall/locales/pt_BR/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/ru/LC_MESSAGES/base.mo | Bin 36023 -> 35889 bytes archinstall/locales/ru/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/sv/LC_MESSAGES/base.po | 11 ++-- archinstall/locales/ta/LC_MESSAGES/base.mo | Bin 47610 -> 47476 bytes archinstall/locales/ta/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/tr/LC_MESSAGES/base.po | 11 ++-- archinstall/locales/uk/LC_MESSAGES/base.mo | Bin 36158 -> 36024 bytes archinstall/locales/uk/LC_MESSAGES/base.po | 20 ++++--- archinstall/locales/ur/LC_MESSAGES/base.po | 11 ++-- archinstall/locales/zh-CN/LC_MESSAGES/base.mo | Bin 24126 -> 23992 bytes archinstall/locales/zh-CN/LC_MESSAGES/base.po | 20 ++++--- 37 files changed, 326 insertions(+), 133 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 3b78847b..ad9ee386 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -151,10 +151,10 @@ def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]: def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Optional[int]: max_downloads = 5 print(_(f"This option enables the number of parallel downloads that can occur during installation")) - print(_(f"Enter the number of parallel downloads to be enabled.\n (Enter a value between 1 to {max_downloads})\nNote:")) - print(_(f" - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )")) - print(_(f" - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )")) - print(_(f" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )")) + print(str(_("Enter the number of parallel downloads to be enabled.\n (Enter a value between 1 to {})\nNote:")).format(max_downloads)) + print(str(_(" - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )")).format(max_downloads, max_downloads, max_downloads + 1)) + print(_(" - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )")) + print(_(" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )")) while True: try: @@ -165,7 +165,7 @@ def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Opt input_number = max_downloads break except: - print(_(f"Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]")) + print(str(_("Invalid input! Try again with a valid input [1 to {}, or 0 to disable]")).format(max_downloads)) pacman_conf_path = pathlib.Path("/etc/pacman.conf") with pacman_conf_path.open() as f: diff --git a/archinstall/locales/ar/LC_MESSAGES/base.po b/archinstall/locales/ar/LC_MESSAGES/base.po index 6c37bdff..0944913a 100644 --- a/archinstall/locales/ar/LC_MESSAGES/base.po +++ b/archinstall/locales/ar/LC_MESSAGES/base.po @@ -751,14 +751,13 @@ msgstr "" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" msgstr "" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" @@ -1056,12 +1055,6 @@ msgstr "" msgid "Defined" msgstr "" -msgid "Mirrors" -msgstr "" - -msgid "Mirror regions" -msgstr "" - msgid "Save user configuration (including disk layout)" msgstr "" @@ -1078,3 +1071,15 @@ msgstr "" msgid "Saving {} configuration files to {}" msgstr "" + +msgid "Mirrors" +msgstr "" + +msgid "Mirror regions" +msgstr "" + +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "" diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot index 2ca84604..cc01f5fc 100644 --- a/archinstall/locales/base.pot +++ b/archinstall/locales/base.pot @@ -793,16 +793,15 @@ msgid "" "installation" msgstr "" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" msgid "" -" - Maximum value : {max_downloads} ( Allows {max_downloads} parallel " -"downloads, allows {max_downloads+1} downloads at a time )" +" - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads " +"at a time )" msgstr "" msgid "" @@ -1146,3 +1145,11 @@ msgstr "" msgid "Mirror regions" msgstr "" + +msgid "" +" - Maximum value : {} ( Allows {} parallel downloads, allows " +"{max_downloads+1} downloads at a time )" +msgstr "" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "" diff --git a/archinstall/locales/cs/LC_MESSAGES/base.mo b/archinstall/locales/cs/LC_MESSAGES/base.mo index a256081c..b8808152 100644 Binary files a/archinstall/locales/cs/LC_MESSAGES/base.mo and b/archinstall/locales/cs/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/cs/LC_MESSAGES/base.po b/archinstall/locales/cs/LC_MESSAGES/base.po index cf6bd3a1..20f3c257 100644 --- a/archinstall/locales/cs/LC_MESSAGES/base.po +++ b/archinstall/locales/cs/LC_MESSAGES/base.po @@ -786,18 +786,17 @@ msgstr "Nakonfigurováno {} rozhraní" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Tato možnost povolí specifikovaný počet paralelních stahování, která mohou nastat při instalaci" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "Zadejte povolený počet paralelních stahování.\n" -" (Zadejte hodnotu mezi 1 a {max_downloads})\n" +" (Zadejte hodnotu mezi 1 a {})\n" "Poznámka:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Maximální hodnota : {max_downloads} (Povolí {max_downloads} paralelních stahování, povolí {max_downloads+1} stahování naráz )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - Maximální hodnota : {} (Povolí {} paralelních stahování, povolí {} stahování naráz )" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - Minimální hodnota : 1 (Povolí 1 paralelní stahování, povolí 2 stahování naráz)" @@ -805,9 +804,9 @@ msgstr " - Minimální hodnota : 1 (Povolí 1 paralelní stahování, povolí msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - Zakázáno/Výchozí : 0 (Zakáže paralelní stahování, povolí pouze 1 stahování naráz)" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "Neplatný vstup! Zkuste to, prosím, znovu s platným vstupem [1 až {max_downloads}, nebo 0 pro vypnutí]" +msgstr "Neplatný vstup! Zkuste to, prosím, znovu s platným vstupem [1 až {}, nebo 0 pro vypnutí]" msgid "Parallel Downloads" msgstr "Paralelní stahování" @@ -1163,3 +1162,10 @@ msgstr "Oblast zrcadla" #, fuzzy msgid "Mirror regions" msgstr "Oblast zrcadla" + +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - Maximální hodnota : {} (Povolí {} paralelních stahování, povolí {} stahování naráz )" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "Neplatný vstup! Zkuste to, prosím, znovu s platným vstupem [1 až {}, nebo 0 pro vypnutí]" diff --git a/archinstall/locales/de/LC_MESSAGES/base.po b/archinstall/locales/de/LC_MESSAGES/base.po index b20d6e90..6dfb0801 100644 --- a/archinstall/locales/de/LC_MESSAGES/base.po +++ b/archinstall/locales/de/LC_MESSAGES/base.po @@ -793,18 +793,17 @@ msgstr "{} Schnittstellen konfiguriert" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Diese Option setzt die Nummer an parallelen Downloads, die während der Installtion durchgeführt werden" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "Geben Sie die Nummer an parallelen Downloads an.\n" " (Wert zwischen 1 und {max_downloads})\n" "Achtung:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Maximalwert :{max_downloads} (Erlaubt {max_downloads} parallele Downloads, erlaubt {max_downloads+1} Downloads gleichzeitig)" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - Maximalwert :{} (Erlaubt {} parallele Downloads, erlaubt {} Downloads gleichzeitig)" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr "- Minimalwert : 1 (Erlaubt einen parallelen Download, erlaubt zwei Downloads gleichzeitig)" @@ -1188,6 +1187,72 @@ msgstr "" msgid "Saving {} configuration files to {}" msgstr "Konfiguration speichern" +#, fuzzy +msgid "Add a custom mirror" +msgstr "Benutzerkonto hinzufügen" + +msgid "Change custom mirror" +msgstr "" + +msgid "Delete custom mirror" +msgstr "" + +#, fuzzy +msgid "Enter name (leave blank to skip): " +msgstr "Geben sie einen weiteren Benutzernamen an der angelegt werden soll (leer lassen um zu Überspringen): " + +#, fuzzy +msgid "Enter url (leave blank to skip): " +msgstr "Geben sie einen weiteren Benutzernamen an der angelegt werden soll (leer lassen um zu Überspringen): " + +#, fuzzy +msgid "Select signature check option" +msgstr "Laufwerke-layout auswählen" + +#, fuzzy +msgid "Select signature option" +msgstr "Laufwerke-layout auswählen" + +msgid "Custom mirrors" +msgstr "" + +msgid "Defined" +msgstr "" + +#, fuzzy +msgid "Save user configuration (including disk layout)" +msgstr "Benutzerkonfiguration speichern" + +#, fuzzy +msgid "" +"Enter a directory for the configuration(s) to be saved (tab completion enabled)\n" +"Save directory: " +msgstr "Geben sie eine Ordner an wo die Konfigurationen gespeichert werden sollen: " + +msgid "" +"Do you want to save {} configuration file(s) in the following location?\n" +"\n" +"{}" +msgstr "" + +#, fuzzy +msgid "Saving {} configuration files to {}" +msgstr "Konfiguration speichern" + +#, fuzzy +msgid "Mirrors" +msgstr "Mirror-region" + +#, fuzzy +msgid "Mirror regions" +msgstr "Mirror-region" + +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "" + #~ msgid "Add :" #~ msgstr "Hinzufügen :" diff --git a/archinstall/locales/el/LC_MESSAGES/base.mo b/archinstall/locales/el/LC_MESSAGES/base.mo index 103eaf0c..9a8c99af 100644 Binary files a/archinstall/locales/el/LC_MESSAGES/base.mo and b/archinstall/locales/el/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/el/LC_MESSAGES/base.po b/archinstall/locales/el/LC_MESSAGES/base.po index 4a33dd5e..4b4db260 100644 --- a/archinstall/locales/el/LC_MESSAGES/base.po +++ b/archinstall/locales/el/LC_MESSAGES/base.po @@ -793,18 +793,17 @@ msgstr "Διαμορφωμένες {} διεπαφές" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Αυτή η επιλογή θέτει τον αριθμό των παράλληλων λήψεων που μπορούν να συμβούν κατά την εγκατάσταση" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "Εισάγετε τον αριθμό των παράλληλων λήψεων προς ενεργοποίηση.\n" -" (Εισάγετε μία τιμή από 1 μέχρι {max_downloads})\n" +" (Εισάγετε μία τιμή από 1 μέχρι {})\n" "Σημείωση:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Μέγιστη τιμή : {max_downloads} ( Επιτρέπει {max_downloads} παράλληλες λήψεις, επιτρέπει {max_downloads+1} λήψεις σε μία στιγμή )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - Μέγιστη τιμή : {} ( Επιτρέπει {} παράλληλες λήψεις, επιτρέπει {} λήψεις σε μία στιγμή )" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - Ελάχιστη τιμή : 1 ( Επιτρέπει 1 παράλληλη λήψη, επιτρέπει 2 λήψεις σε μία στιγμή )" @@ -812,9 +811,9 @@ msgstr " - Ελάχιστη τιμή : 1 ( Επιτρέπει 1 παράλλη msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - Απενεργοποίηση/Προκαθορισμένο : 0 ( Απενεργοποιεί τις παράλληλες λήψεις, επιτρέπει μόνο 1 λήψη σε μία στιγμή )" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "Μη έγκυρη είσοδος! Προσπαθήστε ξανά με μία έγκυρη είσοδο [1 μέχρι {max_downloads}, ή 0 για απενεργοποίηση]" +msgstr "Μη έγκυρη είσοδος! Προσπαθήστε ξανά με μία έγκυρη είσοδο [1 μέχρι {}, ή 0 για απενεργοποίηση]" msgid "Parallel Downloads" msgstr "Παράλληλες Λήψεις" @@ -1170,3 +1169,10 @@ msgstr "Περιοχή mirror" #, fuzzy msgid "Mirror regions" msgstr "Περιοχή mirror" + +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - Μέγιστη τιμή : {} ( Επιτρέπει {} παράλληλες λήψεις, επιτρέπει {} λήψεις σε μία στιγμή )" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "Μη έγκυρη είσοδος! Προσπαθήστε ξανά με μία έγκυρη είσοδο [1 μέχρι {}, ή 0 για απενεργοποίηση]" diff --git a/archinstall/locales/en/LC_MESSAGES/base.po b/archinstall/locales/en/LC_MESSAGES/base.po index f08c5ffc..5a38a4db 100644 --- a/archinstall/locales/en/LC_MESSAGES/base.po +++ b/archinstall/locales/en/LC_MESSAGES/base.po @@ -747,14 +747,13 @@ msgstr "" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" msgstr "" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" @@ -1067,3 +1066,9 @@ msgstr "" msgid "Mirror regions" msgstr "" + +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "" diff --git a/archinstall/locales/es/LC_MESSAGES/base.po b/archinstall/locales/es/LC_MESSAGES/base.po index 6ba5f7b6..22ab693c 100644 --- a/archinstall/locales/es/LC_MESSAGES/base.po +++ b/archinstall/locales/es/LC_MESSAGES/base.po @@ -794,14 +794,13 @@ msgstr "" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" msgstr "" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" @@ -1170,6 +1169,12 @@ msgstr "Región del servidor" msgid "Mirror regions" msgstr "Región del servidor" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "" + #~ msgid "Add :" #~ msgstr "Añadir :" diff --git a/archinstall/locales/fr/LC_MESSAGES/base.mo b/archinstall/locales/fr/LC_MESSAGES/base.mo index 22e004e0..81e6ce00 100644 Binary files a/archinstall/locales/fr/LC_MESSAGES/base.mo and b/archinstall/locales/fr/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/fr/LC_MESSAGES/base.po b/archinstall/locales/fr/LC_MESSAGES/base.po index 15aea6ac..ec5fc68e 100644 --- a/archinstall/locales/fr/LC_MESSAGES/base.po +++ b/archinstall/locales/fr/LC_MESSAGES/base.po @@ -793,18 +793,17 @@ msgstr "Interfaces {} configurées" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Cette option active le nombre de téléchargements parallèles qui peuvent se produire pendant l'installation" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "Saisir le nombre de téléchargements parallèles à activer.\n" -" (Entrer une valeur comprise entre 1 et {max_downloads})\n" +" (Entrer une valeur comprise entre 1 et {})\n" "Note :" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Valeur maximale : {max_downloads} (Autorise {max_downloads} téléchargements parallèles, autorise {max_downloads+1} téléchargements à la fois)" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - Valeur maximale : {} (Autorise {} téléchargements parallèles, autorise {} téléchargements à la fois)" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - Valeur minimale : 1 (Autorise 1 téléchargement parallèle, autorise 2 téléchargements à la fois)" @@ -812,9 +811,9 @@ msgstr " - Valeur minimale : 1 (Autorise 1 téléchargement parallèle, autorise msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - Désactiver/Défaut : 0 (Désactive le téléchargement parallèle, n'autorise qu'un seul téléchargement à la fois)" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "Entrée invalide ! Réessayer avec une entrée valide [1 pour {max_downloads}, ou 0 pour désactiver]" +msgstr "Entrée invalide ! Réessayer avec une entrée valide [1 pour {}, ou 0 pour désactiver]" msgid "Parallel Downloads" msgstr "Téléchargements parallèles" @@ -1171,6 +1170,13 @@ msgstr "Région miroir" msgid "Mirror regions" msgstr "Région miroir" +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - Valeur maximale : {} (Autorise {} téléchargements parallèles, autorise {} téléchargements à la fois)" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "Entrée invalide ! Réessayer avec une entrée valide [1 pour {}, ou 0 pour désactiver]" + #, python-brace-format #~ msgid "Edit {origkey} :" #~ msgstr "Modifier {origkey} :" diff --git a/archinstall/locales/id/LC_MESSAGES/base.mo b/archinstall/locales/id/LC_MESSAGES/base.mo index b81fe108..db3649bd 100644 Binary files a/archinstall/locales/id/LC_MESSAGES/base.mo and b/archinstall/locales/id/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/id/LC_MESSAGES/base.po b/archinstall/locales/id/LC_MESSAGES/base.po index 01986796..f2ede55f 100644 --- a/archinstall/locales/id/LC_MESSAGES/base.po +++ b/archinstall/locales/id/LC_MESSAGES/base.po @@ -793,18 +793,17 @@ msgstr "Interface {} dikonfigurasi" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Opsi ini memungkinkan jumlah unduhan paralel yang dapat terjadi selama instalasi" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "Masukkan jumlah unduhan paralel yang akan diaktifkan.\n" -" (Masukkan nilai antara 1 hingga {max_downloads})\n" +" (Masukkan nilai antara 1 hingga {})\n" "Catatan:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Nilai maksimum : {max_downloads} ( Memungkinkan {max_downloads} unduhan paralel, memungkinkan {max_downloads+1} unduhan sekaligus)" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - Nilai maksimum : {} ( Memungkinkan {} unduhan paralel, memungkinkan {} unduhan sekaligus)" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - Nilai minimum : 1 (Mengizinkan 1 unduhan paralel, memungkinkan 2 unduhan sekaligus)" @@ -812,9 +811,9 @@ msgstr " - Nilai minimum : 1 (Mengizinkan 1 unduhan paralel, memungkinkan 2 un msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - Nonaktifkan/Default: 0 (Menonaktifkan pengunduhan paralel, hanya mengizinkan 1 unduhan pada satu waktu)" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "Input tidak valid! Coba lagi dengan input yang valid [1 untuk {max_downloads}, atau 0 untuk menonaktifkan]" +msgstr "Input tidak valid! Coba lagi dengan input yang valid [1 untuk {}, atau 0 untuk menonaktifkan]" msgid "Parallel Downloads" msgstr "Unduhan Paralel" @@ -1169,3 +1168,10 @@ msgstr "Wilayah mirror" #, fuzzy msgid "Mirror regions" msgstr "Wilayah mirror" + +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - Nilai maksimum : {} ( Memungkinkan {} unduhan paralel, memungkinkan {} unduhan sekaligus)" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "Input tidak valid! Coba lagi dengan input yang valid [1 untuk {}, atau 0 untuk menonaktifkan]" diff --git a/archinstall/locales/it/LC_MESSAGES/base.mo b/archinstall/locales/it/LC_MESSAGES/base.mo index f199746c..e63c377e 100644 Binary files a/archinstall/locales/it/LC_MESSAGES/base.mo and b/archinstall/locales/it/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/it/LC_MESSAGES/base.po b/archinstall/locales/it/LC_MESSAGES/base.po index c856a286..703f0000 100644 --- a/archinstall/locales/it/LC_MESSAGES/base.po +++ b/archinstall/locales/it/LC_MESSAGES/base.po @@ -793,18 +793,17 @@ msgstr "Configurate {} interfacce" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Questa opzione consente di impostare il numero di download paralleli che possono avvenire durante l'installazione" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "Inserisci il numero di download paralleli da abilitare.\n" -" (Inserisci un valore compreso tra 1 e {max_downloads})\n" +" (Inserisci un valore compreso tra 1 e {})\n" "Nota:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Valore massimo : {max_downloads} ( Consente {max_downloads} download parallelo, consente {max_downloads+1} download alla volta )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - Valore massimo : {} ( Consente {} download parallelo, consente {} download alla volta )" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - Valore minimo : 1 ( Consente 1 download parallelo, consente 2 download alla volta )" @@ -812,9 +811,9 @@ msgstr " - Valore minimo : 1 ( Consente 1 download parallelo, consente 2 downl msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - Disabilita/Predefinito : 0 ( Disabilita il download parallelo, consente solo 1 download alla volta )" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "Input non valido! Riprova con un input valido [da 1 a {max_downloads}, o 0 per disabilitare]." +msgstr "Input non valido! Riprova con un input valido [da 1 a {}, o 0 per disabilitare]." msgid "Parallel Downloads" msgstr "Download paralleli" @@ -1169,3 +1168,10 @@ msgstr "Regione dei mirror" #, fuzzy msgid "Mirror regions" msgstr "Regione dei mirror" + +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - Valore massimo : {} ( Consente {} download parallelo, consente {} download alla volta )" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "Input non valido! Riprova con un input valido [da 1 a {}, o 0 per disabilitare]." diff --git a/archinstall/locales/ka/LC_MESSAGES/base.mo b/archinstall/locales/ka/LC_MESSAGES/base.mo index b95a6e0e..31e14aa2 100644 Binary files a/archinstall/locales/ka/LC_MESSAGES/base.mo and b/archinstall/locales/ka/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/ka/LC_MESSAGES/base.po b/archinstall/locales/ka/LC_MESSAGES/base.po index 029e4256..6e4823eb 100644 --- a/archinstall/locales/ka/LC_MESSAGES/base.po +++ b/archinstall/locales/ka/LC_MESSAGES/base.po @@ -794,18 +794,17 @@ msgstr "მორგებულია {} ინტერფეისი" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "ეს პარამეტრი დაყენებისას მითითებული რაოდენობის პარალელურ გადმოწერას დაუშვებს" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "შეიყვანეთ დასაშვები პარალელური გადმოწერების რაოდენობა.\n" -" (შეიყვანეთ მნიშვნელობა 1-დან {max_downloads}-მდე)\n" +" (შეიყვანეთ მნიშვნელობა 1-დან {}-მდე)\n" "დაიმახსოვრეთ:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - მინიმალური მნიშვნელობა : {max_downloads} ( დაუშვებს {max_downloads} პარალელურ გადმოწერას, დაუშვებს {max_downloads+1} ერთდროულ გადმოწერას )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - მინიმალური მნიშვნელობა : {} ( დაუშვებს {} პარალელურ გადმოწერას, დაუშვებს {} ერთდროულ გადმოწერას )" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - მინიმალური მნიშვნელობა : 1 ( დაუშვებს 1 პარალელურ გადმოწერას, დაუშვებს 2 ერთდროულ გადმოწერას )" @@ -813,9 +812,9 @@ msgstr " - მინიმალური მნიშვნელობა : msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - გამორთვა/ნაგულისხმები : 0 ( პარალელური გადმოწერების გათიშვა. დროის ერთ მომენტში მხოლოდ ერთი გადმოწერა მოხდება )" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "შეყვანილი რიცხვი არასწორია! თავიდან სცადეთ [1-დან {max_downloads}-მდე, ან 0, გასათიშად]" +msgstr "შეყვანილი რიცხვი არასწორია! თავიდან სცადეთ [1-დან {}-მდე, ან 0, გასათიშად]" msgid "Parallel Downloads" msgstr "პარალელური გადმოწერები" @@ -1168,3 +1167,10 @@ msgstr "სარკის რეგიონი" #, fuzzy msgid "Mirror regions" msgstr "სარკის რეგიონი" + +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - მინიმალური მნიშვნელობა : {} ( დაუშვებს {} პარალელურ გადმოწერას, დაუშვებს {} ერთდროულ გადმოწერას )" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "შეყვანილი რიცხვი არასწორია! თავიდან სცადეთ [1-დან {}-მდე, ან 0, გასათიშად]" diff --git a/archinstall/locales/ko/LC_MESSAGES/base.mo b/archinstall/locales/ko/LC_MESSAGES/base.mo index 4c89f1f8..ddec6151 100644 Binary files a/archinstall/locales/ko/LC_MESSAGES/base.mo and b/archinstall/locales/ko/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/ko/LC_MESSAGES/base.po b/archinstall/locales/ko/LC_MESSAGES/base.po index 7d30a785..de46b698 100644 --- a/archinstall/locales/ko/LC_MESSAGES/base.po +++ b/archinstall/locales/ko/LC_MESSAGES/base.po @@ -794,18 +794,17 @@ msgstr "구성된 {} 인터페이스" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "이 옵션은 설치 중에 발생할 수 있는 병렬 다운로드 수를 활성화합니다" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "활성화할 병렬 다운로드 수를 입력하세요.\n" -" (1 부터 {max_downloads} 까지의 숫자중 하나를 입력하세요)\n" +" (1 부터 {} 까지의 숫자중 하나를 입력하세요)\n" "메모:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - 최댓값 : {max_downloads} ( {max_downloads} 개의 병렬 다운로드 허용, 한 번에 {max_downloads+1} 개의 다운로드 허용 )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - 최댓값 : {} ( {} 개의 병렬 다운로드 허용, 한 번에 {} 개의 다운로드 허용 )" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - 최솟값 : 1 ( 1 개의 병렬 다운로드 허용, 한 번에 2 개의 다운로드 허용 )" @@ -813,9 +812,9 @@ msgstr " - 최솟값 : 1 ( 1 개의 병렬 다운로드 허용, 한 번에 2 msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - 비활성화/기본 : - ( 병렬 다운로드 비활성화, 한 번에 1 개의 다운로드만 허용 )" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "잘못된 값입니다! 유효한 값으로 다시 시도해주세요 [1 부터 {max_downloads} 까지, 비활성화 하려면 0]" +msgstr "잘못된 값입니다! 유효한 값으로 다시 시도해주세요 [1 부터 {} 까지, 비활성화 하려면 0]" msgid "Parallel Downloads" msgstr "병렬 다운로드" @@ -1170,3 +1169,10 @@ msgstr "미러 위치" #, fuzzy msgid "Mirror regions" msgstr "미러 위치" + +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - 최댓값 : {} ( {} 개의 병렬 다운로드 허용, 한 번에 {} 개의 다운로드 허용 )" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "잘못된 값입니다! 유효한 값으로 다시 시도해주세요 [1 부터 {} 까지, 비활성화 하려면 0]" diff --git a/archinstall/locales/nl/LC_MESSAGES/base.po b/archinstall/locales/nl/LC_MESSAGES/base.po index e22a6d0f..6dfeed53 100644 --- a/archinstall/locales/nl/LC_MESSAGES/base.po +++ b/archinstall/locales/nl/LC_MESSAGES/base.po @@ -823,14 +823,13 @@ msgstr "" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" msgstr "" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" @@ -1195,6 +1194,12 @@ msgstr "Spiegelserverregio" msgid "Mirror regions" msgstr "Spiegelserverregio" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "" + #~ msgid "Add :" #~ msgstr "Toevoegen:" diff --git a/archinstall/locales/pl/LC_MESSAGES/base.mo b/archinstall/locales/pl/LC_MESSAGES/base.mo index 7ca4f9b0..0904a1a1 100644 Binary files a/archinstall/locales/pl/LC_MESSAGES/base.mo and b/archinstall/locales/pl/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/pl/LC_MESSAGES/base.po b/archinstall/locales/pl/LC_MESSAGES/base.po index 8c3ce4f5..fa53c7f0 100644 --- a/archinstall/locales/pl/LC_MESSAGES/base.po +++ b/archinstall/locales/pl/LC_MESSAGES/base.po @@ -794,18 +794,19 @@ msgstr "Skonfigurowano {} interfejsów" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Ta opcja pozwala określić maksymalną liczbę pobieranych plików podczas instalacji" -#, fuzzy, python-brace-format +#, fuzzy msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "Wprowadź maksymalną liczbę plików pobieranych jednocześnie.\n" -" (Liczba z zakresu od 1 do {max_downloads})\n" +" (Liczba z zakresu od 1 do {})\n" "Zauważ:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Maksymalna wartość : {max_downloads} ( Zwiększa liczbę zadań o {max_downloads}, co pozwala na pobieranie {max_downloads+1} plików jednocześnie )" +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - Maksymalna wartość : {} ( Zwiększa liczbę zadań o {}, co pozwala na pobieranie {} plików jednocześnie )" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - Minimalna wartość : 1 ( Zwiększa liczbę zadań o 1, co pozwala na pobieranie 2 plików jednocześnie )" @@ -815,7 +816,7 @@ msgstr " - Wyłącz/domyślnie : 0 ( Wyłącza pobieranie równoległe, więc ty #, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "Nieprawidłowa wartość! Spróbuj wprowadzić wartość od 1 do {max_downloads}, lub 0 aby wyłączyć." +msgstr "Nieprawidłowa wartość! Spróbuj wprowadzić wartość od 1 do {}, lub 0 aby wyłączyć." msgid "Parallel Downloads" msgstr "Pobieranie równoległe" @@ -1181,6 +1182,14 @@ msgstr "Region lustra" msgid "Mirror regions" msgstr "Region lustra" +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - Maksymalna wartość : {} ( Zwiększa liczbę zadań o {}, co pozwala na pobieranie {} plików jednocześnie )" + +#, fuzzy +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "Nieprawidłowa wartość! Spróbuj wprowadzić wartość od 1 do {}, lub 0 aby wyłączyć." + #~ msgid "Add :" #~ msgstr "Dodaj :" diff --git a/archinstall/locales/pt/LC_MESSAGES/base.po b/archinstall/locales/pt/LC_MESSAGES/base.po index 35c3ca73..7f0d5eba 100644 --- a/archinstall/locales/pt/LC_MESSAGES/base.po +++ b/archinstall/locales/pt/LC_MESSAGES/base.po @@ -843,14 +843,13 @@ msgstr "" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" msgstr "" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" @@ -1217,6 +1216,12 @@ msgstr "Região do mirror" msgid "Mirror regions" msgstr "Região do mirror" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "" + #~ msgid "Add :" #~ msgstr "Adicionar :" diff --git a/archinstall/locales/pt_BR/LC_MESSAGES/base.mo b/archinstall/locales/pt_BR/LC_MESSAGES/base.mo index 4dd57dba..51580dbb 100644 Binary files a/archinstall/locales/pt_BR/LC_MESSAGES/base.mo and b/archinstall/locales/pt_BR/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/pt_BR/LC_MESSAGES/base.po b/archinstall/locales/pt_BR/LC_MESSAGES/base.po index 93b33002..c36250f5 100644 --- a/archinstall/locales/pt_BR/LC_MESSAGES/base.po +++ b/archinstall/locales/pt_BR/LC_MESSAGES/base.po @@ -797,18 +797,17 @@ msgstr "{} interfaces configuradas" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Esta opção habilita o número de downloads paralelos que podem ocorrer durante a instalação" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "Insira o número de downloads paralelos para serem habilitados.\n" -" (Insira um valor entre 1 e {max_downloads})\n" +" (Insira um valor entre 1 e {})\n" "Observação:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Valor máximo : {max_downloads} ( Permite {max_donwloads} downloads paralelos, permite {max_donwloads+1} downloads por vez )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - Valor máximo : {} ( Permite {} downloads paralelos, permite {} downloads por vez )" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - Valor minimo : 1 ( Permite 1 download paralelo, permite 2 downloads por vez )" @@ -816,9 +815,9 @@ msgstr " - Valor minimo : 1 ( Permite 1 download paralelo, permite 2 downloads msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - Desativar/Padrão : 0 ( Desativa os downloads paralelos, permite apenas 1 download por vez )" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "Entrada inválida! Tente novamente com uma entrada válida [1 para {max_downloads}, ou 0 para desativar]" +msgstr "Entrada inválida! Tente novamente com uma entrada válida [1 para {}, ou 0 para desativar]" msgid "Parallel Downloads" msgstr "Downloads Paralelos" @@ -1171,3 +1170,10 @@ msgstr "Região do mirror" #, fuzzy msgid "Mirror regions" msgstr "Região do mirror" + +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - Valor máximo : {} ( Permite {} downloads paralelos, permite {} downloads por vez )" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "Entrada inválida! Tente novamente com uma entrada válida [1 para {}, ou 0 para desativar]" diff --git a/archinstall/locales/ru/LC_MESSAGES/base.mo b/archinstall/locales/ru/LC_MESSAGES/base.mo index 1da3a370..4480d852 100644 Binary files a/archinstall/locales/ru/LC_MESSAGES/base.mo and b/archinstall/locales/ru/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/ru/LC_MESSAGES/base.po b/archinstall/locales/ru/LC_MESSAGES/base.po index a0263aa3..90d78f2e 100644 --- a/archinstall/locales/ru/LC_MESSAGES/base.po +++ b/archinstall/locales/ru/LC_MESSAGES/base.po @@ -794,18 +794,17 @@ msgstr "Настроено интерфейсов: {}" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Этот параметр определяет количество параллельных загрузок, которые могут происходить во время установки" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "Введите количество параллельных загрузок, которые будут включены.\n" -" (Введите значение от 1 до {max_downloads})\n" +" (Введите значение от 1 до {})\n" "Примечание:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Максимальное значение: {max_downloads} ( Позволяет {max_downloads} параллельных загрузок, позволяет {max_downloads+1} загрузок одновременно )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - Максимальное значение: {} ( Позволяет {} параллельных загрузок, позволяет {} загрузок одновременно )" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - Минимальное значение: 1 ( Позволяет 1 параллельную загрузку, позволяет 2 загрузки одновременно )" @@ -813,9 +812,9 @@ msgstr " - Минимальное значение: 1 ( Позволяет 1 п msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - Отключить/по умолчанию: 0 ( Отключает параллельную загрузку, позволяет только 1 загрузку за один раз )" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "Неверный ввод! Повторите попытку с правильным вводом [1 - {max_downloads}, или 0 - отключить]" +msgstr "Неверный ввод! Повторите попытку с правильным вводом [1 - {}, или 0 - отключить]" msgid "Parallel Downloads" msgstr "Параллельные загрузки" @@ -1171,6 +1170,13 @@ msgstr "Регион зеркала" msgid "Mirror regions" msgstr "Регион зеркала" +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - Максимальное значение: {} ( Позволяет {} параллельных загрузок, позволяет {} загрузок одновременно )" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "Неверный ввод! Повторите попытку с правильным вводом [1 - {}, или 0 - отключить]" + #, python-brace-format #~ msgid "Edit {origkey} :" #~ msgstr "Редактировать {origkey}:" diff --git a/archinstall/locales/sv/LC_MESSAGES/base.po b/archinstall/locales/sv/LC_MESSAGES/base.po index ea81ad80..ebb7a275 100644 --- a/archinstall/locales/sv/LC_MESSAGES/base.po +++ b/archinstall/locales/sv/LC_MESSAGES/base.po @@ -804,14 +804,13 @@ msgstr "" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" msgstr "" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" @@ -1178,3 +1177,9 @@ msgstr "Region för paketsynk" #, fuzzy msgid "Mirror regions" msgstr "Region för paketsynk" + +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "" diff --git a/archinstall/locales/ta/LC_MESSAGES/base.mo b/archinstall/locales/ta/LC_MESSAGES/base.mo index 3f175509..fc2b70e6 100644 Binary files a/archinstall/locales/ta/LC_MESSAGES/base.mo and b/archinstall/locales/ta/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/ta/LC_MESSAGES/base.po b/archinstall/locales/ta/LC_MESSAGES/base.po index 44aebb19..14ed4d98 100644 --- a/archinstall/locales/ta/LC_MESSAGES/base.po +++ b/archinstall/locales/ta/LC_MESSAGES/base.po @@ -793,18 +793,17 @@ msgstr "கட்டமைக்கப்பட்ட {} இடைமுகங msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "இந்த விருப்பம் நிறுவலின் போது நிகழக்கூடிய இணையான பதிவிறக்கங்களின் எண்ணிக்கையை செயல்படுத்துகிறது" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "இயக்கப்பட வேண்டிய இணையான பதிவிறக்கங்களின் எண்ணிக்கையை உள்ளிடவும்.\n" -" (1 முதல் {max_downloads} வரையிலான மதிப்பை உள்ளிடவும்)\n" +" (1 முதல் {} வரையிலான மதிப்பை உள்ளிடவும்)\n" "குறிப்பு:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - அதிகபட்ச மதிப்பு : {max_downloads} ( {max_downloads} இணையான பதிவிறக்கங்களை அனுமதிக்கிறது, ஒரே நேரத்தில் {max_downloads+1} பதிவிறக்கங்களை அனுமதிக்கிறது )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - அதிகபட்ச மதிப்பு : {} ( {} இணையான பதிவிறக்கங்களை அனுமதிக்கிறது, ஒரே நேரத்தில் {} பதிவிறக்கங்களை அனுமதிக்கிறது )" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - குறைந்தபட்ச மதிப்பு : 1 (1 இணை பதிவிறக்கத்தை அனுமதிக்கிறது, ஒரு நேரத்தில் 2 பதிவிறக்கங்களை அனுமதிக்கிறது )" @@ -812,9 +811,9 @@ msgstr " - குறைந்தபட்ச மதிப்பு : 1 (1 இ msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - முடக்கு/இயல்புநிலை: 0 (இணை பதிவிறக்கத்தை முடக்குகிறது, ஒரு நேரத்தில் 1 பதிவிறக்கத்தை மட்டுமே அனுமதிக்கிறது )" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "தவறான உள்ளீடு! சரியான உள்ளீட்டில் [1 முதல் {max_downloads} வரை அல்லது முடக்க 0 வரை] மீண்டும் முயற்சிக்கவும்" +msgstr "தவறான உள்ளீடு! சரியான உள்ளீட்டில் [1 முதல் {} வரை அல்லது முடக்க 0 வரை] மீண்டும் முயற்சிக்கவும்" msgid "Parallel Downloads" msgstr "இணையான பதிவிறக்கங்கள்" @@ -1169,3 +1168,10 @@ msgstr "மிரர் பிராந்தியம்" #, fuzzy msgid "Mirror regions" msgstr "மிரர் பிராந்தியம்" + +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - அதிகபட்ச மதிப்பு : {} ( {} இணையான பதிவிறக்கங்களை அனுமதிக்கிறது, ஒரே நேரத்தில் {} பதிவிறக்கங்களை அனுமதிக்கிறது )" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "தவறான உள்ளீடு! சரியான உள்ளீட்டில் [1 முதல் {} வரை அல்லது முடக்க 0 வரை] மீண்டும் முயற்சிக்கவும்" diff --git a/archinstall/locales/tr/LC_MESSAGES/base.po b/archinstall/locales/tr/LC_MESSAGES/base.po index 40efea03..20fa4f23 100644 --- a/archinstall/locales/tr/LC_MESSAGES/base.po +++ b/archinstall/locales/tr/LC_MESSAGES/base.po @@ -804,14 +804,13 @@ msgstr "" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" msgstr "" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" @@ -1177,3 +1176,9 @@ msgstr "İndirme sunucusu bölgesi" #, fuzzy msgid "Mirror regions" msgstr "İndirme sunucusu bölgesi" + +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "" diff --git a/archinstall/locales/uk/LC_MESSAGES/base.mo b/archinstall/locales/uk/LC_MESSAGES/base.mo index ae98dcfb..e7cb8b0b 100644 Binary files a/archinstall/locales/uk/LC_MESSAGES/base.mo and b/archinstall/locales/uk/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/uk/LC_MESSAGES/base.po b/archinstall/locales/uk/LC_MESSAGES/base.po index f2c7737c..a700c126 100644 --- a/archinstall/locales/uk/LC_MESSAGES/base.po +++ b/archinstall/locales/uk/LC_MESSAGES/base.po @@ -793,18 +793,17 @@ msgstr "Налаштовані інтерфейси {}" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Цей параметр вмикає кількість паралельних завантажень, які можуть відбуватися під час встановлення" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "Введіть кількість паралельних завантажень, які потрібно ввімкнути.\n" -" (Введіть значення від 1 до {max_downloads})\n" +" (Введіть значення від 1 до {})\n" "Примітка:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Максимальне значення : {max_downloads} ( Дозволяє {max_downloads} паралельних завантажень, дозволяє {max_downloads+1} завантажень за раз )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - Максимальне значення : {} ( Дозволяє {} паралельних завантажень, дозволяє {} завантажень за раз )" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - Мінімальне значення : 1 ( дозволяє 1 паралельне завантаження, дозволяє 2 завантаження одночасно )" @@ -812,9 +811,9 @@ msgstr " - Мінімальне значення : 1 ( дозволяє 1 па msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - Вимкнути/Типово : 0 ( Вимикає паралельне завантаження, дозволяє лише 1 завантаження за раз )" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "Некоректне введення! Повторіть спробу з валідним введенням [від 1 до {max_downloads} або 0 для вимкнення]" +msgstr "Некоректне введення! Повторіть спробу з валідним введенням [від 1 до {} або 0 для вимкнення]" msgid "Parallel Downloads" msgstr "Паралельні Завантаження" @@ -1167,3 +1166,10 @@ msgstr "Регіон дзеркала" #, fuzzy msgid "Mirror regions" msgstr "Регіон дзеркала" + +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - Максимальне значення : {} ( Дозволяє {} паралельних завантажень, дозволяє {} завантажень за раз )" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "Некоректне введення! Повторіть спробу з валідним введенням [від 1 до {} або 0 для вимкнення]" diff --git a/archinstall/locales/ur/LC_MESSAGES/base.po b/archinstall/locales/ur/LC_MESSAGES/base.po index 6660f3b4..ac648108 100644 --- a/archinstall/locales/ur/LC_MESSAGES/base.po +++ b/archinstall/locales/ur/LC_MESSAGES/base.po @@ -825,14 +825,13 @@ msgstr "" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" msgstr "" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" @@ -1198,6 +1197,12 @@ msgstr "متبادل علاقہ" msgid "Mirror regions" msgstr "متبادل علاقہ" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "" + #~ msgid "Add :" #~ msgstr "شامل:" diff --git a/archinstall/locales/zh-CN/LC_MESSAGES/base.mo b/archinstall/locales/zh-CN/LC_MESSAGES/base.mo index 693308ab..4502c562 100644 Binary files a/archinstall/locales/zh-CN/LC_MESSAGES/base.mo and b/archinstall/locales/zh-CN/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/zh-CN/LC_MESSAGES/base.po b/archinstall/locales/zh-CN/LC_MESSAGES/base.po index afa1716b..04a703ed 100644 --- a/archinstall/locales/zh-CN/LC_MESSAGES/base.po +++ b/archinstall/locales/zh-CN/LC_MESSAGES/base.po @@ -792,18 +792,17 @@ msgstr "已配置的 {} 接口" msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "此选项启用安装期间可能发生的并行下载次数" -#, python-brace-format msgid "" "Enter the number of parallel downloads to be enabled.\n" -" (Enter a value between 1 to {max_downloads})\n" +" (Enter a value between 1 to {})\n" "Note:" msgstr "" "输入要启用的并行下载数。\n" -" (输入一个介于 1 到 {max_downloads} 之间的值)\n" +" (输入一个介于 1 到 {} 之间的值)\n" "提示:" -msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - 最大值:{max_downloads}(允许 {max_downloads} 次并行下载,一次允许 {max_downloads+1} 次下载)" +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )" +msgstr " - 最大值:{}(允许 {} 次并行下载,一次允许 {} 次下载)" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - 最小值:1(允许 1 次并行下载,一次允许 2 次下载)" @@ -811,9 +810,9 @@ msgstr " - 最小值:1(允许 1 次并行下载,一次允许 2 次下载 msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" msgstr " - 禁用/默认:0(禁用并行下载,一次只允许 1 个下载)" -#, python-brace-format +#, fuzzy, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" -msgstr "输入无效! 使用有效输入重试 [1 到 {max_downloads},或 0 到禁用]" +msgstr "输入无效! 使用有效输入重试 [1 到 {},或 0 到禁用]" msgid "Parallel Downloads" msgstr "并行下载" @@ -1168,3 +1167,10 @@ msgstr "镜像区域" #, fuzzy msgid "Mirror regions" msgstr "镜像区域" + +#, fuzzy +msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr " - 最大值:{}(允许 {} 次并行下载,一次允许 {} 次下载)" + +msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" +msgstr "输入无效! 使用有效输入重试 [1 到 {},或 0 到禁用]" -- cgit v1.2.3-70-g09d2 From c7c34c9e704b880ba0ad26696946b6561d2ee784 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Wed, 21 Jun 2023 17:52:48 +1000 Subject: Make Gfx driver handling saver (#1885) Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 2 +- archinstall/lib/global_menu.py | 2 +- archinstall/lib/hardware.py | 117 +++++++++++++++++++--------- archinstall/lib/interactions/system_conf.py | 24 +++--- archinstall/lib/profile/profile_menu.py | 15 ++-- archinstall/lib/profile/profile_model.py | 8 +- archinstall/lib/profile/profiles_handler.py | 11 +-- 7 files changed, 113 insertions(+), 66 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 305d4096..ddf81824 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -18,7 +18,7 @@ from .lib import profile from .lib import interactions from . import default_profiles -from .lib.hardware import SysInfo, AVAILABLE_GFX_DRIVERS +from .lib.hardware import SysInfo, GfxDriver from .lib.installer import Installer, accessibility_tools_in_use from .lib.output import FormattedOutput, log, error, debug, warn, info from .lib.storage import storage diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 91ebc6a0..54b30240 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -349,7 +349,7 @@ class GlobalMenu(AbstractMenu): output += profile_config.profile.name + '\n' if profile_config.gfx_driver: - output += str(_('Graphics driver')) + ': ' + profile_config.gfx_driver + '\n' + output += str(_('Graphics driver')) + ': ' + profile_config.gfx_driver.value + '\n' if profile_config.greeter: output += str(_('Greeter')) + ': ' + profile_config.greeter.value + '\n' diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index bd153a63..85f903e1 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -1,4 +1,5 @@ import os +from enum import Enum from functools import cached_property from pathlib import Path from typing import Optional, Dict, List @@ -8,43 +9,85 @@ from .general import SysCommand from .networking import list_interfaces, enrich_iface_types from .output import debug -AVAILABLE_GFX_DRIVERS = { - # Sub-dicts are layer-2 options to be selected - # and lists are a list of packages to be installed - "All open-source (default)": [ - "mesa", - "xf86-video-amdgpu", - "xf86-video-ati", - "xf86-video-nouveau", - "xf86-video-vmware", - "libva-mesa-driver", - "libva-intel-driver", - "intel-media-driver", - "vulkan-radeon", - "vulkan-intel", - ], - "AMD / ATI (open-source)": [ - "mesa", - "xf86-video-amdgpu", - "xf86-video-ati", - "libva-mesa-driver", - "vulkan-radeon", - ], - "Intel (open-source)": [ - "mesa", - "libva-intel-driver", - "intel-media-driver", - "vulkan-intel", - ], - "Nvidia (open kernel module for newer GPUs, Turing+)": ["nvidia-open"], - "Nvidia (open-source nouveau driver)": [ - "mesa", - "xf86-video-nouveau", - "libva-mesa-driver" - ], - "Nvidia (proprietary)": ["nvidia"], - "VMware / VirtualBox (open-source)": ["mesa", "xf86-video-vmware"], -} + +class GfxPackage(Enum): + IntelMediaDriver = 'intel-media-driver' + LibvaIntelDriver = 'libva-intel-driver' + LibvaMesaDriver = 'libva-mesa-driver' + Mesa = "mesa" + Nvidia = 'nvidia' + NvidiaOpen = 'nvidia-open' + VulkanIntel = 'vulkan-intel' + VulkanRadeon = 'vulkan-radeon' + Xf86VideoAmdgpu = "xf86-video-amdgpu" + Xf86VideoAti = "xf86-video-ati" + Xf86VideoNouveau = 'xf86-video-nouveau' + Xf86VideoVmware = 'xf86-video-vmware' + + +class GfxDriver(Enum): + AllOpenSource = 'All open-source' + AmdOpenSource = 'AMD / ATI (open-source)' + IntelOpenSource = 'Intel (open-source)' + NvidiaOpenKernel = 'Nvidia (open kernel module for newer GPUs, Turing+)' + NvidiaOpenSource = 'Nvidia (open-source nouveau driver)' + NvidiaProprietary = 'Nvidia (proprietary)' + VMOpenSource = 'VMware / VirtualBox (open-source)' + + def is_nvidia(self) -> bool: + match self: + case GfxDriver.NvidiaProprietary | \ + GfxDriver.NvidiaOpenSource | \ + GfxDriver.NvidiaOpenKernel: + return True + case _: + return False + + def packages(self) -> List[GfxPackage]: + match self: + case GfxDriver.AllOpenSource: + return [ + GfxPackage.Mesa, + GfxPackage.Xf86VideoAmdgpu, + GfxPackage.Xf86VideoAti, + GfxPackage.Xf86VideoNouveau, + GfxPackage.Xf86VideoVmware, + GfxPackage.LibvaMesaDriver, + GfxPackage.LibvaIntelDriver, + GfxPackage.IntelMediaDriver, + GfxPackage.VulkanRadeon, + GfxPackage.VulkanIntel + ] + case GfxDriver.AmdOpenSource: + return [ + GfxPackage.Mesa, + GfxPackage.Xf86VideoAmdgpu, + GfxPackage.Xf86VideoAti, + GfxPackage.LibvaMesaDriver, + GfxPackage.VulkanRadeon + ] + case GfxDriver.IntelOpenSource: + return [ + GfxPackage.Mesa, + GfxPackage.LibvaIntelDriver, + GfxPackage.IntelMediaDriver, + GfxPackage.VulkanIntel + ] + case GfxDriver.NvidiaOpenKernel: + return [GfxPackage.NvidiaOpen] + case GfxDriver.NvidiaOpenSource: + return [ + GfxPackage.Mesa, + GfxPackage.Xf86VideoNouveau, + GfxPackage.LibvaMesaDriver + ] + case GfxDriver.NvidiaProprietary: + return [GfxPackage.Nvidia] + case GfxDriver.VMOpenSource: + return [ + GfxPackage.Mesa, + GfxPackage.Xf86VideoVmware + ] class _SysInfo: diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py index ea7e5989..5b1bc456 100644 --- a/archinstall/lib/interactions/system_conf.py +++ b/archinstall/lib/interactions/system_conf.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import List, Any, Dict, TYPE_CHECKING, Optional +from typing import List, Any, TYPE_CHECKING, Optional -from ..hardware import AVAILABLE_GFX_DRIVERS, SysInfo +from ..hardware import SysInfo, GfxDriver from ..menu import MenuSelectionType, Menu from ..models.bootloader import Bootloader @@ -65,7 +65,7 @@ def ask_for_bootloader(preset: Bootloader) -> Bootloader: return preset -def select_driver(options: Dict[str, Any] = {}, current_value: Optional[str] = None) -> Optional[str]: +def select_driver(options: List[GfxDriver] = [], current_value: Optional[GfxDriver] = None) -> Optional[GfxDriver]: """ Some what convoluted function, whose job is simple. Select a graphics driver from a pre-defined set of popular options. @@ -73,11 +73,10 @@ def select_driver(options: Dict[str, Any] = {}, current_value: Optional[str] = N (The template xorg is for beginner users, not advanced, and should there for appeal to the general public first and edge cases later) """ - if not options: - options = AVAILABLE_GFX_DRIVERS + options = [driver for driver in GfxDriver] - drivers = sorted(list(options.keys())) + drivers = sorted([o.value for o in options]) if drivers: title = '' @@ -90,13 +89,18 @@ def select_driver(options: Dict[str, Any] = {}, current_value: Optional[str] = N title += str(_('\nSelect a graphics driver or leave blank to install all open-source drivers')) - preset = current_value if current_value else None - choice = Menu(title, drivers, preset_values=preset).run() + preset = current_value.value if current_value else None + choice = Menu( + title, + drivers, + preset_values=preset, + default_option=GfxDriver.AllOpenSource.value + ).run() if choice.type_ != MenuSelectionType.Selection: - return None + return current_value - return choice.value # type: ignore + return GfxDriver(choice.single_value) return current_value diff --git a/archinstall/lib/profile/profile_menu.py b/archinstall/lib/profile/profile_menu.py index 213466a6..079a9817 100644 --- a/archinstall/lib/profile/profile_menu.py +++ b/archinstall/lib/profile/profile_menu.py @@ -4,9 +4,9 @@ from typing import TYPE_CHECKING, Any, Optional, Dict from archinstall.default_profiles.profile import Profile, GreeterType from .profile_model import ProfileConfiguration -from ..hardware import AVAILABLE_GFX_DRIVERS from ..menu import Menu, MenuSelectionType, AbstractSubMenu, Selector from ..interactions.system_conf import select_driver +from ..hardware import GfxDriver if TYPE_CHECKING: _: Any @@ -38,7 +38,7 @@ class ProfileMenu(AbstractSubMenu): self._menu_options['gfx_driver'] = Selector( _('Graphics driver'), lambda preset: self._select_gfx_driver(preset), - display_func=lambda x: x if x else None, + display_func=lambda x: x.value if x else None, dependencies=['profile'], default=self._preset.gfx_driver if self._preset.profile and self._preset.profile.is_graphic_driver_supported() else None, enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False @@ -73,7 +73,7 @@ class ProfileMenu(AbstractSubMenu): self._menu_options['gfx_driver'].set_current_selection(None) else: self._menu_options['gfx_driver'].set_enabled(True) - self._menu_options['gfx_driver'].set_current_selection('All open-source (default)') + self._menu_options['gfx_driver'].set_current_selection(GfxDriver.AllOpenSource) if not profile.is_greeter_supported(): self._menu_options['greeter'].set_enabled(False) @@ -87,7 +87,7 @@ class ProfileMenu(AbstractSubMenu): return profile - def _select_gfx_driver(self, preset: Optional[str] = None) -> Optional[str]: + def _select_gfx_driver(self, preset: Optional[GfxDriver] = None) -> Optional[GfxDriver]: driver = preset profile: Optional[Profile] = self._menu_options['profile'].current_selection @@ -96,11 +96,8 @@ class ProfileMenu(AbstractSubMenu): driver = select_driver(current_value=preset) if driver and 'Sway' in profile.current_selection_names(): - packages = AVAILABLE_GFX_DRIVERS[driver] - - if packages and "nvidia" in packages: - prompt = str( - _('The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?')) + if driver.is_nvidia(): + prompt = str(_('The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?')) choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run() if choice.value == Menu.no(): diff --git a/archinstall/lib/profile/profile_model.py b/archinstall/lib/profile/profile_model.py index ad3015ae..2b52073a 100644 --- a/archinstall/lib/profile/profile_model.py +++ b/archinstall/lib/profile/profile_model.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Optional, Dict +from ..hardware import GfxDriver from archinstall.default_profiles.profile import Profile, GreeterType if TYPE_CHECKING: @@ -12,14 +13,14 @@ if TYPE_CHECKING: @dataclass class ProfileConfiguration: profile: Optional[Profile] = None - gfx_driver: Optional[str] = None + gfx_driver: Optional[GfxDriver] = None greeter: Optional[GreeterType] = None def json(self) -> Dict[str, Any]: from .profiles_handler import profile_handler return { 'profile': profile_handler.to_json(self.profile), - 'gfx_driver': self.gfx_driver, + 'gfx_driver': self.gfx_driver.value if self.gfx_driver else None, 'greeter': self.greeter.value if self.greeter else None } @@ -27,9 +28,10 @@ class ProfileConfiguration: def parse_arg(cls, arg: Dict[str, Any]) -> 'ProfileConfiguration': from .profiles_handler import profile_handler greeter = arg.get('greeter', None) + gfx_driver = arg.get('gfx_driver', None) return ProfileConfiguration( profile_handler.parse_profile_config(arg['profile']), - arg.get('gfx_driver', None), + GfxDriver(gfx_driver) if gfx_driver else None, GreeterType(greeter) if greeter else None ) diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 2cc15d8e..4e7c3d2b 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -11,7 +11,7 @@ from typing import List, TYPE_CHECKING, Any, Optional, Dict, Union from archinstall.default_profiles.profile import Profile, TProfile, GreeterType from .profile_model import ProfileConfiguration -from ..hardware import AVAILABLE_GFX_DRIVERS +from ..hardware import GfxDriver, GfxPackage from ..menu import MenuSelectionType, Menu, MenuSelection from ..networking import list_interfaces, fetch_data_from_url from ..output import error, debug, info, warn @@ -188,17 +188,18 @@ class ProfileHandler: if service: install_session.enable_service(service) - def install_gfx_driver(self, install_session: 'Installer', driver: str): + def install_gfx_driver(self, install_session: 'Installer', driver: Optional[GfxDriver]): try: - driver_pkgs = AVAILABLE_GFX_DRIVERS[driver] if driver else [] - additional_pkg = ' '.join(['xorg-server', 'xorg-xinit'] + driver_pkgs) + driver_pkgs = driver.packages() if driver else [] + pkg_names = [p.value for p in driver_pkgs] + additional_pkg = ' '.join(['xorg-server', 'xorg-xinit'] + pkg_names) if driver is not None: # Find the intersection between the set of known nvidia drivers # and the selected driver packages. Since valid intesections can # only have one element or none, we iterate and try to take the # first element. - if driver_pkg := next(iter({'nvidia','nvidia-open'} & set(driver_pkgs)), None): + if driver_pkg := next(iter({GfxPackage.Nvidia, GfxPackage.NvidiaOpen} & set(driver_pkgs)), None): if any(kernel in install_session.base_packages for kernel in ("linux-lts", "linux-zen")): for kernel in install_session.kernels: # Fixes https://github.com/archlinux/archinstall/issues/585 -- cgit v1.2.3-70-g09d2 From a0e4e6ee7604419d58d80f22b0348df6e745d8c8 Mon Sep 17 00:00:00 2001 From: Anhad Singh <62820092+Andy-Python-Programmer@users.noreply.github.com> Date: Fri, 30 Jun 2023 17:53:53 +1000 Subject: installer: add Limine bootloader (#1815) * installer: add Limine bootloader Limine is a modern, advanced, portable, multiprotocol bootloader. [Limine GitHub](https://github.com/limine-bootloader/limine) [Limine Arch Wiki](https://wiki.archlinux.org/title/Limine) Signed-off-by: Anhad Singh * limine: add UEFI support Signed-off-by: Anhad Singh * global_menu: check filesystem and bootloader compatibility Before on install, only missing configurations were checked. This commit introduces bootloader validatity checks on install which verify if the selected filesystem is compatiable with the selected bootloader (for example, it is not possible to boot limine from BTRFS). Signed-off-by: Anhad Singh * misc: fix the return value of `_validate_bootloader` Signed-off-by: Anhad Singh * global_menu: make `mypy` happy Signed-off-by: Anhad Singh * misc: make `flake8` happy Signed-off-by: Anhad Singh * limine: upgrade to v5 Signed-off-by: Anhad Singh * limine: install packman hooks Create the BIOS and UEFI pacman hooks so limine gets auto deployed on update. Signed-off-by: Anhad Singh * installer::limine: fix broken root UUID Signed-off-by: Anhad Singh * docs: add a note saying its in beta Signed-off-by: Anhad Singh * install_limine: use `safe_fs_type` Signed-off-by: Anhad Singh --------- Signed-off-by: Anhad Singh --- archinstall/lib/global_menu.py | 44 ++++++++++- archinstall/lib/installer.py | 112 ++++++++++++++++++++++++++++ archinstall/lib/interactions/system_conf.py | 4 +- archinstall/lib/models/bootloader.py | 1 + 4 files changed, 156 insertions(+), 5 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 54b30240..5a431010 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -169,8 +169,8 @@ class GlobalMenu(AbstractMenu): self._menu_options['install'] = \ Selector( self._install_text(), - exec_func=lambda n, v: True if len(self._missing_configs()) == 0 else False, - preview_func=self._prev_install_missing_config, + exec_func=lambda n, v: self._is_config_valid(), + preview_func=self._prev_install_invalid_config, no_store=True) self._menu_options['abort'] = Selector(_('Abort'), exec_func=lambda n,v:exit(1)) @@ -200,6 +200,14 @@ class GlobalMenu(AbstractMenu): return list(missing) + 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 _update_install_text(self, name: str, value: str): text = self._install_text() self._menu_options['install'].update_description(text) @@ -321,12 +329,42 @@ class GlobalMenu(AbstractMenu): return disk.EncryptionType.type_to_text(current_value.encryption_type) return '' - def _prev_install_missing_config(self) -> Optional[str]: + def _validate_bootloader(self) -> Optional[str]: + """ + 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 = self._menu_options['bootloader'].current_selection + boot_partition: Optional[disk.PartitionModification] = None + + if disk_config := self._menu_options['disk_config'].current_selection: + for layout in disk_config.device_modifications: + if boot_partition := layout.get_boot_partition(): + break + else: + return "No disk layout selected" + + if boot_partition is None: + return "Boot partition not found" + + if bootloader == Bootloader.Limine and boot_partition.fs_type == disk.FilesystemType.Btrfs: + return "Limine bootloader does not support booting from BTRFS filesystem" + + return None + + def _prev_install_invalid_config(self) -> Optional[str]: if missing := self._missing_configs(): text = str(_('Missing configurations:\n')) for m in missing: text += f'- {m}\n' return text[:-1] # remove last new line + + if error := self._validate_bootloader(): + return f"Invalid configuration: {error}" + return None def _prev_users(self) -> Optional[str]: diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index ee546993..0d43b2fe 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -8,6 +8,8 @@ import time from pathlib import Path from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable +from ..lib.disk.device_model import get_lsblk_info + from . import disk from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .general import SysCommand @@ -850,6 +852,113 @@ class Installer: self.helper_flags['bootloader'] = "grub" + def _add_limine_bootloader( + self, + boot_partition: disk.PartitionModification, + root_partition: disk.PartitionModification + ): + self.pacman.strap('limine') + info(f"Limine boot partition: {boot_partition.dev_path}") + + # XXX: We cannot use `root_partition.uuid` since corresponds to the UUID of the root + # partition before the format. + root_uuid = get_lsblk_info(root_partition.safe_dev_path).uuid + + device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) + if not device: + raise ValueError(f'Can not find block device: {boot_partition.safe_dev_path}') + + def create_pacman_hook(contents: str): + HOOK_DIR = "/etc/pacman.d/hooks" + SysCommand(f"/usr/bin/arch-chroot {self.target} mkdir -p {HOOK_DIR}") + SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c \"echo '{contents}' > {HOOK_DIR}/liminedeploy.hook\"") + + if SysInfo.has_uefi(): + try: + # The `limine.sys` file, contains stage 3 code. + cmd = f'/usr/bin/arch-chroot' \ + f' {self.target}' \ + f' cp' \ + f' /usr/share/limine/BOOTX64.EFI' \ + f' /boot/EFI/BOOT/' + except SysCallError as err: + raise DiskError(f"Failed to install Limine BOOTX64.EFI on {boot_partition.dev_path}: {err}") + + # Create the EFI limine pacman hook. + create_pacman_hook(""" +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = limine + +[Action] +Description = Deploying Limine after upgrade... +When = PostTransaction +Exec = /usr/bin/cp /usr/share/limine/BOOTX64.EFI /boot/EFI/BOOT/ + """) + else: + try: + # The `limine.sys` file, contains stage 3 code. + cmd = f'/usr/bin/arch-chroot' \ + f' {self.target}' \ + f' cp' \ + f' /usr/share/limine/limine-bios.sys' \ + f' /boot/limine-bios.sys' + + SysCommand(cmd, peek_output=True) + + # `limine bios-install` deploys the stage 1 and 2 to the disk. + cmd = f'/usr/bin/arch-chroot' \ + f' {self.target}' \ + f' limine' \ + f' bios-install' \ + f' {device.device_info.path}' + + SysCommand(cmd, peek_output=True) + except SysCallError as err: + raise DiskError(f"Failed to install Limine on {boot_partition.dev_path}: {err}") + + create_pacman_hook(f""" +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = limine + +[Action] +Description = Deploying Limine after upgrade... +When = PostTransaction +# XXX: Kernel name descriptors cannot be used since they are not persistent and +# can change after each boot. +Exec = /bin/sh -c \\"/usr/bin/limine bios-install /dev/disk/by-uuid/{root_uuid} && /usr/bin/cp /usr/share/limine/limine-bios.sys /boot/\\" + """) + + # Limine does not ship with a default configuation file. We are going to + # create a basic one that is similar to the one GRUB generates. + try: + config = f""" +TIMEOUT=5 + +:Arch Linux + PROTOCOL=linux + KERNEL_PATH=boot:///vmlinuz-linux + CMDLINE=root=UUID={root_uuid} rw rootfstype={root_partition.safe_fs_type.value} loglevel=3 + MODULE_PATH=boot:///initramfs-linux.img + +:Arch Linux (fallback) + PROTOCOL=linux + KERNEL_PATH=boot:///vmlinuz-linux + CMDLINE=root=UUID={root_uuid} rw rootfstype={root_partition.safe_fs_type.value} loglevel=3 + MODULE_PATH=boot:///initramfs-linux-fallback.img + """ + + SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c \"echo '{config}' > /boot/limine.cfg\"") + except SysCallError as err: + raise DiskError(f"Could not configure Limine: {err}") + + self.helper_flags['bootloader'] = "limine" + def _add_efistub_bootloader( self, boot_partition: disk.PartitionModification, @@ -918,6 +1027,7 @@ class Installer: Archinstall supports one of three types: * systemd-bootctl * grub + * limine (beta) * efistub (beta) :param bootloader: Type of bootloader to be added @@ -948,6 +1058,8 @@ class Installer: self._add_grub_bootloader(boot_partition, root_partition) case Bootloader.Efistub: self._add_efistub_bootloader(boot_partition, root_partition) + case Bootloader.Limine: + self._add_limine_bootloader(boot_partition, root_partition) def add_additional_packages(self, packages: Union[str, List[str]]) -> bool: return self.pacman.strap(packages) diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py index 5b1bc456..0e5e0f1e 100644 --- a/archinstall/lib/interactions/system_conf.py +++ b/archinstall/lib/interactions/system_conf.py @@ -40,9 +40,9 @@ def select_kernel(preset: List[str] = []) -> List[str]: def ask_for_bootloader(preset: Bootloader) -> Bootloader: - # when the system only supports grub + # Systemd is UEFI only if not SysInfo.has_uefi(): - options = [Bootloader.Grub.value] + options = [Bootloader.Grub.value, Bootloader.Limine.value] default = Bootloader.Grub.value else: options = Bootloader.values() diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py index e21cda33..be9812a0 100644 --- a/archinstall/lib/models/bootloader.py +++ b/archinstall/lib/models/bootloader.py @@ -12,6 +12,7 @@ class Bootloader(Enum): Systemd = 'Systemd-boot' Grub = 'Grub' Efistub = 'Efistub' + Limine = 'Limine' def json(self): return self.value -- cgit v1.2.3-70-g09d2 From 2f273868d416c3309191db8c616aae683d78370a Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 17 Jul 2023 17:27:21 +1000 Subject: Fix network settings loading from config file (#1921) * Fix network config error and simplify code * Update schema and exmaple --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 7 +- archinstall/lib/global_menu.py | 27 ++-- archinstall/lib/installer.py | 18 +-- archinstall/lib/interactions/__init__.py | 2 +- archinstall/lib/interactions/network_conf.py | 172 -------------------- archinstall/lib/interactions/network_menu.py | 159 +++++++++++++++++++ archinstall/lib/models/__init__.py | 6 +- archinstall/lib/models/network_configuration.py | 200 ++++++++++-------------- archinstall/scripts/guided.py | 11 +- archinstall/scripts/swiss.py | 9 +- docs/installing/guided.rst | 8 +- examples/config-sample.json | 20 ++- examples/custom-command-sample.json | 4 +- examples/interactive_installation.py | 7 +- schema.json | 23 +-- 15 files changed, 313 insertions(+), 360 deletions(-) delete mode 100644 archinstall/lib/interactions/network_conf.py create mode 100644 archinstall/lib/interactions/network_menu.py (limited to 'archinstall/lib/interactions') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index af811465..c4b64912 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -225,10 +225,9 @@ def load_config(): if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) - if arguments.get('nic', None) is not None: - handler = models.NetworkConfigurationHandler() - handler.parse_arguments(arguments.get('nic')) - arguments['nic'] = handler.configuration + if arguments.get('network_config', None) is not None: + config = NetworkConfiguration.parse_arg(arguments.get('network_config')) + arguments['network_config'] = config if arguments.get('!users', None) is not None or arguments.get('!superusers', None) is not None: users = arguments.get('!users', None) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 02b1b0b6..5503d9ce 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -7,7 +7,7 @@ from .general import secret from .locale.locale_menu import LocaleConfiguration, LocaleMenu from .menu import Selector, AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu -from .models import NetworkConfiguration +from .models import NetworkConfiguration, NicType from .models.bootloader import Bootloader from .models.users import User from .output import FormattedOutput @@ -142,7 +142,7 @@ class GlobalMenu(AbstractMenu): lambda preset: select_additional_repositories(preset), display_func=lambda x: ', '.join(x) if x else None, default=[]) - self._menu_options['nic'] = \ + self._menu_options['network_config'] = \ Selector( _('Network configuration'), lambda preset: ask_to_configure_network(preset), @@ -221,14 +221,11 @@ class GlobalMenu(AbstractMenu): return _('Install ({} config(s) missing)').format(missing) return _('Install') - def _display_network_conf(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str: - if not cur_value: - return _('Not configured, unavailable unless setup manually') - else: - if isinstance(cur_value, list): - return str(_('Configured {} interfaces')).format(len(cur_value)) - else: - return str(cur_value) + def _display_network_conf(self, config: Optional[NetworkConfiguration]) -> str: + if not config: + return str(_('Not configured, unavailable unless setup manually')) + + return config.type.display_msg() def _disk_encryption(self, preset: Optional[disk.DiskEncryption]) -> Optional[disk.DiskEncryption]: mods: Optional[List[disk.DeviceModification]] = self._menu_options['disk_config'].current_selection @@ -257,11 +254,11 @@ class GlobalMenu(AbstractMenu): return None def _prev_network_config(self) -> Optional[str]: - selector = self._menu_options['nic'] - if selector.has_selection(): - ifaces = selector.current_selection - if isinstance(ifaces, list): - return FormattedOutput.as_table(ifaces) + selector: Optional[NetworkConfiguration] = self._menu_options['network_config'].current_selection + if selector: + if selector.type == NicType.MANUAL: + output = FormattedOutput.as_table(selector.nics) + return output return None def _prev_additional_pkgs(self): diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 02d48768..8c5e7648 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -19,7 +19,7 @@ from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .luks import Luks2 from .mirrors import use_mirrors, MirrorConfiguration, add_custom_mirrors from .models.bootloader import Bootloader -from .models.network_configuration import NetworkConfiguration +from .models.network_configuration import Nic from .models.users import User from .output import log, error, info, warn, debug from . import pacman @@ -458,20 +458,20 @@ class Installer: def drop_to_shell(self) -> None: subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True) - def configure_nic(self, network_config: NetworkConfiguration) -> None: - conf = network_config.as_systemd_config() + def configure_nic(self, nic: Nic): + conf = nic.as_systemd_config() for plugin in plugins.values(): if hasattr(plugin, 'on_configure_nic'): conf = plugin.on_configure_nic( - network_config.iface, - network_config.dhcp, - network_config.ip, - network_config.gateway, - network_config.dns + nic.iface, + nic.dhcp, + nic.ip, + nic.gateway, + nic.dns ) or conf - with open(f"{self.target}/etc/systemd/network/10-{network_config.iface}.network", "a") as netconf: + with open(f"{self.target}/etc/systemd/network/10-{nic.iface}.network", "a") as netconf: netconf.write(str(conf)) def copy_iso_network_config(self, enable_services :bool = False) -> bool: diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 466cfa0b..53be8e7a 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -1,5 +1,5 @@ from .manage_users_conf import UserList, ask_for_additional_users -from .network_conf import ManualNetworkConfig, ask_to_configure_network +from .network_menu import ManualNetworkConfig, ask_to_configure_network from .utils import get_password from .disk_conf import ( diff --git a/archinstall/lib/interactions/network_conf.py b/archinstall/lib/interactions/network_conf.py deleted file mode 100644 index 18a834a1..00000000 --- a/archinstall/lib/interactions/network_conf.py +++ /dev/null @@ -1,172 +0,0 @@ -from __future__ import annotations - -import ipaddress -from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict - -from ..menu import MenuSelectionType, TextInput -from ..models.network_configuration import NetworkConfiguration, NicType - -from ..networking import list_interfaces -from ..output import FormattedOutput, warn -from ..menu import ListManager, Menu - -if TYPE_CHECKING: - _: Any - - -class ManualNetworkConfig(ListManager): - """ - subclass of ListManager for the managing of network configurations - """ - - def __init__(self, prompt: str, ifaces: List[NetworkConfiguration]): - self._actions = [ - str(_('Add interface')), - str(_('Edit interface')), - str(_('Delete interface')) - ] - - super().__init__(prompt, ifaces, [self._actions[0]], self._actions[1:]) - - def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[NetworkConfiguration]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, iface in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = iface - - return display_data - - def selected_action_display(self, iface: NetworkConfiguration) -> str: - return iface.iface if iface.iface else '' - - def handle_action(self, action: str, entry: Optional[NetworkConfiguration], data: List[NetworkConfiguration]): - if action == self._actions[0]: # add - iface_name = self._select_iface(data) - if iface_name: - iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) - iface = self._edit_iface(iface) - data += [iface] - elif entry: - if action == self._actions[1]: # edit interface - data = [d for d in data if d.iface != entry.iface] - data.append(self._edit_iface(entry)) - elif action == self._actions[2]: # delete - data = [d for d in data if d != entry] - - return data - - def _select_iface(self, data: List[NetworkConfiguration]) -> Optional[Any]: - all_ifaces = list_interfaces().values() - existing_ifaces = [d.iface for d in data] - available = set(all_ifaces) - set(existing_ifaces) - choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() - - if choice.type_ == MenuSelectionType.Skip: - return None - - return choice.value - - def _edit_iface(self, edit_iface: NetworkConfiguration): - iface_name = edit_iface.iface - modes = ['DHCP (auto detect)', 'IP (static)'] - default_mode = 'DHCP (auto detect)' - - prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) - mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() - - if mode.value == 'IP (static)': - while 1: - prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) - ip = TextInput(prompt, edit_iface.ip).run().strip() - # Implemented new check for correct IP/subnet input - try: - ipaddress.ip_interface(ip) - break - except ValueError: - warn("You need to enter a valid IP in IP-config mode") - - # Implemented new check for correct gateway IP address - gateway = None - - while 1: - gateway = TextInput( - _('Enter your gateway (router) IP address or leave blank for none: '), - edit_iface.gateway - ).run().strip() - try: - if len(gateway) > 0: - ipaddress.ip_address(gateway) - break - except ValueError: - warn("You need to enter a valid gateway (router) IP address") - - if edit_iface.dns: - display_dns = ' '.join(edit_iface.dns) - else: - display_dns = None - dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() - - dns = [] - if len(dns_input): - dns = dns_input.split(' ') - - return NetworkConfiguration(NicType.MANUAL, iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) - else: - # this will contain network iface names - return NetworkConfiguration(NicType.MANUAL, iface=iface_name) - - -def ask_to_configure_network( - preset: Union[NetworkConfiguration, List[NetworkConfiguration]] -) -> Optional[NetworkConfiguration | List[NetworkConfiguration]]: - """ - Configure the network on the newly installed system - """ - network_options = { - 'none': str(_('No network configuration')), - 'iso_config': str(_('Copy ISO network configuration to installation')), - 'network_manager': str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')), - 'manual': str(_('Manual configuration')) - } - # for this routine it's easier to set the cursor position rather than a preset value - cursor_idx = None - - if preset and not isinstance(preset, list): - if preset.type == 'iso_config': - cursor_idx = 0 - elif preset.type == 'network_manager': - cursor_idx = 1 - - warning = str(_('Are you sure you want to reset this setting?')) - - choice = Menu( - _('Select one network interface to configure'), - list(network_options.values()), - cursor_index=cursor_idx, - sort=False, - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return None - - if choice.value == network_options['none']: - return None - elif choice.value == network_options['iso_config']: - return NetworkConfiguration(NicType.ISO) - elif choice.value == network_options['network_manager']: - return NetworkConfiguration(NicType.NM) - elif choice.value == network_options['manual']: - preset_ifaces = preset if isinstance(preset, list) else [] - return ManualNetworkConfig('Configure interfaces', preset_ifaces).run() - - return preset diff --git a/archinstall/lib/interactions/network_menu.py b/archinstall/lib/interactions/network_menu.py new file mode 100644 index 00000000..14fc5785 --- /dev/null +++ b/archinstall/lib/interactions/network_menu.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import ipaddress +from typing import Any, Optional, TYPE_CHECKING, List, Dict + +from ..menu import MenuSelectionType, TextInput +from ..models.network_configuration import NetworkConfiguration, NicType, Nic + +from ..networking import list_interfaces +from ..output import FormattedOutput, warn +from ..menu import ListManager, Menu + +if TYPE_CHECKING: + _: Any + + +class ManualNetworkConfig(ListManager): + """ + subclass of ListManager for the managing of network configurations + """ + + def __init__(self, prompt: str, preset: List[Nic]): + self._actions = [ + str(_('Add interface')), + str(_('Edit interface')), + str(_('Delete interface')) + ] + super().__init__(prompt, preset, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[Nic]) -> Dict[str, Optional[Nic]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[Nic]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, iface in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = iface + + return display_data + + def selected_action_display(self, nic: Nic) -> str: + return nic.iface if nic.iface else '' + + def handle_action(self, action: str, entry: Optional[Nic], data: List[Nic]): + if action == self._actions[0]: # add + iface = self._select_iface(data) + if iface: + nic = Nic(iface=iface) + nic = self._edit_iface(nic) + data += [nic] + elif entry: + if action == self._actions[1]: # edit interface + data = [d for d in data if d.iface != entry.iface] + data.append(self._edit_iface(entry)) + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] + + return data + + def _select_iface(self, data: List[Nic]) -> Optional[str]: + all_ifaces = list_interfaces().values() + existing_ifaces = [d.iface for d in data] + available = set(all_ifaces) - set(existing_ifaces) + choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() + + if choice.type_ == MenuSelectionType.Skip: + return None + + return choice.single_value + + def _edit_iface(self, edit_nic: Nic) -> Nic: + iface_name = edit_nic.iface + modes = ['DHCP (auto detect)', 'IP (static)'] + default_mode = 'DHCP (auto detect)' + + prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) + mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() + + if mode.value == 'IP (static)': + while 1: + prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) + ip = TextInput(prompt, edit_nic.ip).run().strip() + # Implemented new check for correct IP/subnet input + try: + ipaddress.ip_interface(ip) + break + except ValueError: + warn("You need to enter a valid IP in IP-config mode") + + # Implemented new check for correct gateway IP address + gateway = None + + while 1: + gateway = TextInput( + _('Enter your gateway (router) IP address or leave blank for none: '), + edit_nic.gateway + ).run().strip() + try: + if len(gateway) > 0: + ipaddress.ip_address(gateway) + break + except ValueError: + warn("You need to enter a valid gateway (router) IP address") + + if edit_nic.dns: + display_dns = ' '.join(edit_nic.dns) + else: + display_dns = None + dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() + + dns = [] + if len(dns_input): + dns = dns_input.split(' ') + + return Nic(iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) + else: + # this will contain network iface names + return Nic(iface=iface_name) + + +def ask_to_configure_network(preset: Optional[NetworkConfiguration]) -> Optional[NetworkConfiguration]: + """ + Configure the network on the newly installed system + """ + options = {n.display_msg(): n for n in NicType} + preset_val = preset.type.display_msg() if preset else None + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select one network interface to configure'), + list(options.keys()), + preset_values=preset_val, + sort=False, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + case MenuSelectionType.Selection: + nic_type = options[choice.single_value] + + match nic_type: + case NicType.ISO: + return NetworkConfiguration(NicType.ISO) + case NicType.NM: + return NetworkConfiguration(NicType.NM) + case NicType.MANUAL: + preset_nics = preset.nics if preset else [] + nics = ManualNetworkConfig('Configure interfaces', preset_nics).run() + if nics: + return NetworkConfiguration(NicType.MANUAL, nics) + + return preset diff --git a/archinstall/lib/models/__init__.py b/archinstall/lib/models/__init__.py index 8cc49ea0..7415f63f 100644 --- a/archinstall/lib/models/__init__.py +++ b/archinstall/lib/models/__init__.py @@ -1,4 +1,8 @@ -from .network_configuration import NetworkConfiguration, NicType, NetworkConfigurationHandler +from .network_configuration import ( + NetworkConfiguration, + NicType, + Nic +) from .bootloader import Bootloader from .gen import VersionDef, PackageSearchResult, PackageSearch, LocalPackage from .users import PasswordStrength, User diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index e564b97b..fac7bbef 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -2,56 +2,64 @@ from __future__ import annotations from dataclasses import dataclass, field from enum import Enum -from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING, Tuple +from typing import List, Optional, Dict, Any, TYPE_CHECKING, Tuple -from ..output import debug from ..profile import ProfileConfiguration if TYPE_CHECKING: _: Any -class NicType(str, Enum): +class NicType(Enum): ISO = "iso" NM = "nm" MANUAL = "manual" + def display_msg(self) -> str: + match self: + case NicType.ISO: + return str(_('Copy ISO network configuration to installation')) + case NicType.NM: + return str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')) + case NicType.MANUAL: + return str(_('Manual configuration')) + @dataclass -class NetworkConfiguration: - type: NicType +class Nic: iface: Optional[str] = None ip: Optional[str] = None dhcp: bool = True gateway: Optional[str] = None dns: List[str] = field(default_factory=list) - def __str__(self): - if self.is_iso(): - return "Copy ISO configuration" - elif self.is_network_manager(): - return "Use NetworkManager" - elif self.is_manual(): - if self.dhcp: - return f'iface={self.iface}, dhcp=auto' - else: - return f'iface={self.iface}, ip={self.ip}, dhcp=staticIp, gateway={self.gateway}, dns={self.dns}' - else: - return 'Unknown type' - def table_data(self) -> Dict[str, Any]: - exclude_fields = ['type'] - data = {} - for k, v in self.__dict__.items(): - if k not in exclude_fields: - if isinstance(v, list) and len(v) == 0: - v = '' - elif v is None: - v = '' - - data[k] = v - - return data + return { + 'iface': self.iface if self.iface else '', + 'ip': self.ip if self.ip else '', + 'dhcp': self.dhcp, + 'gateway': self.gateway if self.gateway else '', + 'dns': self.dns + } + + def __dump__(self) -> Dict[str, Any]: + return { + 'iface': self.iface, + 'ip': self.ip, + 'dhcp': self.dhcp, + 'gateway': self.gateway, + 'dns': self.dns + } + + @staticmethod + def parse_arg(arg: Dict[str, Any]) -> Nic: + return Nic( + iface=arg.get('iface', None), + ip=arg.get('ip', None), + dhcp=arg.get('dhcp', True), + gateway=arg.get('gateway', None), + dns=arg.get('dns', []), + ) def as_systemd_config(self) -> str: match: List[Tuple[str, str]] = [] @@ -80,107 +88,57 @@ class NetworkConfiguration: return config_str - def json(self) -> Dict: - # for json serialization when calling json.dumps(...) on this class - return self.__dict__ - - def is_iso(self) -> bool: - return self.type == NicType.ISO - - def is_network_manager(self) -> bool: - return self.type == NicType.NM - - def is_manual(self) -> bool: - return self.type == NicType.MANUAL - - -class NetworkConfigurationHandler: - def __init__(self, config: Union[None, NetworkConfiguration, List[NetworkConfiguration]] = None): - self._configuration = config - - @property - def configuration(self): - return self._configuration - def config_installer( +@dataclass +class NetworkConfiguration: + type: NicType + nics: List[Nic] = field(default_factory=list) + + def __dump__(self) -> Dict[str, Any]: + config: Dict[str, Any] = {'type': self.type.value} + if self.nics: + config['nics'] = [n.__dump__() for n in self.nics] + + return config + + @staticmethod + def parse_arg(config: Dict[str, Any]) -> Optional[NetworkConfiguration]: + nic_type = config.get('type', None) + if not nic_type: + return None + + match NicType(nic_type): + case NicType.ISO: + return NetworkConfiguration(NicType.ISO) + case NicType.NM: + return NetworkConfiguration(NicType.NM) + case NicType.MANUAL: + nics_arg = config.get('nics', []) + if nics_arg: + nics = [Nic.parse_arg(n) for n in nics_arg] + return NetworkConfiguration(NicType.MANUAL, nics) + + return None + + def install_network_config( self, installation: Any, profile_config: Optional[ProfileConfiguration] = None ): - if self._configuration is None: - return - - if isinstance(self._configuration, list): - for config in self._configuration: - installation.configure_nic(config) - - installation.enable_service('systemd-networkd') - installation.enable_service('systemd-resolved') - else: - # If user selected to copy the current ISO network configuration - # Perform a copy of the config - if self._configuration.is_iso(): + match self.type: + case NicType.ISO: installation.copy_iso_network_config( - enable_services=True # Sources the ISO network configuration to the install medium. + enable_services=True # Sources the ISO network configuration to the install medium. ) - elif self._configuration.is_network_manager(): + case NicType.NM: installation.add_additional_packages(["networkmanager"]) if profile_config and profile_config.profile: if profile_config.profile.is_desktop_type_profile(): installation.add_additional_packages(["network-manager-applet"]) installation.enable_service('NetworkManager.service') + case NicType.MANUAL: + for nic in self.nics: + installation.configure_nic(nic) - def _parse_manual_config(self, configs: List[Dict[str, Any]]) -> Optional[List[NetworkConfiguration]]: - configurations = [] - - for manual_config in configs: - iface = manual_config.get('iface', None) - - if iface is None: - raise ValueError('No iface specified for manual configuration') - - if manual_config.get('dhcp', False) or not any([manual_config.get(v, '') for v in ['ip', 'gateway', 'dns']]): - configurations.append( - NetworkConfiguration(NicType.MANUAL, iface=iface) - ) - else: - ip = manual_config.get('ip', '') - if not ip: - raise ValueError('Manual nic configuration with no auto DHCP requires an IP address') - - dns = manual_config.get('dns', []) - if not isinstance(dns, list): - dns = [dns] - - configurations.append( - NetworkConfiguration( - NicType.MANUAL, - iface=iface, - ip=ip, - gateway=manual_config.get('gateway', ''), - dns=dns, - dhcp=False - ) - ) - - return configurations - - def _parse_nic_type(self, nic_type: str) -> NicType: - try: - return NicType(nic_type) - except ValueError: - options = [e.value for e in NicType] - raise ValueError(f'Unknown nic type: {nic_type}. Possible values are {options}') - - def parse_arguments(self, config: Any): - if isinstance(config, list): # new data format - self._configuration = self._parse_manual_config(config) - elif nic_type := config.get('type', None): # new data format - type_ = self._parse_nic_type(nic_type) - - if type_ != NicType.MANUAL: - self._configuration = NetworkConfiguration(type_) - else: # manual configuration settings - self._configuration = self._parse_manual_config([config]) - else: - debug(f'Unable to parse network configuration: {config}') + installation.enable_service('systemd-networkd') + installation.enable_service('systemd-resolved') diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 7f9b9fd6..c8df590d 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, Optional import archinstall from archinstall import info, debug @@ -14,7 +14,7 @@ from archinstall.lib.installer import Installer from archinstall.lib.menu import Menu from archinstall.lib.mirrors import use_mirrors, add_custom_mirrors from archinstall.lib.models.bootloader import Bootloader -from archinstall.lib.models.network_configuration import NetworkConfigurationHandler +from archinstall.lib.models.network_configuration import NetworkConfiguration from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler @@ -82,7 +82,7 @@ def ask_user_questions(): global_menu.enable('parallel downloads') # Ask or Call the helper function that asks the user to optionally configure a network. - global_menu.enable('nic') + global_menu.enable('network_config') global_menu.enable('timezone') @@ -158,11 +158,10 @@ def perform_installation(mountpoint: Path): # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config = archinstall.arguments.get('nic', None) + network_config: Optional[NetworkConfiguration] = archinstall.arguments.get('network_config', None) if network_config: - handler = NetworkConfigurationHandler(network_config) - handler.config_installer( + network_config.install_network_config( installation, archinstall.arguments.get('profile_config', None) ) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 375458a1..a2ab0549 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -95,7 +95,7 @@ class SwissMainMenu(GlobalMenu): options_list = [ 'mirror_config', 'disk_config', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', - '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', + '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] @@ -110,7 +110,7 @@ class SwissMainMenu(GlobalMenu): options_list = [ 'mirror_config','bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', - 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' + 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] mandatory_list = ['hostname'] @@ -222,11 +222,10 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config = archinstall.arguments.get('nic', None) + network_config = archinstall.arguments.get('network_config', None) if network_config: - handler = models.NetworkConfigurationHandler(network_config) - handler.config_installer( + network_config.install_network_config( installation, archinstall.arguments.get('profile_config', None) ) diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index 4cb07ae1..0a075282 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -29,7 +29,7 @@ To start the installer, run the following in the latest Arch Linux ISO: .. code-block:: sh archinstall --script guided - + | The ``--script guided`` argument is optional as it's the default behavior. | But this will use our most guided installation and if you skip all the option steps it will install a minimal Arch Linux experience. @@ -49,7 +49,7 @@ There are three different configuration files, all of which are optional. .. note:: You can always get the latest options with ``archinstall --dry-run``, but edit the following json according to your needs. Save the configuration as a ``.json`` file. Archinstall can source it via a local or remote path (URL) - + .. code-block:: json { @@ -72,8 +72,8 @@ There are three different configuration files, all of which are optional. ], "keyboard-language": "us", "mirror-region": "Worldwide", - "nic": { - "type": "NM" + "network_config": { + "type": "nm" }, "ntp": true, "packages": ["docker", "git", "wget", "zsh"], diff --git a/examples/config-sample.json b/examples/config-sample.json index a7c5d537..38415b2c 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -99,13 +99,19 @@ "http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch": true, } }, - "nic": { - "dhcp": true, - "dns": null, - "gateway": null, - "iface": null, - "ip": null, - "type": "nm" + "network_config": { + "nics": [ + { + "dhcp": false, + "dns": [ + "3.3.3.3" + ], + "gateway": "2.2.2.2", + "iface": "enp0s31f6", + "ip": "1.1.1.1" + } + ], + "type": "manual" }, "no_pkg_lookups": false, "ntp": true, diff --git a/examples/custom-command-sample.json b/examples/custom-command-sample.json index 8d8d611d..b2250e2c 100644 --- a/examples/custom-command-sample.json +++ b/examples/custom-command-sample.json @@ -12,8 +12,8 @@ ], "keyboard-layout": "us", "mirror-region": "Worldwide", - "nic": { - "type": "NM" + "network_config": { + "type": "nm" }, "ntp": true, "packages": ["docker", "git", "wget", "zsh"], diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index ce1a80ec..8e82ca7e 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -61,7 +61,7 @@ def ask_user_questions(): global_menu.enable('parallel downloads') # Ask or Call the helper function that asks the user to optionally configure a network. - global_menu.enable('nic') + global_menu.enable('network_config') global_menu.enable('timezone') @@ -137,11 +137,10 @@ def perform_installation(mountpoint: Path): # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config = archinstall.arguments.get('nic', None) + network_config = archinstall.arguments.get('network_config', None) if network_config: - handler = models.NetworkConfigurationHandler(network_config) - handler.config_installer( + network_config.install_network_config( installation, archinstall.arguments.get('profile_config', None) ) diff --git a/schema.json b/schema.json index 0a41ebf0..b74588a1 100644 --- a/schema.json +++ b/schema.json @@ -69,21 +69,26 @@ "description": "By default, it will autodetect the best region. Enter a region or a dictionary of regions and mirrors to use specific ones", "type": "object" }, - "nic": { + "network_config": { "description": "Choose between NetworkManager, manual configuration, use systemd-networkd from the ISO or no configuration", "type": "object", "properties": { "type": "string", - "iface": "string", - "dhcp": "boolean", - "ip": "string", - "gateway": "string", - "dns": { - "description": "List of DNS servers", + "nics": [ "type": "array", "items": { - "type": "string" - } + "iface": "string", + "dhcp": "boolean", + "ip": "string", + "gateway": "string", + "dns": { + "description": "List of DNS servers", + "type": "array", + "items": { + "type": "string" + } + } + ] } } }, -- cgit v1.2.3-70-g09d2 From 439bb5428bb6a6f512f695a83ee6b3b8f6537598 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 25 Jul 2023 19:17:09 +1000 Subject: Fix 1934 - audio server regression (#1946) * Audio configuration * Update * Update schema --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 3 ++ archinstall/lib/global_menu.py | 27 ++++++++------ archinstall/lib/interactions/general_conf.py | 30 +++++++++++---- archinstall/lib/models/__init__.py | 1 + archinstall/lib/models/audio_configuration.py | 54 +++++++++++++++++++++++++++ archinstall/scripts/guided.py | 19 +++------- archinstall/scripts/swiss.py | 19 ++++------ docs/installing/guided.rst | 2 +- examples/config-sample.json | 22 ++++------- examples/custom-command-sample.json | 1 - examples/interactive_installation.py | 22 +++-------- schema.json | 19 ++++++---- 12 files changed, 137 insertions(+), 82 deletions(-) create mode 100644 archinstall/lib/models/audio_configuration.py (limited to 'archinstall/lib/interactions') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index c4b64912..cfaecd16 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -237,6 +237,9 @@ def load_config(): if arguments.get('bootloader', None) is not None: arguments['bootloader'] = models.Bootloader.from_arg(arguments['bootloader']) + if arguments.get('audio_config', None) is not None: + arguments['audio_config'] = models.AudioConfiguration.parse_arg(arguments['audio_config']) + if arguments.get('disk_encryption', None) is not None and disk_config is not None: password = arguments.get('encryption_password', '') arguments['disk_encryption'] = disk.DiskEncryption.parse_arg( diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 5503d9ce..fb62b7b5 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING +from typing import Any, List, Optional, Dict, TYPE_CHECKING from . import disk from .general import secret @@ -9,6 +9,7 @@ from .menu import Selector, AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu from .models import NetworkConfiguration, NicType from .models.bootloader import Bootloader +from .models.audio_configuration import Audio, AudioConfiguration from .models.users import User from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration @@ -109,12 +110,11 @@ class GlobalMenu(AbstractMenu): display_func=lambda x: x.profile.name if x else '', preview_func=self._prev_profile ) - self._menu_options['audio'] = \ + self._menu_options['audio_config'] = \ Selector( _('Audio'), lambda preset: self._select_audio(preset), - display_func=lambda x: x if x else '', - default=None + display_func=lambda x: self._display_audio(x) ) self._menu_options['parallel downloads'] = \ Selector( @@ -421,13 +421,18 @@ class GlobalMenu(AbstractMenu): profile_config = ProfileMenu(store, preset=current_profile).run() return profile_config - def _select_audio(self, current: Union[str, None]) -> Optional[str]: - profile_config: Optional[ProfileConfiguration] = self._menu_options['profile_config'].current_selection - if profile_config and profile_config.profile: - is_desktop = profile_config.profile.is_desktop_profile() if profile_config else False - selection = ask_for_audio_selection(is_desktop, current) - return selection - return None + def _select_audio( + self, + current: Optional[AudioConfiguration] = None + ) -> Optional[AudioConfiguration]: + selection = ask_for_audio_selection(current) + return selection + + def _display_audio(self, current: Optional[AudioConfiguration]) -> str: + if not current: + return Audio.no_audio_text() + else: + return current.audio.name def _create_user_account(self, defined_users: List[User]) -> List[User]: users = ask_for_additional_users(defined_users=defined_users) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index ad9ee386..1c570a69 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -5,6 +5,7 @@ from typing import List, Any, Optional, TYPE_CHECKING from ..locale import list_timezones, list_keyboard_languages from ..menu import MenuSelectionType, Menu, TextInput +from ..models.audio_configuration import Audio, AudioConfiguration from ..output import warn from ..packages.packages import validate_package_list from ..storage import storage @@ -55,16 +56,31 @@ def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: return None -def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]: - no_audio = str(_('No audio server')) - choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] - default = 'pipewire' if desktop else no_audio +def ask_for_audio_selection( + current: Optional[AudioConfiguration] = None +) -> Optional[AudioConfiguration]: + choices = [ + Audio.Pipewire.name, + Audio.Pulseaudio.name, + Audio.no_audio_text() + ] - choice = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=default).run() + preset = current.audio.name if current else None + + choice = Menu( + _('Choose an audio server'), + choices, + preset_values=preset + ).run() match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return current + case MenuSelectionType.Selection: + value = choice.single_value + if value == Audio.no_audio_text(): + return None + else: + return AudioConfiguration(Audio[value]) return None diff --git a/archinstall/lib/models/__init__.py b/archinstall/lib/models/__init__.py index 7415f63f..a1c90e48 100644 --- a/archinstall/lib/models/__init__.py +++ b/archinstall/lib/models/__init__.py @@ -6,3 +6,4 @@ from .network_configuration import ( from .bootloader import Bootloader from .gen import VersionDef, PackageSearchResult, PackageSearch, LocalPackage from .users import PasswordStrength, User +from .audio_configuration import Audio, AudioConfiguration diff --git a/archinstall/lib/models/audio_configuration.py b/archinstall/lib/models/audio_configuration.py new file mode 100644 index 00000000..3a4029db --- /dev/null +++ b/archinstall/lib/models/audio_configuration.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Any, TYPE_CHECKING, Dict + +from ..hardware import SysInfo +from ..output import info +from ...default_profiles.applications.pipewire import PipewireProfile + +if TYPE_CHECKING: + _: Any + + +@dataclass +class Audio(Enum): + Pipewire = 'pipewire' + Pulseaudio = 'pulseaudio' + + @staticmethod + def no_audio_text() -> str: + return str(_('No audio server')) + + +@dataclass +class AudioConfiguration: + audio: Audio + + def __dump__(self) -> Dict[str, Any]: + return { + 'audio': self.audio.value + } + + @staticmethod + def parse_arg(arg: Dict[str, Any]) -> 'AudioConfiguration': + return AudioConfiguration( + Audio(arg['audio']) + ) + + def install_audio_config( + self, + installation: Any + ): + info(f'Installing audio server: {self.audio.name}') + + match self.audio: + case Audio.Pipewire: + PipewireProfile().install(installation) + case Audio.Pulseaudio: + installation.add_additional_packages("pulseaudio") + + if SysInfo.requires_sof_fw(): + installation.add_additional_packages('sof-firmware') + + if SysInfo.requires_alsa_fw(): + installation.add_additional_packages('alsa-firmware') diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index c8df590d..605d2b0e 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -8,11 +8,11 @@ from archinstall import SysInfo from archinstall.lib import locale from archinstall.lib import disk from archinstall.lib.global_menu import GlobalMenu -from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.installer import Installer from archinstall.lib.menu import Menu from archinstall.lib.mirrors import use_mirrors, add_custom_mirrors +from archinstall.lib.models import AudioConfiguration from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.network_configuration import NetworkConfiguration from archinstall.lib.networking import check_mirror_reachable @@ -70,7 +70,7 @@ def ask_user_questions(): global_menu.enable('profile_config') # Ask about audio server selection if one is not already set - global_menu.enable('audio') + global_menu.enable('audio_config') # Ask for preferred kernel: global_menu.enable('kernels', mandatory=True) @@ -172,18 +172,9 @@ def perform_installation(mountpoint: Path): if users := archinstall.arguments.get('!users', None): installation.create_users(users) - if audio := archinstall.arguments.get('audio', None): - info(f'Installing audio server: {audio}') - if audio == 'pipewire': - PipewireProfile().install(installation) - elif audio == 'pulseaudio': - installation.add_additional_packages("pulseaudio") - - if SysInfo.requires_sof_fw(): - installation.add_additional_packages('sof-firmware') - - if SysInfo.requires_alsa_fw(): - installation.add_additional_packages('alsa-firmware') + audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None) + if audio_config: + audio_config.install_audio_config(installation) else: info("No audio server will be installed") diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index a2ab0549..cd532f6d 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -1,7 +1,7 @@ import os from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict +from typing import TYPE_CHECKING, Any, Dict, Optional import archinstall from archinstall import SysInfo, info, debug @@ -9,13 +9,13 @@ from archinstall.lib import mirrors from archinstall.lib import models from archinstall.lib import disk from archinstall.lib import locale +from archinstall.lib.models import AudioConfiguration from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib import menu from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer from archinstall.lib.configuration import ConfigurationOutput -from archinstall.default_profiles.applications.pipewire import PipewireProfile if TYPE_CHECKING: _: Any @@ -95,7 +95,7 @@ class SwissMainMenu(GlobalMenu): options_list = [ 'mirror_config', 'disk_config', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', - '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'network_config', + '!users', 'profile_config', 'audio_config', 'kernels', 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] @@ -109,7 +109,7 @@ class SwissMainMenu(GlobalMenu): case ExecutionMode.Only_OS: options_list = [ 'mirror_config','bootloader', 'hostname', - '!root-password', '!users', 'profile_config', 'audio', 'kernels', + '!root-password', '!users', 'profile_config', 'audio_config', 'kernels', 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] @@ -236,14 +236,11 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): if users := archinstall.arguments.get('!users', None): installation.create_users(users) - if audio := archinstall.arguments.get('audio', None): - info(f'Installing audio server: {audio}') - if audio == 'pipewire': - PipewireProfile().install(installation) - elif audio == 'pulseaudio': - installation.add_additional_packages("pulseaudio") + audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None) + if audio_config: + audio_config.install_audio_config(installation) else: - info("No audio server will be installed.") + info("No audio server will be installed") if profile_config := archinstall.arguments.get('profile_config', None): profile_handler.install_profile_config(installation, profile_config) diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index 0a075282..c5e7f1ed 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -53,7 +53,7 @@ There are three different configuration files, all of which are optional. .. code-block:: json { - "audio": "pipewire", + "audio_config": {"audio": "pipewire"}, "bootloader": "systemd-bootctl", "custom-commands": [ "cd /home/devel; git clone https://aur.archlinux.org/paru.git", diff --git a/examples/config-sample.json b/examples/config-sample.json index 38415b2c..ed1cc38e 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -2,7 +2,7 @@ "config_version": "2.5.2", "additional-repositories": [], "archinstall-language": "English", - "audio": "pipewire", + "audio_config": {"audio": "pipewire"}, "bootloader": "Systemd-boot", "debug": false, "disk_config": { @@ -99,19 +99,13 @@ "http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch": true, } }, - "network_config": { - "nics": [ - { - "dhcp": false, - "dns": [ - "3.3.3.3" - ], - "gateway": "2.2.2.2", - "iface": "enp0s31f6", - "ip": "1.1.1.1" - } - ], - "type": "manual" + "nic": { + "dhcp": true, + "dns": null, + "gateway": null, + "iface": null, + "ip": null, + "type": "nm" }, "no_pkg_lookups": false, "ntp": true, diff --git a/examples/custom-command-sample.json b/examples/custom-command-sample.json index b2250e2c..34d63d74 100644 --- a/examples/custom-command-sample.json +++ b/examples/custom-command-sample.json @@ -1,6 +1,5 @@ { "dry_run": true, - "audio": "none", "bootloader": "systemd-bootctl", "debug": false, "harddrives": [ diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 8e82ca7e..e075df9b 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -1,12 +1,11 @@ from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Optional import archinstall from archinstall import Installer from archinstall import profile from archinstall import SysInfo from archinstall import mirrors -from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall import disk from archinstall import menu from archinstall import models @@ -49,7 +48,7 @@ def ask_user_questions(): global_menu.enable('profile_config') # Ask about audio server selection if one is not already set - global_menu.enable('audio') + global_menu.enable('audio_config') # Ask for preferred kernel: global_menu.enable('kernels', mandatory=True) @@ -151,20 +150,11 @@ def perform_installation(mountpoint: Path): if users := archinstall.arguments.get('!users', None): installation.create_users(users) - if audio := archinstall.arguments.get('audio', None): - info(f'Installing audio server: {audio}') - if audio == 'pipewire': - PipewireProfile().install(installation) - elif audio == 'pulseaudio': - installation.add_additional_packages("pulseaudio") - - if SysInfo.requires_sof_fw(): - installation.add_additional_packages('sof-firmware') - - if SysInfo.requires_alsa_fw(): - installation.add_additional_packages('alsa-firmware') + audio_config: Optional[models.AudioConfiguration] = archinstall.arguments.get('audio_config', None) + if audio_config: + audio_config.install_audio_config(installation) else: - info("No audio server will be installed.") + info("No audio server will be installed") if profile_config := archinstall.arguments.get('profile_config', None): profile.profile_handler.install_profile_config(installation, profile_config) diff --git a/schema.json b/schema.json index b74588a1..5616ed41 100644 --- a/schema.json +++ b/schema.json @@ -12,14 +12,19 @@ "testing" ] }, - "audio": { + "audio_config": { "description": "Audio server to be installed", - "type": "string", - "enum": [ - "pipewire", - "pulseaudio", - "none" - ] + "type": "object", + "properties": { + "audio": { + "description": "Audio server to be installed", + "type": "string", + "enum": [ + "pipewire", + "pulseaudio" + ] + } + }, }, "bootloader": { "description": "Bootloader to be installed", -- cgit v1.2.3-70-g09d2 From 9cbb2b75940e3aad1a0236662c45437d70eac1af Mon Sep 17 00:00:00 2001 From: Mário Victor Ribeiro Silva Date: Mon, 31 Jul 2023 04:38:42 -0300 Subject: Parallel downloads (#1952) * refactor: remove max_downloads limit * Update parallel downloads * Update parallel downloads --- archinstall/lib/interactions/general_conf.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 1c570a69..14fcc3f8 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -165,23 +165,20 @@ def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]: def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Optional[int]: - max_downloads = 5 - print(_(f"This option enables the number of parallel downloads that can occur during installation")) - print(str(_("Enter the number of parallel downloads to be enabled.\n (Enter a value between 1 to {})\nNote:")).format(max_downloads)) - print(str(_(" - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )")).format(max_downloads, max_downloads, max_downloads + 1)) - print(_(" - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )")) - print(_(" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )")) + max_recommended = 5 + print(_(f"This option enables the number of parallel downloads that can occur during package downloads")) + print(_("Enter the number of parallel downloads to be enabled.\n\nNote:\n")) + print(str(_(" - Maximum recommended value : {} ( Allows {} parallel downloads at a time )")).format(max_recommended, max_recommended)) + print(_(" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n")) while True: try: input_number = int(TextInput(_("[Default value: 0] > ")).run().strip() or 0) if input_number <= 0: input_number = 0 - elif input_number > max_downloads: - input_number = max_downloads break except: - print(str(_("Invalid input! Try again with a valid input [1 to {}, or 0 to disable]")).format(max_downloads)) + print(str(_("Invalid input! Try again with a valid input [or 0 to disable]")).format(max_recommended)) pacman_conf_path = pathlib.Path("/etc/pacman.conf") with pacman_conf_path.open() as f: @@ -190,7 +187,7 @@ def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Opt with pacman_conf_path.open("w") as fwrite: for line in pacman_conf: if "ParallelDownloads" in line: - fwrite.write(f"ParallelDownloads = {input_number+1}\n") if not input_number == 0 else fwrite.write("#ParallelDownloads = 0\n") + fwrite.write(f"ParallelDownloads = {input_number}\n") if not input_number == 0 else fwrite.write("#ParallelDownloads = 0\n") else: fwrite.write(f"{line}\n") -- cgit v1.2.3-70-g09d2 From 12b7017240a040fd4fbebf7c5794a1ca5560f0ea Mon Sep 17 00:00:00 2001 From: Alexander Seiler Date: Mon, 18 Sep 2023 14:04:36 +0200 Subject: Fix many typos (#1692) Signed-off-by: Alexander Seiler --- archinstall/__init__.py | 2 +- archinstall/default_profiles/profile.py | 6 +++--- archinstall/lib/disk/device_model.py | 2 +- archinstall/lib/disk/fido.py | 2 +- archinstall/lib/general.py | 2 +- archinstall/lib/global_menu.py | 4 ++-- archinstall/lib/installer.py | 10 +++++----- archinstall/lib/interactions/__init__.py | 2 +- archinstall/lib/interactions/general_conf.py | 2 +- archinstall/lib/menu/list_manager.py | 2 +- archinstall/lib/menu/menu.py | 2 +- archinstall/lib/output.py | 2 +- archinstall/lib/translationhandler.py | 2 +- archinstall/scripts/guided.py | 4 ++-- archinstall/scripts/swiss.py | 6 +++--- examples/interactive_installation.py | 4 ++-- 16 files changed, 27 insertions(+), 27 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 39588904..7645ae39 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -43,7 +43,7 @@ __version__ = "2.6.0" storage['__version__'] = __version__ -# add the custome _ as a builtin, it can now be used anywhere in the +# add the custom _ as a builtin, it can now be used anywhere in the # project to mark strings as translatable with _('translate me') DeferredTranslation.install() diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index 982bd5a3..49a9c19d 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -81,7 +81,7 @@ class Profile: def packages(self) -> List[str]: """ Returns a list of packages that should be installed when - this profile is among the choosen ones + this profile is among the chosen ones """ return self._packages @@ -128,7 +128,7 @@ class Profile: """ Set the custom settings for the profile. This is also called when the settings are parsed from the config - and can be overriden to perform further actions based on the profile + and can be overridden to perform further actions based on the profile """ self.custom_settings = settings @@ -179,7 +179,7 @@ class Profile: def preview_text(self) -> Optional[str]: """ Used for preview text in profiles_bck. If a description is set for a - profile it will automatically display that one in the preivew. + profile it will automatically display that one in the preview. If no preview or a different text should be displayed just """ if self.description: diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 6eeb0d91..69038b01 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -202,7 +202,7 @@ class Size: # not sure why we would ever wanna convert to percentages if target_unit == Unit.Percent and total_size is None: - raise ValueError('Missing paramter total size to be able to convert to percentage') + raise ValueError('Missing parameter total size to be able to convert to percentage') if self.unit == target_unit: return self diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 96a74991..9eeba56a 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -34,7 +34,7 @@ class Fido2: /dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID """ - # to prevent continous reloading which will slow + # to prevent continuous reloading which will slow # down moving the cursor in the menu if not cls._loaded or reload: try: diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 611378ee..e22e7eed 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: def generate_password(length :int = 64) -> str: - haystack = string.printable # digits, ascii_letters, punctiation (!"#$[] etc) and whitespace + haystack = string.printable # digits, ascii_letters, punctuation (!"#$[] etc) and whitespace return ''.join(secrets.choice(haystack) for i in range(length)) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index fb62b7b5..b38dac0b 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -15,7 +15,7 @@ from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration from .storage import storage from .configuration import save_config -from .interactions import add_number_of_parrallel_downloads +from .interactions import add_number_of_parallel_downloads from .interactions import ask_additional_packages_to_install from .interactions import ask_for_additional_users from .interactions import ask_for_audio_selection @@ -119,7 +119,7 @@ class GlobalMenu(AbstractMenu): self._menu_options['parallel downloads'] = \ Selector( _('Parallel Downloads'), - lambda preset: add_number_of_parrallel_downloads(preset), + lambda preset: add_number_of_parallel_downloads(preset), display_func=lambda x: x if x else '0', default=0 ) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 09e91ab8..34c9441f 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -194,7 +194,7 @@ class Installer: for part_mod in sorted_part_mods: if luks_handler := luks_handlers.get(part_mod): # mount encrypted partition - self._mount_luks_partiton(part_mod, luks_handler) + self._mount_luks_partition(part_mod, luks_handler) else: # partition is not encrypted self._mount_partition(part_mod) @@ -219,7 +219,7 @@ class Installer: if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.dev_path: self._mount_btrfs_subvol(part_mod.dev_path, part_mod.btrfs_subvols) - def _mount_luks_partiton(self, part_mod: disk.PartitionModification, luks_handler: Luks2): + def _mount_luks_partition(self, part_mod: disk.PartitionModification, luks_handler: Luks2): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined if part_mod.mountpoint and luks_handler.mapper_dev: target = self.target / part_mod.relative_mountpoint @@ -315,7 +315,7 @@ class Installer: raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {err}') if not gen_fstab: - raise RequirementError(f'Genrating fstab returned empty value') + raise RequirementError(f'Generating fstab returned empty value') with open(fstab_path, 'a') as fp: fp.write(gen_fstab) @@ -434,7 +434,7 @@ class Installer: return False - def activate_time_syncronization(self) -> None: + def activate_time_synchronization(self) -> None: info('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers') self.enable_service('systemd-timesyncd') @@ -1008,7 +1008,7 @@ When = PostTransaction Exec = /bin/sh -c \\"/usr/bin/limine bios-install /dev/disk/by-uuid/{root_uuid} && /usr/bin/cp /usr/share/limine/limine-bios.sys /boot/\\" """) - # Limine does not ship with a default configuation file. We are going to + # Limine does not ship with a default configuration file. We are going to # create a basic one that is similar to the one GRUB generates. try: config = f""" diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 53be8e7a..50c0012d 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -11,7 +11,7 @@ from .disk_conf import ( from .general_conf import ( ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_archinstall_language, ask_additional_packages_to_install, - add_number_of_parrallel_downloads, select_additional_repositories + add_number_of_parallel_downloads, select_additional_repositories ) from .system_conf import ( diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 14fcc3f8..8dd6e94f 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -164,7 +164,7 @@ def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]: return packages -def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Optional[int]: +def add_number_of_parallel_downloads(input_number :Optional[int] = None) -> Optional[int]: max_recommended = 5 print(_(f"This option enables the number of parallel downloads that can occur during package downloads")) print(_("Enter the number of parallel downloads to be enabled.\n\nNote:\n")) diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index be31fdf0..54fb6a1b 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -80,7 +80,7 @@ class ListManager: self._data = self.handle_action(choice.value, None, self._data) elif choice.value in self._terminate_actions: break - else: # an entry of the existing selection was choosen + else: # an entry of the existing selection was chosen selected_entry = data_formatted[choice.value] # type: ignore self._run_actions_on_entry(selected_entry) diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 358ba5e4..3bd31b88 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -123,7 +123,7 @@ class Menu(TerminalMenu): :param allow_reset: This will explicitly handle a ctrl+c instead and return that specific state :type allow_reset: bool - param allow_reset_warning_msg: If raise_error_on_interrupt is True the warnign is set, a user confirmation is displayed + param allow_reset_warning_msg: If raise_error_on_interrupt is True the warning is set, a user confirmation is displayed type allow_reset_warning_msg: str :param extra_bottom_space: Add an extra empty line at the end of the menu diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index 63d9c1fb..62a1ba27 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -22,7 +22,7 @@ class FormattedOutput: ) -> Dict[str, Any]: """ the original values returned a dataclass as dict thru the call to some specific methods - this version allows thru the parameter class_formatter to call a dynamicly selected formatting method. + this version allows thru the parameter class_formatter to call a dynamically selected formatting method. Can transmit a filter list to the class_formatter, """ if class_formatter: diff --git a/archinstall/lib/translationhandler.py b/archinstall/lib/translationhandler.py index a2e44065..33230562 100644 --- a/archinstall/lib/translationhandler.py +++ b/archinstall/lib/translationhandler.py @@ -138,7 +138,7 @@ class TranslationHandler: def get_language_by_abbr(self, abbr: str) -> Language: """ - Get a language object by its abbrevation, e.g. en + Get a language object by its abbreviation, e.g. en """ try: return next(filter(lambda x: x.abbr == abbr, self._translated_languages)) diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 51549fa8..d7cf16cd 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -185,7 +185,7 @@ def perform_installation(mountpoint: Path): installation.set_timezone(timezone) if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() + installation.activate_time_synchronization() if archinstall.accessibility_tools_in_use(): installation.enable_espeakup() @@ -193,7 +193,7 @@ def perform_installation(mountpoint: Path): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) - # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # This step must be after profile installs to allow profiles_bck to install language pre-requisites. # After which, this step will set the language both for console and x11 if x11 was installed for instance. installation.set_keyboard_language(locale_config.kb_layout) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 80fa0a48..c04ccca4 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -54,7 +54,7 @@ class SetupMenu(GlobalMenu): super().setup_selection_menu_options() self._menu_options['mode'] = menu.Selector( - 'Excution mode', + 'Execution mode', lambda x : select_mode(), display_func=lambda x: x.value if x else '', default=ExecutionMode.Full) @@ -249,7 +249,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): installation.set_timezone(timezone) if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() + installation.activate_time_synchronization() if archinstall.accessibility_tools_in_use(): installation.enable_espeakup() @@ -257,7 +257,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) - # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # This step must be after profile installs to allow profiles_bck to install language pre-requisites. # After which, this step will set the language both for console and x11 if x11 was installed for instance. installation.set_keyboard_language(locale_config.kb_layout) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 9eac029c..f8cc75fc 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -163,7 +163,7 @@ def perform_installation(mountpoint: Path): installation.set_timezone(timezone) if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() + installation.activate_time_synchronization() if archinstall.accessibility_tools_in_use(): installation.enable_espeakup() @@ -171,7 +171,7 @@ def perform_installation(mountpoint: Path): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) - # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # This step must be after profile installs to allow profiles_bck to install language pre-requisites. # After which, this step will set the language both for console and x11 if x11 was installed for instance. installation.set_keyboard_language(locale_config.kb_layout) -- cgit v1.2.3-70-g09d2 From d6e3a4651f2ff944a00e8acfb316db8be2fbcf3e Mon Sep 17 00:00:00 2001 From: Sxmourai <49468969+Sxmourai@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:46:59 +0200 Subject: Renamed hyperland to hyprland, fixed seatd via post_installation and added waybar-hyprland (#1824) * Renamed hyperland to hyprland, fixed seatd via post_installation and installed waybar * Removed the launching of seatd on the installation process * Starting to add nvidia support, and automatic configuring of hyprland * Starting to add auto configuration of hyprland... But this will need maintenance * Added hyprpaper auto config Gonna make waybar auto config next * Waybar auto config is starting... I can't test rn I'm on vacation and my connection is quite bad (68 days for arch iso) * Added wlogout support (and swaylock) * Fixed file managers printing * Starting to add a shell config... Definitely don't push this * Reverted custom-shell config (create a separate PR) * Removed systemd-logind, as that was just for testing the selector * Added polkit as an option for the seat. As it's a dependency of the hyprland package * Flake8 fix * The name change wasn't propegated to the menu * Added newline at the end of general_conf.py to not alter it * Removed newline at the end of general_conf.py to not alter it * Renamed the Hyprland class --------- Co-authored-by: Anton Hvornum Co-authored-by: Anton Hvornum --- archinstall/default_profiles/desktops/hyperland.py | 27 ---------- archinstall/default_profiles/desktops/hyprland.py | 62 ++++++++++++++++++++++ archinstall/lib/interactions/general_conf.py | 2 +- 3 files changed, 63 insertions(+), 28 deletions(-) delete mode 100644 archinstall/default_profiles/desktops/hyperland.py create mode 100644 archinstall/default_profiles/desktops/hyprland.py (limited to 'archinstall/lib/interactions') diff --git a/archinstall/default_profiles/desktops/hyperland.py b/archinstall/default_profiles/desktops/hyperland.py deleted file mode 100644 index 58ee8cca..00000000 --- a/archinstall/default_profiles/desktops/hyperland.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import List, Optional, Any, TYPE_CHECKING - -from archinstall.default_profiles.profile import ProfileType, GreeterType -from archinstall.default_profiles.xorg import XorgProfile - -if TYPE_CHECKING: - _: Any - - -class HyperlandProfile(XorgProfile): - def __init__(self): - super().__init__('Hyperland', ProfileType.DesktopEnv, description='') - - @property - def packages(self) -> List[str]: - return [ - "hyprland", - "dunst", - "xdg-desktop-portal-hyprland", - "kitty", - "qt5-wayland", - "qt6-wayland" - ] - - @property - def default_greeter_type(self) -> Optional[GreeterType]: - return GreeterType.Sddm diff --git a/archinstall/default_profiles/desktops/hyprland.py b/archinstall/default_profiles/desktops/hyprland.py new file mode 100644 index 00000000..f464c828 --- /dev/null +++ b/archinstall/default_profiles/desktops/hyprland.py @@ -0,0 +1,62 @@ +from enum import Enum +from typing import List, Optional, TYPE_CHECKING, Any + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile +from archinstall.lib.menu import Menu + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +class SeatAccess(Enum): + seatd = 'seatd' + polkit = 'polkit' + + +class HyprlandProfile(XorgProfile): + def __init__(self): + super().__init__('Hyprland', ProfileType.DesktopEnv, description='') + + self.custom_settings = {'seat_access': None} + + @property + def packages(self) -> List[str]: + return [ + "hyprland", + "dunst", + "xdg-desktop-portal-hyprland", + "qt5-wayland", + "qt6-wayland" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Sddm + + @property + def services(self) -> List[str]: + if pref := self.custom_settings.get('seat_access', None): + return [pref] + return [] + + def _ask_seat_access(self): + # need to activate seat service and add to seat group + title = str(_('Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')) + title += str(_('\n\nChoose an option to give Hyprland access to your hardware')) + + options = [e.value for e in SeatAccess] + default = None + + if seat := self.custom_settings.get('seat_access', None): + default = seat + + choice = Menu(title, options, skip=False, preset_values=default).run() + self.custom_settings['seat_access'] = choice.single_value + + def do_on_select(self): + self._ask_seat_access() + + def install(self, install_session: 'Installer'): + super().install(install_session) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 8dd6e94f..56598e25 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -218,4 +218,4 @@ def select_additional_repositories(preset: List[str]) -> List[str]: case MenuSelectionType.Reset: return [] case MenuSelectionType.Selection: return choice.single_value - return [] + return [] \ No newline at end of file -- cgit v1.2.3-70-g09d2 From b141609990fa4f7305443ee6ea6fe8796604c539 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sun, 24 Sep 2023 19:47:38 +1000 Subject: Fix 1669 | Refactor display of sizes in tables (#2100) * Use sector as default display * Display tables in sector size * Refactor size * Update * Update * fix flake8 --------- Co-authored-by: Daniel Girtler --- archinstall/lib/disk/__init__.py | 1 + archinstall/lib/disk/device_model.py | 169 +++++++++++++++++++----------- archinstall/lib/disk/partitioning_menu.py | 39 ++++--- archinstall/lib/installer.py | 2 +- archinstall/lib/interactions/disk_conf.py | 55 ++++++---- examples/config-sample.json | 17 +-- examples/full_automated_installation.py | 15 +-- 7 files changed, 178 insertions(+), 120 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/disk/__init__.py b/archinstall/lib/disk/__init__.py index cdc96373..24dafef5 100644 --- a/archinstall/lib/disk/__init__.py +++ b/archinstall/lib/disk/__init__.py @@ -14,6 +14,7 @@ from .device_model import ( PartitionTable, Unit, Size, + SectorSize, SubvolumeModification, DeviceGeometry, PartitionType, diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 8bc41e0c..08861a63 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -93,7 +93,7 @@ class DiskLayoutConfiguration: status=ModificationStatus(partition['status']), fs_type=FilesystemType(partition['fs_type']), start=Size.parse_args(partition['start']), - length=Size.parse_args(partition['length']), + length=Size.parse_args(partition['size']), mount_options=partition['mount_options'], mountpoint=Path(partition['mountpoint']) if partition['mountpoint'] else None, dev_path=Path(partition['dev_path']) if partition['dev_path'] else None, @@ -138,80 +138,89 @@ class Unit(Enum): sectors = 'sectors' # size in sector - Percent = '%' # size in percentile - @staticmethod def get_all_units() -> List[str]: return [u.name for u in Unit] + @staticmethod + def get_si_units() -> List[Unit]: + return [u for u in Unit if 'i' not in u.name and u.name != 'sectors'] + @dataclass -class Size: +class SectorSize: value: int unit: Unit - sector_size: Optional[Size] = None # only required when unit is sector - total_size: Optional[Size] = None # required when operating on percentages def __post_init__(self): - if self.unit == Unit.sectors and self.sector_size is None: - raise ValueError('Sector size is required when unit is sectors') - elif self.unit == Unit.Percent: - if self.value < 0 or self.value > 100: - raise ValueError('Percentage must be between 0 and 100') - elif self.total_size is None: - raise ValueError('Total size is required when unit is percentage') + match self.unit: + case Unit.sectors: + raise ValueError('Unit type sector not allowed for SectorSize') - @property - def _total_size(self) -> Size: + @staticmethod + def default() -> SectorSize: + return SectorSize(512, Unit.B) + + def json(self) -> Dict[str, Any]: + return { + 'value': self.value, + 'unit': self.unit.name, + } + + @classmethod + def parse_args(cls, arg: Dict[str, Any]) -> SectorSize: + return SectorSize( + arg['value'], + Unit[arg['unit']] + ) + + def normalize(self) -> int: """ - Save method to get the total size, mainly to satisfy mypy - This shouldn't happen as the Size object fails instantiation on missing total size + will normalize the value of the unit to Byte """ - if self.unit == Unit.Percent and self.total_size is None: - raise ValueError('Percent unit size must specify a total size') - return self.total_size # type: ignore + return int(self.value * self.unit.value) # type: ignore + + +@dataclass +class Size: + value: int + unit: Unit + sector_size: SectorSize + + def __post_init__(self): + if not isinstance(self.sector_size, SectorSize): + raise ValueError('sector size must be of type SectorSize') def json(self) -> Dict[str, Any]: return { 'value': self.value, 'unit': self.unit.name, - 'sector_size': self.sector_size.json() if self.sector_size else None, - 'total_size': self._total_size.json() if self._total_size else None + 'sector_size': self.sector_size.json() if self.sector_size else None } @classmethod def parse_args(cls, size_arg: Dict[str, Any]) -> Size: sector_size = size_arg['sector_size'] - total_size = size_arg['total_size'] return Size( size_arg['value'], Unit[size_arg['unit']], - Size.parse_args(sector_size) if sector_size else None, - Size.parse_args(total_size) if total_size else None + SectorSize.parse_args(sector_size), ) def convert( self, target_unit: Unit, - sector_size: Optional[Size] = None, - total_size: Optional[Size] = None + sector_size: Optional[SectorSize] = None ) -> Size: if target_unit == Unit.sectors and sector_size is None: raise ValueError('If target has unit sector, a sector size must be provided') - # not sure why we would ever wanna convert to percentages - if target_unit == Unit.Percent and total_size is None: - raise ValueError('Missing parameter total size to be able to convert to percentage') - if self.unit == target_unit: return self - elif self.unit == Unit.Percent: - amount = int(self._total_size._normalize() * (self.value / 100)) - return Size(amount, Unit.B) elif self.unit == Unit.sectors: norm = self._normalize() - return Size(norm, Unit.B).convert(target_unit, sector_size) + return Size(norm, Unit.B, self.sector_size).convert(target_unit, sector_size) else: if target_unit == Unit.sectors and sector_size is not None: norm = self._normalize() @@ -219,7 +228,7 @@ class Size: return Size(sectors, Unit.sectors, sector_size) else: value = int(self._normalize() / target_unit.value) # type: ignore - return Size(value, target_unit) + return Size(value, target_unit, self.sector_size) def as_text(self) -> str: return self.format_size( @@ -230,31 +239,45 @@ class Size: def format_size( self, target_unit: Unit, - sector_size: Optional[Size] = None, + sector_size: Optional[SectorSize] = None, include_unit: bool = True ) -> str: - if self.unit == Unit.Percent: - return f'{self.value}%' - else: - target_size = self.convert(target_unit, sector_size) - if include_unit: - return f'{target_size.value} {target_unit.name}' - return f'{target_size.value}' + target_size = self.convert(target_unit, sector_size) + + if include_unit: + return f'{target_size.value} {target_unit.name}' + return f'{target_size.value}' + + def format_highest(self, include_unit: bool = True) -> str: + si_units = Unit.get_si_units() + all_si_values = [self.convert(si) for si in si_units] + filtered = filter(lambda x: x.value >= 1, all_si_values) + + # we have to get the max by the unit value as we're interested + # in getting the value in the highest possible unit without floats + si_value = max(filtered, key=lambda x: x.unit.value) + + if include_unit: + return f'{si_value.value} {si_value.unit.name}' + return f'{si_value.value}' def _normalize(self) -> int: """ will normalize the value of the unit to Byte """ - if self.unit == Unit.Percent: - return self.convert(Unit.B).value - elif self.unit == Unit.sectors and self.sector_size is not None: - return self.value * self.sector_size._normalize() + if self.unit == Unit.sectors and self.sector_size is not None: + return self.value * self.sector_size.normalize() return int(self.value * self.unit.value) # type: ignore def __sub__(self, other: Size) -> Size: src_norm = self._normalize() dest_norm = other._normalize() - return Size(abs(src_norm - dest_norm), Unit.B) + return Size(abs(src_norm - dest_norm), Unit.B, self.sector_size) + + def __add__(self, other: Size) -> Size: + src_norm = self._normalize() + dest_norm = other._normalize() + return Size(abs(src_norm + dest_norm), Unit.B, self.sector_size) def __lt__(self, other): return self._normalize() < other._normalize() @@ -296,14 +319,22 @@ class _PartitionInfo: mountpoints: List[Path] btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list) + @property + def sector_size(self) -> SectorSize: + sector_size = self.partition.geometry.device.sectorSize + return SectorSize(sector_size, Unit.B) + def table_data(self) -> Dict[str, Any]: + end = self.start + self.length + part_info = { 'Name': self.name, 'Type': self.type.value, 'Filesystem': self.fs_type.value if self.fs_type else str(_('Unknown')), 'Path': str(self.path), - 'Start': self.start.format_size(Unit.MiB), - 'Length': self.length.format_size(Unit.MiB), + 'Start': self.start.format_size(Unit.sectors, self.sector_size, include_unit=False), + 'End': end.format_size(Unit.sectors, self.sector_size, include_unit=False), + 'Size': self.length.format_highest(), 'Flags': ', '.join([f.name for f in self.flags]) } @@ -327,10 +358,14 @@ class _PartitionInfo: start = Size( partition.geometry.start, Unit.sectors, - Size(partition.disk.device.sectorSize, Unit.B) + SectorSize(partition.disk.device.sectorSize, Unit.B) ) - length = Size(int(partition.getLength(unit='B')), Unit.B) + length = Size( + int(partition.getLength(unit='B')), + Unit.B, + SectorSize(partition.disk.device.sectorSize, Unit.B) + ) return _PartitionInfo( partition=partition, @@ -355,7 +390,7 @@ class _DeviceInfo: type: str total_size: Size free_space_regions: List[DeviceGeometry] - sector_size: Size + sector_size: SectorSize read_only: bool dirty: bool @@ -365,7 +400,7 @@ class _DeviceInfo: 'Model': self.model, 'Path': str(self.path), 'Type': self.type, - 'Size': self.total_size.format_size(Unit.MiB), + 'Size': self.total_size.format_highest(), 'Free space': int(total_free_space), 'Sector size': self.sector_size.value, 'Read only': self.read_only @@ -379,15 +414,17 @@ class _DeviceInfo: else: device_type = parted.devices[device.type] - sector_size = Size(device.sectorSize, Unit.B) + sector_size = SectorSize(device.sectorSize, Unit.B) free_space = [DeviceGeometry(g, sector_size) for g in disk.getFreeSpaceRegions()] + sector_size = SectorSize(device.sectorSize, Unit.B) + return _DeviceInfo( model=device.model.strip(), path=Path(device.path), type=device_type, sector_size=sector_size, - total_size=Size(int(device.getLength(unit='B')), Unit.B), + total_size=Size(int(device.getLength(unit='B')), Unit.B, sector_size), free_space_regions=free_space, read_only=device.readOnly, dirty=device.dirty @@ -470,7 +507,7 @@ class SubvolumeModification: class DeviceGeometry: - def __init__(self, geometry: Geometry, sector_size: Size): + def __init__(self, geometry: Geometry, sector_size: SectorSize): self._geometry = geometry self._sector_size = sector_size @@ -498,7 +535,7 @@ class DeviceGeometry: 'Sector size': self._sector_size.value, 'Start (sector/B)': start_str, 'End (sector/B)': end_str, - 'Length (sectors/B)': length_str + 'Size (sectors/B)': length_str } @@ -751,7 +788,7 @@ class PartitionModification: 'status': self.status.value, 'type': self.type.value, 'start': self.start.json(), - 'length': self.length.json(), + 'size': self.length.json(), 'fs_type': self.fs_type.value if self.fs_type else '', 'mountpoint': str(self.mountpoint) if self.mountpoint else None, 'mount_options': self.mount_options, @@ -764,12 +801,15 @@ class PartitionModification: """ Called for displaying data in table format """ + end = self.start + self.length + part_mod = { 'Status': self.status.value, 'Device': str(self.dev_path) if self.dev_path else '', 'Type': self.type.value, - 'Start': self.start.format_size(Unit.MiB), - 'Length': self.length.format_size(Unit.MiB), + 'Start': self.start.format_size(Unit.sectors, self.start.sector_size, include_unit=False), + 'End': end.format_size(Unit.sectors, self.start.sector_size, include_unit=False), + 'Size': self.length.format_highest(), 'FS type': self.fs_type.value if self.fs_type else 'Unknown', 'Mountpoint': self.mountpoint if self.mountpoint else '', 'Mount options': ', '.join(self.mount_options), @@ -938,7 +978,7 @@ class LsblkInfo: name: str = '' path: Path = Path() pkname: str = '' - size: Size = field(default_factory=lambda: Size(0, Unit.B)) + size: Size = field(default_factory=lambda: Size(0, Unit.B, SectorSize.default())) log_sec: int = 0 pttype: str = '' ptuuid: str = '' @@ -1017,7 +1057,8 @@ class LsblkInfo: if isinstance(getattr(lsblk_info, data_field), Path): val = Path(blockdevice[lsblk_field]) elif isinstance(getattr(lsblk_info, data_field), Size): - val = Size(blockdevice[lsblk_field], Unit.B) + sector_size = SectorSize(blockdevice['log-sec'], Unit.B) + val = Size(blockdevice[lsblk_field], Unit.B, sector_size) else: val = blockdevice[lsblk_field] diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 549c7f34..c5263b82 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ - ModificationStatus, DeviceGeometry + ModificationStatus, DeviceGeometry, SectorSize from ..menu import Menu, ListManager, MenuSelection, TextInput from ..output import FormattedOutput, warn from .subvolume_menu import SubvolumeMenu @@ -194,42 +194,47 @@ class PartitioningList(ListManager): def _validate_value( self, - sector_size: Size, + sector_size: SectorSize, total_size: Size, - value: str + text: str, + start: Optional[Size] ) -> Optional[Size]: - match = re.match(r'([0-9]+)([a-zA-Z|%]*)', value, re.I) + match = re.match(r'([0-9]+)([a-zA-Z|%]*)', text, re.I) if match: - value, unit = match.groups() + str_value, unit = match.groups() - if unit == '%': - unit = Unit.Percent.name + if unit == '%' and start: + available = total_size - start + value = int(available.value * (int(str_value) / 100)) + unit = available.unit.name + else: + value = int(str_value) if unit and unit not in Unit.get_all_units(): return None unit = Unit[unit] if unit else Unit.sectors - return Size(int(value), unit, sector_size, total_size) + return Size(value, unit, sector_size) return None def _enter_size( self, - sector_size: Size, + sector_size: SectorSize, total_size: Size, prompt: str, - default: Size + default: Size, + start: Optional[Size], ) -> Size: while True: value = TextInput(prompt).run().strip() - size: Optional[Size] = None if not value: size = default else: - size = self._validate_value(sector_size, total_size, value) + size = self._validate_value(sector_size, total_size, value, start) if size: return size @@ -247,7 +252,7 @@ class PartitioningList(ListManager): total_bytes = device_info.total_size.format_size(Unit.B) prompt += str(_('Total: {} / {}')).format(total_sectors, total_bytes) + '\n\n' - prompt += str(_('All entered values can be suffixed with a unit: B, KB, KiB, MB, MiB...')) + '\n' + prompt += str(_('All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...')) + '\n' prompt += str(_('If no unit is provided, the value is interpreted as sectors')) + '\n' print(prompt) @@ -260,13 +265,14 @@ class PartitioningList(ListManager): device_info.sector_size, device_info.total_size, start_prompt, - default_start + default_start, + None ) if start_size.value == largest_free_area.start: end_size = Size(largest_free_area.end, Unit.sectors, device_info.sector_size) else: - end_size = Size(100, Unit.Percent, total_size=device_info.total_size) + end_size = device_info.total_size # prompt until valid end sector was entered end_prompt = str(_('Enter end (default: {}): ')).format(end_size.as_text()) @@ -274,7 +280,8 @@ class PartitioningList(ListManager): device_info.sector_size, device_info.total_size, end_prompt, - end_size + end_size, + start_size ) return start_size, end_size diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 05eb5867..a238bb8f 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -163,7 +163,7 @@ class Installer: lsblk_info = disk.get_lsblk_by_mountpoint(boot_mount) if len(lsblk_info) > 0: - if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB): + if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB, disk.SectorSize.default()): raise DiskError( f'The boot partition mounted at {boot_mount} is not large enough to install a boot loader. ' f'Please resize it to at least 200MiB and re-run the installation.' diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 78e4cff4..8542ab75 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -170,13 +170,13 @@ def select_disk_config( return None -def _boot_partition() -> disk.PartitionModification: +def _boot_partition(sector_size: disk.SectorSize) -> disk.PartitionModification: if SysInfo.has_uefi(): - start = disk.Size(1, disk.Unit.MiB) - size = disk.Size(512, disk.Unit.MiB) + start = disk.Size(1, disk.Unit.MiB, sector_size) + size = disk.Size(512, disk.Unit.MiB, sector_size) else: - start = disk.Size(3, disk.Unit.MiB) - size = disk.Size(203, disk.Unit.MiB) + start = disk.Size(3, disk.Unit.MiB, sector_size) + size = disk.Size(203, disk.Unit.MiB, sector_size) # boot partition return disk.PartitionModification( @@ -215,8 +215,9 @@ def suggest_single_disk_layout( if not filesystem_type: filesystem_type = select_main_filesystem_format(advanced_options) - min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB) - root_partition_size = disk.Size(20, disk.Unit.GiB) + sector_size = device.device_info.sector_size + min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB, sector_size) + root_partition_size = disk.Size(20, disk.Unit.GiB, sector_size) using_subvolumes = False using_home_partition = False compression = False @@ -244,7 +245,7 @@ def suggest_single_disk_layout( # Also re-align the start to 1MiB since we don't need the first sectors # like we do in MBR layouts where the boot loader is installed traditionally. - boot_partition = _boot_partition() + boot_partition = _boot_partition(sector_size) device_modification.add_partition(boot_partition) if not using_subvolumes: @@ -259,11 +260,11 @@ def suggest_single_disk_layout( using_home_partition = False # root partition - start = disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB) + start = disk.Size(513, disk.Unit.MiB, sector_size) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB, sector_size) # Set a size for / (/root) if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: - length = disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size) + length = device.device_info.total_size - start else: length = min(device.device_info.total_size, root_partition_size) @@ -294,11 +295,14 @@ def suggest_single_disk_layout( # If we don't want to use subvolumes, # But we want to be able to re-use data between re-installs.. # A second partition for /home would be nice if we have the space for it + start = root_partition.length + length = device.device_info.total_size - root_partition.length + home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=root_partition.length, - length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + start=start, + length=length, mountpoint=Path('/home'), fs_type=filesystem_type, mount_options=['compress=zstd'] if compression else [] @@ -319,9 +323,9 @@ def suggest_multi_disk_layout( # Not really a rock solid foundation of information to stand on, but it's a start: # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ - min_home_partition_size = disk.Size(40, disk.Unit.GiB) + min_home_partition_size = disk.Size(40, disk.Unit.GiB, disk.SectorSize.default()) # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? - desired_root_partition_size = disk.Size(20, disk.Unit.GiB) + desired_root_partition_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default()) compression = False if not filesystem_type: @@ -362,28 +366,41 @@ def suggest_multi_disk_layout( root_device_modification = disk.DeviceModification(root_device, wipe=True) home_device_modification = disk.DeviceModification(home_device, wipe=True) + root_device_sector_size = root_device_modification.device.device_info.sector_size + home_device_sector_size = home_device_modification.device.device_info.sector_size + # add boot partition to the root device - boot_partition = _boot_partition() + boot_partition = _boot_partition(root_device_sector_size) root_device_modification.add_partition(boot_partition) + if SysInfo.has_uefi(): + root_start = disk.Size(513, disk.Unit.MiB, root_device_sector_size) + else: + root_start = disk.Size(206, disk.Unit.MiB, root_device_sector_size) + + root_length = root_device.device_info.total_size - root_start + # add root partition to the root device root_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB), - length=disk.Size(100, disk.Unit.Percent, total_size=root_device.device_info.total_size), + start=root_start, + length=root_length, mountpoint=Path('/'), mount_options=['compress=zstd'] if compression else [], fs_type=filesystem_type ) root_device_modification.add_partition(root_partition) + start = disk.Size(1, disk.Unit.MiB, home_device_sector_size) + length = home_device.device_info.total_size - start + # add home partition to home device home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(1, disk.Unit.MiB), - length=disk.Size(100, disk.Unit.Percent, total_size=home_device.device_info.total_size), + start=start, + length=length, mountpoint=Path('/home'), mount_options=['compress=zstd'] if compression else [], fs_type=filesystem_type, diff --git a/examples/config-sample.json b/examples/config-sample.json index ed1cc38e..d43f7ea6 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -17,9 +17,8 @@ "Boot" ], "fs_type": "fat32", - "length": { + "size": { "sector_size": null, - "total_size": null, "unit": "MiB", "value": 512 }, @@ -28,7 +27,6 @@ "obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0", "start": { "sector_size": null, - "total_size": null, "unit": "MiB", "value": 1 }, @@ -39,9 +37,8 @@ "btrfs": [], "flags": [], "fs_type": "ext4", - "length": { + "size": { "sector_size": null, - "total_size": null, "unit": "GiB", "value": 20 }, @@ -50,7 +47,6 @@ "obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208", "start": { "sector_size": null, - "total_size": null, "unit": "MiB", "value": 513 }, @@ -61,14 +57,8 @@ "btrfs": [], "flags": [], "fs_type": "ext4", - "length": { + "size": { "sector_size": null, - "total_size": { - "sector_size": null, - "total_size": null, - "unit": "B", - "value": 250148290560 - }, "unit": "Percent", "value": 100 }, @@ -77,7 +67,6 @@ "obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f", "start": { "sector_size": null, - "total_size": null, "unit": "GiB", "value": 20 }, diff --git a/examples/full_automated_installation.py b/examples/full_automated_installation.py index 79e85348..d25575d4 100644 --- a/examples/full_automated_installation.py +++ b/examples/full_automated_installation.py @@ -23,8 +23,8 @@ device_modification = disk.DeviceModification(device, wipe=True) boot_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(1, disk.Unit.MiB), - length=disk.Size(512, disk.Unit.MiB), + start=disk.Size(1, disk.Unit.MiB, device.device_info.sector_size), + length=disk.Size(512, disk.Unit.MiB, device.device_info.sector_size), mountpoint=Path('/boot'), fs_type=disk.FilesystemType.Fat32, flags=[disk.PartitionFlag.Boot] @@ -35,20 +35,23 @@ device_modification.add_partition(boot_partition) root_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(513, disk.Unit.MiB), - length=disk.Size(20, disk.Unit.GiB), + start=disk.Size(513, disk.Unit.MiB, device.device_info.sector_size), + length=disk.Size(20, disk.Unit.GiB, device.device_info.sector_size), mountpoint=None, fs_type=fs_type, mount_options=[], ) device_modification.add_partition(root_partition) +start_home = root_partition.length +length_home = device.device_info.total_size - start_home + # create a new home partition home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=root_partition.length, - length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + start=start_home, + length=length_home, mountpoint=Path('/home'), fs_type=fs_type, mount_options=[] -- cgit v1.2.3-70-g09d2 From 717a22371fd25aac10d1f435b65842e800fd9d7e Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Tue, 26 Sep 2023 04:57:45 -0400 Subject: Fix `mountpoint` for pre-mounted disk configuration (#2113) * Fix `mountpoint` for pre-mounted disk configuration * Add missing commas --- archinstall/lib/disk/device_handler.py | 4 +++- archinstall/lib/disk/device_model.py | 22 ++++++---------------- archinstall/lib/installer.py | 6 +++--- archinstall/lib/interactions/disk_conf.py | 1 - examples/auto_discovery_mounted.py | 1 - 5 files changed, 12 insertions(+), 22 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 9646103f..4cb35c03 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -594,7 +594,9 @@ class DeviceHandler(object): if is_subpath(mountpoint, base_mountpoint): path = Path(part_info.disk.device.path) part_mods.setdefault(path, []) - part_mods[path].append(PartitionModification.from_existing_partition(part_info)) + part_mod = PartitionModification.from_existing_partition(part_info) + part_mod.mountpoint = mountpoint.root / mountpoint.relative_to(base_mountpoint) + part_mods[path].append(part_mod) break device_mods: List[DeviceModification] = [] diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 08861a63..b1f012f7 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -42,12 +42,6 @@ class DiskLayoutType(Enum): class DiskLayoutConfiguration: config_type: DiskLayoutType device_modifications: List[DeviceModification] = field(default_factory=list) - # used for pre-mounted config - relative_mountpoint: Optional[Path] = None - - def __post_init__(self): - if self.config_type == DiskLayoutType.Pre_mount and self.relative_mountpoint is None: - raise ValueError('Must set a relative mountpoint when layout type is pre-mount"') def json(self) -> Dict[str, Any]: return { @@ -487,10 +481,8 @@ class SubvolumeModification: raise ValueError('Mountpoint is not specified') - def is_root(self, relative_mountpoint: Optional[Path] = None) -> bool: + def is_root(self) -> bool: if self.mountpoint: - if relative_mountpoint is not None: - return self.mountpoint.relative_to(relative_mountpoint) == Path('.') return self.mountpoint == Path('/') return False @@ -742,14 +734,12 @@ class PartitionModification: """ return any(set(self.flags) & set(self._boot_indicator_flags)) - def is_root(self, relative_mountpoint: Optional[Path] = None) -> bool: - if relative_mountpoint is not None and self.mountpoint is not None: - return self.mountpoint.relative_to(relative_mountpoint) == Path('.') - elif self.mountpoint is not None: + def is_root(self) -> bool: + if self.mountpoint is not None: return Path('/') == self.mountpoint else: for subvol in self.btrfs_subvols: - if subvol.is_root(relative_mountpoint): + if subvol.is_root(): return True return False @@ -861,8 +851,8 @@ class DeviceModification: filtered = filter(lambda x: x.is_boot() and x.mountpoint, self.partitions) return next(filtered, None) - def get_root_partition(self, relative_path: Optional[Path]) -> Optional[PartitionModification]: - filtered = filter(lambda x: x.is_root(relative_path), self.partitions) + def get_root_partition(self) -> Optional[PartitionModification]: + filtered = filter(lambda x: x.is_root(), self.partitions) return next(filtered, None) def json(self) -> Dict[str, Any]: diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 8a0acf64..f8f59cc0 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -716,7 +716,7 @@ class Installer: def _get_root_partition(self) -> Optional[disk.PartitionModification]: for mod in self._disk_config.device_modifications: - if root := mod.get_root_partition(self._disk_config.relative_mountpoint): + if root := mod.get_root_partition(): return root return None @@ -903,8 +903,8 @@ class Installer: add_options = [ '--target=x86_64-efi', - f'--efi-directory={efi_partition.mountpoint}' - f'--boot-directory={boot_partition.mountpoint if boot_partition else "/boot"}' + f'--efi-directory={efi_partition.mountpoint}', + f'--boot-directory={boot_partition.mountpoint if boot_partition else "/boot"}', '--bootloader-id=GRUB', '--removable' ] diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 8542ab75..253a623d 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -139,7 +139,6 @@ def select_disk_config( return disk.DiskLayoutConfiguration( config_type=disk.DiskLayoutType.Pre_mount, - relative_mountpoint=path, device_modifications=mods ) diff --git a/examples/auto_discovery_mounted.py b/examples/auto_discovery_mounted.py index 0bd30cd1..e3cb80b7 100644 --- a/examples/auto_discovery_mounted.py +++ b/examples/auto_discovery_mounted.py @@ -9,5 +9,4 @@ mods = disk.device_handler.detect_pre_mounted_mods(root_mount_dir) disk_config = disk.DiskLayoutConfiguration( disk.DiskLayoutType.Pre_mount, device_modifications=mods, - relative_mountpoint=Path('/mnt/archinstall') ) -- cgit v1.2.3-70-g09d2 From 9f5c2bb70b0a4551eaa871164a3c9d71c1e65086 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:09:28 -0400 Subject: Add support for ESP partition flag (#2133) --- archinstall/lib/disk/device_model.py | 18 +++++++++++------- archinstall/lib/disk/partitioning_menu.py | 7 +++++++ archinstall/lib/installer.py | 11 +++++++++-- archinstall/lib/interactions/disk_conf.py | 4 +++- 4 files changed, 30 insertions(+), 10 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index b1f012f7..6992bccb 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -658,7 +658,8 @@ class PartitionModification: partuuid: Optional[str] = None uuid: Optional[str] = None - _boot_indicator_flags = [PartitionFlag.Boot, PartitionFlag.XBOOTLDR] + _efi_indicator_flags = (PartitionFlag.Boot, PartitionFlag.ESP) + _boot_indicator_flags = (PartitionFlag.Boot, PartitionFlag.XBOOTLDR) def __post_init__(self): # needed to use the object as a dictionary key due to hash func @@ -728,6 +729,13 @@ class PartitionModification: raise ValueError('Mountpoint is not specified') + def is_efi(self) -> bool: + return ( + any(set(self.flags) & set(self._efi_indicator_flags)) + and self.fs_type == FilesystemType.Fat32 + and PartitionFlag.XBOOTLDR not in self.flags + ) + def is_boot(self) -> bool: """ Returns True if any of the boot indicator flags are found in self.flags @@ -828,9 +836,8 @@ class DeviceModification: def get_efi_partition(self) -> Optional[PartitionModification]: """ Similar to get_boot_partition() but excludes XBOOTLDR partitions from it's candidates. - Also works with ESP flag. """ - filtered = filter(lambda x: (x.is_boot() or PartitionFlag.ESP in x.flags) and x.fs_type == FilesystemType.Fat32 and PartitionFlag.XBOOTLDR not in x.flags, self.partitions) + filtered = filter(lambda x: x.is_efi() and x.mountpoint, self.partitions) return next(filtered, None) def get_boot_partition(self) -> Optional[PartitionModification]: @@ -843,10 +850,7 @@ class DeviceModification: filtered = filter(lambda x: x.is_boot() and x != efi_partition and x.mountpoint, self.partitions) if boot_partition := next(filtered, None): return boot_partition - if efi_partition.is_boot(): - return efi_partition - else: - return None + return efi_partition else: filtered = filter(lambda x: x.is_boot() and x.mountpoint, self.partitions) return next(filtered, None) diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index c5263b82..a9478158 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -6,6 +6,7 @@ from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ ModificationStatus, DeviceGeometry, SectorSize +from ..hardware import SysInfo from ..menu import Menu, ListManager, MenuSelection, TextInput from ..output import FormattedOutput, warn from .subvolume_menu import SubvolumeMenu @@ -105,10 +106,14 @@ class PartitioningList(ListManager): entry.mountpoint = self._prompt_mountpoint() if entry.mountpoint == Path('/boot'): entry.set_flag(PartitionFlag.Boot) + if SysInfo.has_uefi(): + entry.set_flag(PartitionFlag.ESP) case 'mark_formatting' if entry: self._prompt_formatting(entry) case 'mark_bootable' if entry: entry.invert_flag(PartitionFlag.Boot) + if SysInfo.has_uefi(): + entry.invert_flag(PartitionFlag.ESP) case 'set_filesystem' if entry: fs_type = self._prompt_partition_fs_type() if fs_type: @@ -310,6 +315,8 @@ class PartitioningList(ListManager): if partition.mountpoint == Path('/boot'): partition.set_flag(PartitionFlag.Boot) + if SysInfo.has_uefi(): + partition.set_flag(PartitionFlag.ESP) return partition diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index e7895a1a..585389ed 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -903,15 +903,22 @@ class Installer: '--debug' ] - if SysInfo.has_uefi() and efi_partition is not None: + if SysInfo.has_uefi(): + if not efi_partition: + raise ValueError('Could not detect efi partition') + info(f"GRUB EFI partition: {efi_partition.dev_path}") self.pacman.strap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? + boot_dir_arg = [] + if boot_partition != efi_partition: + boot_dir_arg.append(f'--boot-directory={boot_dir}') + add_options = [ '--target=x86_64-efi', f'--efi-directory={efi_partition.mountpoint}', - f'--boot-directory={boot_dir}', + *boot_dir_arg, '--bootloader-id=GRUB', '--removable' ] diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 253a623d..84a3196c 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -170,9 +170,11 @@ def select_disk_config( def _boot_partition(sector_size: disk.SectorSize) -> disk.PartitionModification: + flags = [disk.PartitionFlag.Boot] if SysInfo.has_uefi(): start = disk.Size(1, disk.Unit.MiB, sector_size) size = disk.Size(512, disk.Unit.MiB, sector_size) + flags.append(disk.PartitionFlag.ESP) else: start = disk.Size(3, disk.Unit.MiB, sector_size) size = disk.Size(203, disk.Unit.MiB, sector_size) @@ -185,7 +187,7 @@ def _boot_partition(sector_size: disk.SectorSize) -> disk.PartitionModification: length=size, mountpoint=Path('/boot'), fs_type=disk.FilesystemType.Fat32, - flags=[disk.PartitionFlag.Boot] + flags=flags ) -- cgit v1.2.3-70-g09d2 From dc69acd4b43931f9fd3a267d78834d1a38fbb10f Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Mon, 9 Oct 2023 06:40:59 -0400 Subject: Fix keyboard layout and timezone menus (#2153) --- archinstall/lib/interactions/general_conf.py | 4 ++-- archinstall/lib/locale/locale_menu.py | 2 +- archinstall/lib/locale/utils.py | 23 ++++++++++------------- 3 files changed, 13 insertions(+), 16 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 56598e25..a23426d0 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -44,7 +44,7 @@ def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: choice = Menu( _('Select a timezone'), - list(timezones), + timezones, preset_values=preset, default_option=default ).run() @@ -95,7 +95,7 @@ def select_language(preset: Optional[str] = None) -> Optional[str]: """ kb_lang = list_keyboard_languages() # sort alphabetically and then by length - sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) + sorted_kb_lang = sorted(kb_lang, key=lambda x: (len(x), x)) choice = Menu( _('Select keyboard layout'), diff --git a/archinstall/lib/locale/locale_menu.py b/archinstall/lib/locale/locale_menu.py index 729b3b6e..75cc1332 100644 --- a/archinstall/lib/locale/locale_menu.py +++ b/archinstall/lib/locale/locale_menu.py @@ -139,7 +139,7 @@ def select_kb_layout(preset: Optional[str] = None) -> Optional[str]: """ kb_lang = list_keyboard_languages() # sort alphabetically and then by length - sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) + sorted_kb_lang = sorted(kb_lang, key=lambda x: (len(x), x)) choice = Menu( _('Select keyboard layout'), diff --git a/archinstall/lib/locale/utils.py b/archinstall/lib/locale/utils.py index 330ca0ce..d7641d50 100644 --- a/archinstall/lib/locale/utils.py +++ b/archinstall/lib/locale/utils.py @@ -1,16 +1,15 @@ -from typing import Iterator, List +from typing import List from ..exceptions import ServiceException, SysCallError from ..general import SysCommand from ..output import error -def list_keyboard_languages() -> Iterator[str]: - for line in SysCommand( +def list_keyboard_languages() -> List[str]: + return SysCommand( "localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'} - ).decode(): - yield line + ).decode().splitlines() def list_locales() -> List[str]: @@ -24,12 +23,11 @@ def list_locales() -> List[str]: return locales -def list_x11_keyboard_languages() -> Iterator[str]: - for line in SysCommand( +def list_x11_keyboard_languages() -> List[str]: + return SysCommand( "localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'} - ).decode(): - yield line + ).decode().splitlines() def verify_keyboard_layout(layout :str) -> bool: @@ -62,9 +60,8 @@ def set_kb_layout(locale :str) -> bool: return False -def list_timezones() -> Iterator[str]: - for line in SysCommand( +def list_timezones() -> List[str]: + return SysCommand( "timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'} - ).decode(): - yield line + ).decode().splitlines() -- cgit v1.2.3-70-g09d2 From 5e59acf937c3bb9cfe6a3b7a0a264b9df00239ee Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Tue, 10 Oct 2023 04:00:22 -0400 Subject: Add handling of signal interrupt and EOF at input prompts (#2154) --- archinstall/lib/configuration.py | 2 +- archinstall/lib/interactions/disk_conf.py | 5 ++++- archinstall/lib/interactions/general_conf.py | 15 ++++++++------- archinstall/lib/interactions/manage_users_conf.py | 6 +++++- archinstall/lib/interactions/utils.py | 7 ++++++- archinstall/lib/menu/text_input.py | 11 ++++++++++- 6 files changed, 34 insertions(+), 12 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index aeeddbb8..95e237d7 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -177,5 +177,5 @@ def save_config(config: Dict): case "all": config_output.save(dest_path) - except KeyboardInterrupt: + except (KeyboardInterrupt, EOFError): return diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 84a3196c..c18119ec 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -134,7 +134,10 @@ def select_disk_config( output = "You will use whatever drive-setup is mounted at the specified directory\n" output += "WARNING: Archinstall won't check the suitability of this setup\n" - path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output) + try: + path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output) + except (KeyboardInterrupt, EOFError): + return preset mods = disk.device_handler.detect_pre_mounted_mods(path) return disk.DiskLayoutConfiguration( diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index a23426d0..b12a6fb8 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -28,14 +28,15 @@ def ask_ntp(preset: bool = True) -> bool: def ask_hostname(preset: str = '') -> str: - while True: - hostname = TextInput( - str(_('Desired hostname for the installation: ')), - preset - ).run().strip() + hostname = TextInput( + str(_('Desired hostname for the installation: ')), + preset + ).run().strip() + + if not hostname: + return preset - if hostname: - return hostname + return hostname def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: diff --git a/archinstall/lib/interactions/manage_users_conf.py b/archinstall/lib/interactions/manage_users_conf.py index 879578da..ca912283 100644 --- a/archinstall/lib/interactions/manage_users_conf.py +++ b/archinstall/lib/interactions/manage_users_conf.py @@ -75,7 +75,11 @@ class UserList(ListManager): prompt = '\n\n' + str(_('Enter username (leave blank to skip): ')) while True: - username = input(prompt).strip(' ') + try: + username = input(prompt).strip(' ') + except (KeyboardInterrupt, EOFError): + return None + if not username: return None if not self._check_for_correct_username(username): diff --git a/archinstall/lib/interactions/utils.py b/archinstall/lib/interactions/utils.py index f6b5b2d3..fdbb4625 100644 --- a/archinstall/lib/interactions/utils.py +++ b/archinstall/lib/interactions/utils.py @@ -17,7 +17,12 @@ def get_password(prompt: str = '') -> Optional[str]: if not prompt: prompt = _("Enter a password: ") - while password := getpass.getpass(prompt): + while True: + try: + password = getpass.getpass(prompt) + except (KeyboardInterrupt, EOFError): + break + if len(password.strip()) <= 0: break diff --git a/archinstall/lib/menu/text_input.py b/archinstall/lib/menu/text_input.py index 05ca0f22..971df5fd 100644 --- a/archinstall/lib/menu/text_input.py +++ b/archinstall/lib/menu/text_input.py @@ -1,4 +1,5 @@ import readline +import sys class TextInput: @@ -12,6 +13,14 @@ class TextInput: def run(self) -> str: readline.set_pre_input_hook(self._hook) - result = input(self._prompt) + try: + result = input(self._prompt) + except (KeyboardInterrupt, EOFError): + # To make sure any output that may follow + # will be on the line after the prompt + sys.stdout.write('\n') + sys.stdout.flush() + + result = '' readline.set_pre_input_hook() return result -- cgit v1.2.3-70-g09d2 From 07b0bb18351c5fec332fc27808f9c51996acbae1 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Sun, 15 Oct 2023 03:26:34 -0400 Subject: Fix `MOUNT_POINT` for pre-mounted disk configuration (#2168) --- archinstall/lib/interactions/disk_conf.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index c18119ec..bf24a22c 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -11,6 +11,7 @@ from ..menu import TableMenu from ..menu.menu import MenuSelectionType from ..output import FormattedOutput, debug from ..utils.util import prompt_dir +from ..storage import storage if TYPE_CHECKING: _: Any @@ -140,6 +141,8 @@ def select_disk_config( return preset mods = disk.device_handler.detect_pre_mounted_mods(path) + storage['MOUNT_POINT'] = Path(path) + return disk.DiskLayoutConfiguration( config_type=disk.DiskLayoutType.Pre_mount, device_modifications=mods -- cgit v1.2.3-70-g09d2 From bc3b3a35e6408144587f8c2ace95c4ac68d53bcc Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Tue, 17 Oct 2023 05:23:09 -0400 Subject: Add support for unified kernel image (#1519) --- archinstall/lib/global_menu.py | 10 ++ archinstall/lib/installer.py | 166 ++++++++++++++++++++-------- archinstall/lib/interactions/__init__.py | 2 +- archinstall/lib/interactions/system_conf.py | 16 +++ archinstall/scripts/guided.py | 9 +- schema.json | 4 + 6 files changed, 160 insertions(+), 47 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 86c341a7..e4aa1235 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -4,6 +4,7 @@ from typing import Any, List, Optional, Dict, TYPE_CHECKING from . import disk from .general import secret +from .hardware import SysInfo from .locale.locale_menu import LocaleConfiguration, LocaleMenu from .menu import Selector, AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu @@ -20,6 +21,7 @@ from .interactions import ask_additional_packages_to_install from .interactions import ask_for_additional_users from .interactions import ask_for_audio_selection from .interactions import ask_for_bootloader +from .interactions import ask_for_uki from .interactions import ask_for_swap from .interactions import ask_hostname from .interactions import ask_to_configure_network @@ -85,6 +87,11 @@ class GlobalMenu(AbstractMenu): lambda preset: ask_for_bootloader(preset), display_func=lambda x: x.value, default=Bootloader.get_default()) + self._menu_options['uki'] = \ + Selector( + _('Unified kernel images'), + lambda preset: ask_for_uki(preset), + default=False) self._menu_options['hostname'] = \ Selector( _('Hostname'), @@ -216,6 +223,9 @@ class GlobalMenu(AbstractMenu): self._menu_options['install'].update_description(text) def post_callback(self, name: Optional[str] = None, value: Any = None): + if not SysInfo.has_uefi(): + self._menu_options['uki'].set_enabled(False) + self._update_install_text(name, value) def _install_text(self): diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 39298204..4d6c65b3 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -542,18 +542,13 @@ class Installer: return True - def mkinitcpio(self, flags: List[str], locale_config: LocaleConfiguration) -> bool: + def mkinitcpio(self, flags: List[str]) -> bool: for plugin in plugins.values(): if hasattr(plugin, 'on_mkinitcpio'): # Allow plugins to override the usage of mkinitcpio altogether. if plugin.on_mkinitcpio(self): return True - # mkinitcpio will error out if there's no vconsole. - if (vconsole := Path(f"{self.target}/etc/vconsole.conf")).exists() is False: - with vconsole.open('w') as fh: - fh.write(f"KEYMAP={locale_config.kb_layout}\n") - with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit: mkinit.write(f"MODULES=({' '.join(self.modules)})\n") mkinit.write(f"BINARIES=({' '.join(self._binaries)})\n") @@ -587,6 +582,7 @@ class Installer: self, testing: bool = False, multilib: bool = False, + mkinitcpio: bool = True, hostname: str = 'archinstall', locale_config: LocaleConfiguration = LocaleConfiguration.default() ): @@ -674,7 +670,7 @@ class Installer: # TODO: Use python functions for this SysCommand(f'/usr/bin/arch-chroot {self.target} chmod 700 /root') - if not self.mkinitcpio(['-P'], locale_config): + if mkinitcpio and not self.mkinitcpio(['-P']): error(f"Error generating initramfs (continuing anyway)") self.helper_flags['base'] = True @@ -783,7 +779,8 @@ class Installer: self, boot_partition: disk.PartitionModification, root_partition: disk.PartitionModification, - efi_partition: Optional[disk.PartitionModification] + efi_partition: Optional[disk.PartitionModification], + uki_enabled: bool = False ): self.pacman.strap('efibootmgr') @@ -815,11 +812,18 @@ class Installer: loader_dir = self.target / 'boot/loader' loader_dir.mkdir(parents=True, exist_ok=True) + default_kernel = self.kernels[0] + if uki_enabled: + default_entry = f'arch-{default_kernel}.efi' + else: + entry_name = self.init_time + '_{kernel}{variant}.conf' + default_entry = entry_name.format(kernel=default_kernel, variant='') + + default = f'default {default_entry}' + # Modify or create a loader.conf loader_conf = loader_dir / 'loader.conf' - default = f'default {self.init_time}_{self.kernels[0]}.conf' - try: loader_data = loader_conf.read_text().splitlines() except FileNotFoundError: @@ -837,6 +841,9 @@ class Installer: loader_conf.write_text('\n'.join(loader_data) + '\n') + if uki_enabled: + return + # Ensure that the $BOOT/loader/entries/ directory exists before we try to create files in it entries_dir = loader_dir / 'entries' entries_dir.mkdir(parents=True, exist_ok=True) @@ -867,7 +874,8 @@ class Installer: options, ] - entry_conf = entries_dir / f'{self.init_time}_{kernel}{variant}.conf' + name = entry_name.format(kernel=kernel, variant=variant) + entry_conf = entries_dir / name entry_conf.write_text('\n'.join(entry) + '\n') self.helper_flags['bootloader'] = 'systemd' @@ -876,17 +884,19 @@ class Installer: self, boot_partition: disk.PartitionModification, root_partition: disk.PartitionModification, - efi_partition: Optional[disk.PartitionModification] + efi_partition: Optional[disk.PartitionModification], + uki_enabled: bool = False ): self.pacman.strap('grub') # no need? - grub_default = self.target / 'etc/default/grub' - config = grub_default.read_text() + if not uki_enabled: + grub_default = self.target / 'etc/default/grub' + config = grub_default.read_text() - kernel_parameters = ' '.join(self._get_kernel_params(root_partition, False, False)) - config = re.sub(r'(GRUB_CMDLINE_LINUX=")("\n)', rf'\1{kernel_parameters}\2', config, 1) + kernel_parameters = ' '.join(self._get_kernel_params(root_partition, False, False)) + config = re.sub(r'(GRUB_CMDLINE_LINUX=")("\n)', rf'\1{kernel_parameters}\2', config, 1) - grub_default.write_text(config) + grub_default.write_text(config) info(f"GRUB boot partition: {boot_partition.dev_path}") @@ -1067,7 +1077,8 @@ TIMEOUT=5 def _add_efistub_bootloader( self, boot_partition: disk.PartitionModification, - root_partition: disk.PartitionModification + root_partition: disk.PartitionModification, + uki_enabled: bool = False ): self.pacman.strap('efibootmgr') @@ -1078,41 +1089,103 @@ TIMEOUT=5 # points towards the same disk and/or partition. # And in which case we should do some clean up. - microcode = [] + if not uki_enabled: + loader = '/vmlinuz-{kernel}' - if ucode := self._get_microcode(): - microcode.append(f'initrd=\\{ucode}') - else: - debug('Archinstall will not add any ucode to firmware boot entry.') + microcode = [] + + if ucode := self._get_microcode(): + microcode.append(f'initrd=/{ucode}') + else: + debug('Archinstall will not add any ucode to firmware boot entry.') - kernel_parameters = self._get_kernel_params(root_partition) + entries = ( + *microcode, + 'initrd=/initramfs-{kernel}.img', + *self._get_kernel_params(root_partition) + ) + + cmdline = tuple(' '.join(entries)) + else: + loader = '/EFI/Linux/arch-{kernel}.efi' + cmdline = tuple() parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path) + cmd_template = ( + 'efibootmgr', + '--create', + '--disk', str(parent_dev_path), + '--part', str(boot_partition.partn), + '--label', 'Arch Linux ({kernel})', + '--loader', loader, + '--unicode', *cmdline, + '--verbose' + ) + for kernel in self.kernels: # Setup the firmware entry - cmdline = [ - *microcode, - f"initrd=\\initramfs-{kernel}.img", - *kernel_parameters, - ] - - cmd = [ - 'efibootmgr', - '--disk', str(parent_dev_path), - '--part', str(boot_partition.partn), - '--create', - '--label', f'Arch Linux ({kernel})', - '--loader', f"/vmlinuz-{kernel}", - '--unicode', ' '.join(cmdline), - '--verbose' - ] - + cmd = [arg.format(kernel=kernel) for arg in cmd_template] SysCommand(cmd) self.helper_flags['bootloader'] = "efistub" - def add_bootloader(self, bootloader: Bootloader): + def _config_uki( + self, + root_partition: disk.PartitionModification, + efi_partition: Optional[disk.PartitionModification] + ): + if not efi_partition or not efi_partition.mountpoint: + raise ValueError(f'Could not detect ESP at mountpoint {self.target}') + + # Set up kernel command line + with open(self.target / 'etc/kernel/cmdline', 'w') as cmdline: + kernel_parameters = self._get_kernel_params(root_partition) + cmdline.write(' '.join(kernel_parameters) + '\n') + + ucode = self._get_microcode() + + esp = efi_partition.mountpoint + + diff_mountpoint = None + if esp != Path('/efi'): + diff_mountpoint = str(esp) + + image_re = re.compile('(.+_image="/([^"]+).+\n)') + uki_re = re.compile('#((.+_uki=")/[^/]+(.+\n))') + + # Modify .preset files + for kernel in self.kernels: + preset = self.target / 'etc/mkinitcpio.d' / (kernel + '.preset') + config = preset.read_text().splitlines(True) + + for index, line in enumerate(config): + if not ucode and line.startswith('ALL_microcode='): + config[index] = '#' + line + # Avoid storing redundant image file + elif m := image_re.match(line): + image = self.target / m.group(2) + image.unlink(missing_ok=True) + config[index] = '#' + m.group(1) + elif m := uki_re.match(line): + if diff_mountpoint: + config[index] = m.group(2) + diff_mountpoint + m.group(3) + else: + config[index] = m.group(1) + elif line.startswith('#default_options='): + config[index] = line.removeprefix('#') + + preset.write_text(''.join(config)) + + # Directory for the UKIs + uki_dir = self.target / esp.relative_to(Path('/')) / 'EFI/Linux' + uki_dir.mkdir(parents=True, exist_ok=True) + + # Build the UKIs + if not self.mkinitcpio(['-P']): + error(f"Error generating initramfs (continuing anyway)") + + def add_bootloader(self, bootloader: Bootloader, uki_enabled: bool = False): """ Adds a bootloader to the installation instance. Archinstall supports one of three types: @@ -1143,13 +1216,16 @@ TIMEOUT=5 info(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}') + if uki_enabled: + self._config_uki(root_partition, efi_partition) + match bootloader: case Bootloader.Systemd: - self._add_systemd_bootloader(boot_partition, root_partition, efi_partition) + self._add_systemd_bootloader(boot_partition, root_partition, efi_partition, uki_enabled) case Bootloader.Grub: - self._add_grub_bootloader(boot_partition, root_partition, efi_partition) + self._add_grub_bootloader(boot_partition, root_partition, efi_partition, uki_enabled) case Bootloader.Efistub: - self._add_efistub_bootloader(boot_partition, root_partition) + self._add_efistub_bootloader(boot_partition, root_partition, uki_enabled) case Bootloader.Limine: self._add_limine_bootloader(boot_partition, root_partition) diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 50c0012d..4b696a78 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -15,5 +15,5 @@ from .general_conf import ( ) from .system_conf import ( - select_kernel, ask_for_bootloader, select_driver, ask_for_swap + select_kernel, ask_for_bootloader, ask_for_uki, select_driver, ask_for_swap ) diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py index 0e5e0f1e..aa72748e 100644 --- a/archinstall/lib/interactions/system_conf.py +++ b/archinstall/lib/interactions/system_conf.py @@ -65,6 +65,22 @@ def ask_for_bootloader(preset: Bootloader) -> Bootloader: return preset +def ask_for_uki(preset: bool = True) -> bool: + if preset: + preset_val = Menu.yes() + else: + preset_val = Menu.no() + + prompt = _('Would you like to use unified kernel images?') + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), preset_values=preset_val).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True + + return preset + + def select_driver(options: List[GfxDriver] = [], current_value: Optional[GfxDriver] = None) -> Optional[GfxDriver]: """ Some what convoluted function, whose job is simple. diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index d7cf16cd..fdf05c99 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -56,6 +56,8 @@ def ask_user_questions(): # Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB) global_menu.enable('bootloader') + global_menu.enable('uki') + global_menu.enable('swap') # Get the hostname for the machine @@ -111,6 +113,7 @@ def perform_installation(mountpoint: Path): # Retrieve list of additional repositories and set boolean values appropriately enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) + run_mkinitcpio = not archinstall.arguments.get('uki') locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config'] disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) @@ -141,6 +144,7 @@ def perform_installation(mountpoint: Path): installation.minimal_installation( testing=enable_testing, multilib=enable_multilib, + mkinitcpio=run_mkinitcpio, hostname=archinstall.arguments.get('hostname', 'archlinux'), locale_config=locale_config ) @@ -154,7 +158,10 @@ def perform_installation(mountpoint: Path): if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages("grub") - installation.add_bootloader(archinstall.arguments["bootloader"]) + installation.add_bootloader( + archinstall.arguments["bootloader"], + archinstall.arguments["uki"] + ) # If user selected to copy the current ISO network configuration # Perform a copy of the config diff --git a/schema.json b/schema.json index 5616ed41..b1d45f64 100644 --- a/schema.json +++ b/schema.json @@ -35,6 +35,10 @@ "efistub" ] }, + "uki": { + "description": "Set to true to use a unified kernel images", + "type": "boolean" + }, "custom-commands": { "description": "Custom commands to be run post install", "type": "array", -- cgit v1.2.3-70-g09d2 From 30a374a65b84c2d7dfbb13a4643fb27f31bc71e2 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Mon, 20 Nov 2023 06:54:04 -0500 Subject: Fix parsing pre-mounted disk configuration from configuration file (#2221) --- archinstall/lib/disk/device_model.py | 31 +++++++++++++++++++++++++++---- archinstall/lib/interactions/disk_conf.py | 3 ++- 2 files changed, 29 insertions(+), 5 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 15e68116..54b4932b 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -42,12 +42,20 @@ class DiskLayoutType(Enum): class DiskLayoutConfiguration: config_type: DiskLayoutType device_modifications: List[DeviceModification] = field(default_factory=list) + # used for pre-mounted config + mountpoint: Optional[Path] = None def json(self) -> Dict[str, Any]: - return { - 'config_type': self.config_type.value, - 'device_modifications': [mod.json() for mod in self.device_modifications] - } + if self.config_type == DiskLayoutType.Pre_mount: + return { + 'config_type': self.config_type.value, + 'mountpoint': str(self.mountpoint) + } + else: + return { + 'config_type': self.config_type.value, + 'device_modifications': [mod.json() for mod in self.device_modifications] + } @classmethod def parse_arg(cls, disk_config: Dict[str, List[Dict[str, Any]]]) -> Optional[DiskLayoutConfiguration]: @@ -64,6 +72,21 @@ class DiskLayoutConfiguration: device_modifications=device_modifications ) + if config_type == DiskLayoutType.Pre_mount.value: + if not (mountpoint := disk_config.get('mountpoint')): + raise ValueError('Must set a mountpoint when layout type is pre-mount') + + path = Path(str(mountpoint)) + + mods = device_handler.detect_pre_mounted_mods(path) + device_modifications.extend(mods) + + storage['MOUNT_POINT'] = path + + config.mountpoint = path + + return config + for entry in disk_config.get('device_modifications', []): device_path = Path(entry.get('device', None)) if entry.get('device', None) else None diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index bf24a22c..8e9643df 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -145,7 +145,8 @@ def select_disk_config( return disk.DiskLayoutConfiguration( config_type=disk.DiskLayoutType.Pre_mount, - device_modifications=mods + device_modifications=mods, + mountpoint=path ) preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] -- cgit v1.2.3-70-g09d2 From 6ee6d1eda05d3f69f6aaabac55a741e693449994 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Mon, 20 Nov 2023 06:55:45 -0500 Subject: Remove `select_language()` duplicate of `select_kb_layout()` (#2151) * Remove `select_language()` duplicate of `select_kb_layout()` * Added a deprecation warning on select_language() * Moved select_language() back into it's original location, just to keep the PR diff minimal * Removed import for now, to please flake8 --------- Co-authored-by: Anton Hvornum --- archinstall/lib/interactions/general_conf.py | 29 ++++++++-------------------- 1 file changed, 8 insertions(+), 21 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index b12a6fb8..a879552e 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -3,7 +3,7 @@ from __future__ import annotations import pathlib from typing import List, Any, Optional, TYPE_CHECKING -from ..locale import list_timezones, list_keyboard_languages +from ..locale import list_timezones from ..menu import MenuSelectionType, Menu, TextInput from ..models.audio_configuration import Audio, AudioConfiguration from ..output import warn @@ -87,29 +87,16 @@ def ask_for_audio_selection( def select_language(preset: Optional[str] = None) -> Optional[str]: - """ - Asks the user to select a language - Usually this is combined with :ref:`archinstall.list_keyboard_languages`. - - :return: The language/dictionary key of the selected language - :rtype: str - """ - kb_lang = list_keyboard_languages() - # sort alphabetically and then by length - sorted_kb_lang = sorted(kb_lang, key=lambda x: (len(x), x)) + from ..locale.locale_menu import select_kb_layout - choice = Menu( - _('Select keyboard layout'), - sorted_kb_lang, - preset_values=preset, - sort=False - ).run() + # We'll raise an exception in an upcoming version. + # from ..exceptions import Deprecated + # raise Deprecated("select_language() has been deprecated, use select_kb_layout() instead.") - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.single_value + # No need to translate this i feel, as it's a short lived message. + warn("select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version") - return None + return select_kb_layout(preset) def select_archinstall_language(languages: List[Language], preset: Language) -> Language: -- cgit v1.2.3-70-g09d2 From f16af43949085b06478d2e4c45ed61fa8e595171 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Mon, 20 Nov 2023 06:58:09 -0500 Subject: Fix GPT end alignment (#2210) --- archinstall/lib/interactions/disk_conf.py | 61 ++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 22 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 8e9643df..85b377b7 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -176,9 +176,9 @@ def select_disk_config( return None -def _boot_partition(sector_size: disk.SectorSize) -> disk.PartitionModification: +def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.PartitionModification: flags = [disk.PartitionFlag.Boot] - if SysInfo.has_uefi(): + if using_gpt: start = disk.Size(1, disk.Unit.MiB, sector_size) size = disk.Size(512, disk.Unit.MiB, sector_size) flags.append(disk.PartitionFlag.ESP) @@ -242,6 +242,8 @@ def suggest_single_disk_layout( device_modification = disk.DeviceModification(device, wipe=True) + using_gpt = SysInfo.has_uefi() + # Used for reference: https://wiki.archlinux.org/title/partitioning # 2 MiB is unallocated for GRUB on BIOS. Potentially unneeded for other bootloaders? @@ -253,7 +255,7 @@ def suggest_single_disk_layout( # Also re-align the start to 1MiB since we don't need the first sectors # like we do in MBR layouts where the boot loader is installed traditionally. - boot_partition = _boot_partition(sector_size) + boot_partition = _boot_partition(sector_size, using_gpt) device_modification.add_partition(boot_partition) if not using_subvolumes: @@ -267,20 +269,25 @@ def suggest_single_disk_layout( else: using_home_partition = False + align_buffer = disk.Size(1, disk.Unit.MiB, sector_size) + # root partition - start = disk.Size(513, disk.Unit.MiB, sector_size) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB, sector_size) + root_start = boot_partition.start + boot_partition.length # Set a size for / (/root) if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: - length = device.device_info.total_size - start + root_length = device.device_info.total_size - root_start else: - length = min(device.device_info.total_size, root_partition_size) + root_length = min(device.device_info.total_size, root_partition_size) + + if using_gpt and not using_home_partition: + root_length -= align_buffer root_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=start, - length=length, + start=root_start, + length=root_length, mountpoint=Path('/') if not using_subvolumes else None, fs_type=filesystem_type, mount_options=['compress=zstd'] if compression else [], @@ -303,14 +310,17 @@ def suggest_single_disk_layout( # If we don't want to use subvolumes, # But we want to be able to re-use data between re-installs.. # A second partition for /home would be nice if we have the space for it - start = root_partition.length - length = device.device_info.total_size - root_partition.length + home_start = root_partition.length + home_length = device.device_info.total_size - root_partition.length + + if using_gpt: + home_length -= align_buffer home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=start, - length=length, + start=home_start, + length=home_length, mountpoint=Path('/home'), fs_type=filesystem_type, mount_options=['compress=zstd'] if compression else [] @@ -377,17 +387,21 @@ def suggest_multi_disk_layout( root_device_sector_size = root_device_modification.device.device_info.sector_size home_device_sector_size = home_device_modification.device.device_info.sector_size + root_align_buffer = disk.Size(1, disk.Unit.MiB, root_device_sector_size) + home_align_buffer = disk.Size(1, disk.Unit.MiB, home_device_sector_size) + + using_gpt = SysInfo.has_uefi() + # add boot partition to the root device - boot_partition = _boot_partition(root_device_sector_size) + boot_partition = _boot_partition(root_device_sector_size, using_gpt) root_device_modification.add_partition(boot_partition) - if SysInfo.has_uefi(): - root_start = disk.Size(513, disk.Unit.MiB, root_device_sector_size) - else: - root_start = disk.Size(206, disk.Unit.MiB, root_device_sector_size) - + root_start = boot_partition.start + boot_partition.length root_length = root_device.device_info.total_size - root_start + if using_gpt: + root_length -= root_align_buffer + # add root partition to the root device root_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, @@ -400,15 +414,18 @@ def suggest_multi_disk_layout( ) root_device_modification.add_partition(root_partition) - start = disk.Size(1, disk.Unit.MiB, home_device_sector_size) - length = home_device.device_info.total_size - start + home_start = home_align_buffer + home_length = home_device.device_info.total_size - home_start + + if using_gpt: + home_length -= home_align_buffer # add home partition to home device home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=start, - length=length, + start=home_start, + length=home_length, mountpoint=Path('/home'), mount_options=['compress=zstd'] if compression else [], fs_type=filesystem_type, -- cgit v1.2.3-70-g09d2 From 0d5e1cf752010e1c2d068077bbd55ae25d3d0bd7 Mon Sep 17 00:00:00 2001 From: Rafael Fontenelle Date: Thu, 7 Mar 2024 09:19:44 -0300 Subject: Fix misspellings (#2306) --- archinstall/__init__.py | 2 +- archinstall/lib/installer.py | 2 +- archinstall/lib/interactions/disk_conf.py | 2 +- archinstall/lib/menu/abstract_menu.py | 2 +- docs/cli_parameters/config/config_options.csv | 8 ++++---- docs/cli_parameters/config/disk_config.rst | 4 ++-- docs/examples/python.rst | 4 ++-- docs/help/known_issues.rst | 6 +++--- docs/installing/guided.rst | 4 ++-- 9 files changed, 17 insertions(+), 17 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index efb73710..78798441 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -84,7 +84,7 @@ def define_arguments(): parser.add_argument("--script", default="guided", nargs="?", help="Script to run for installation", type=str) parser.add_argument("--mount-point", "--mount_point", nargs="?", type=str, help="Define an alternate mount point for installation") - parser.add_argument("--skip-ntp", action="store_true", help="Disables NTP checks during instalation", default=False) + parser.add_argument("--skip-ntp", action="store_true", help="Disables NTP checks during installation", default=False) parser.add_argument("--debug", action="store_true", default=False, help="Adds debug info into the log") parser.add_argument("--offline", action="store_true", default=False, help="Disabled online upstream services such as package search and key-ring auto update.") diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index ccae8faa..2ea728bb 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -148,7 +148,7 @@ class Installer: if not _notified and time.time() - _started_wait > 5: _notified = True warn( - _("Time syncronization not completing, while you wait - check the docs for workarounds: https://archinstall.readthedocs.io/")) + _("Time synchronization not completing, while you wait - check the docs for workarounds: https://archinstall.readthedocs.io/")) time_val = SysCommand('timedatectl show --property=NTPSynchronized --value').decode() if time_val and time_val.strip() == 'yes': diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 85b377b7..bbd8957d 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -308,7 +308,7 @@ def suggest_single_disk_layout( root_partition.btrfs_subvols = subvolumes elif using_home_partition: # If we don't want to use subvolumes, - # But we want to be able to re-use data between re-installs.. + # But we want to be able to reuse data between re-installs.. # A second partition for /home would be nice if we have the space for it home_start = root_partition.length home_length = device.device_info.total_size - root_partition.length diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 2ceb6ca7..14db98ca 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -344,7 +344,7 @@ class AbstractMenu: value = value.strip() # if this calls returns false, we exit the menu - # we allow for an callback for special processing on realeasing control + # we allow for an callback for special processing on releasing control if not self._process_selection(value): break diff --git a/docs/cli_parameters/config/config_options.csv b/docs/cli_parameters/config/config_options.csv index 1861b1e1..7cf10982 100644 --- a/docs/cli_parameters/config/config_options.csv +++ b/docs/cli_parameters/config/config_options.csv @@ -5,8 +5,8 @@ audio_config,`pipewire `_!, `pulseaud bootloader,`Systemd-boot `_!, `grub `_,Bootloader to be installed *(grub being mandatory on BIOS machines)*,Yes debug,``true``!, ``false``,Enables debug output,No disk_config,*Read more under* :ref:`disk config`,Contains the desired disk setup to be used during installation,No -disk_encryption,*Read more about under* :ref:`disk encryption`,Parameters for disk encryption applied ontop of ``disk_config``,No -hostname,``str``,A string definining your machines hostname on the network *(defaults to ``archinstall``)*,No +disk_encryption,*Read more about under* :ref:`disk encryption`,Parameters for disk encryption applied on top of ``disk_config``,No +hostname,``str``,A string defining your machines hostname on the network *(defaults to ``archinstall``)*,No 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 @@ -16,9 +16,9 @@ no_pkg_lookups,``true``!, ``false``,Disabled package checking against https://ar ntp,``true``!, ``false``,enables or disables `NTP `_ during installation,No offline,``true``!, ``false``,enables or disables certain online checks such as mirror reachability etc,No packages,[ !, !, ... ],A list of packages to install during installation,No -parallel downloads,0-∞,sets a given number of paralell downloads to be used by `pacman `_,No +parallel downloads,0-∞,sets a given number of parallel downloads to be used by `pacman `_,No profile_config,*`read more under the profiles section`*,Installs a given profile if defined,No script,`guided `__! *(default)*!, `minimal `__!, `only_hdd `_!, `swiss `_!, `unattended `_,When used to autorun an installation!, this sets which script to autorun with,No silent,``true``!, ``false``,disables or enables user questions using the TUI,No swap,``true``!, ``false``,enables or disables swap,No -timezone,`timezone `_,sets a timezone for the installed system,No \ No newline at end of file +timezone,`timezone `_,sets a timezone for the installed system,No diff --git a/docs/cli_parameters/config/disk_config.rst b/docs/cli_parameters/config/disk_config.rst index ed5f42c1..3dc01fb2 100644 --- a/docs/cli_parameters/config/disk_config.rst +++ b/docs/cli_parameters/config/disk_config.rst @@ -26,7 +26,7 @@ Given the following disk example: ├── boot (/dev/sda1) └── home (/dev/sda3) -Runing ``archinstall --conf your.json --silent`` where the above JSON is configured. The disk will be left alone — and a working system will be installed to the above folders and mountpoints will be translated into the installed system. +Running ``archinstall --conf your.json --silent`` where the above JSON is configured. The disk will be left alone — and a working system will be installed to the above folders and mountpoints will be translated into the installed system. .. note:: @@ -242,4 +242,4 @@ This example contains both subvolumes and compression. }, "status": "create", "type": "primary" - } \ No newline at end of file + } diff --git a/docs/examples/python.rst b/docs/examples/python.rst index 7fb3f6c3..7226c825 100644 --- a/docs/examples/python.rst +++ b/docs/examples/python.rst @@ -38,7 +38,7 @@ To do this, we'll begin by importing :code:`archinstall` in our "`scripts`_:code print(archinstall.disk.device_handler.devices) Now, go ahead and reference the :ref:`installing.python.manual` installation method. -After runnig ``python -m archinstall test_installer`` it should print something that looks like: +After running ``python -m archinstall test_installer`` it should print something that looks like: .. code-block:: text @@ -93,4 +93,4 @@ That means your script is in the right place, and ``archinstall`` is working as Most calls, including the one above requires `root `_ privileges. -.. _scripts: https://github.com/archlinux/archinstall/tree/master/archinstall/scripts \ No newline at end of file +.. _scripts: https://github.com/archlinux/archinstall/tree/master/archinstall/scripts diff --git a/docs/help/known_issues.rst b/docs/help/known_issues.rst index 425829e5..622356c1 100644 --- a/docs/help/known_issues.rst +++ b/docs/help/known_issues.rst @@ -22,7 +22,7 @@ Waiting for time sync `#2144`_ Missing Nvidia Proprietary Driver `#2002`_ ------------------------------------------ -| In some instances, the nvidia driver might not have all the nessecary packages installed. +| In some instances, the nvidia driver might not have all the necessary packages installed. | This is due to the kernel selection and/or hardware setups requiring additional packages to work properly. A common workaround is to install the package `linux-headers`_ and `nvidia-dkms`_ @@ -49,7 +49,7 @@ Keyring is out of date `#2213`_ | Subsequently the ``archinstall`` run might operate on a old keyring despite there being an update service for this. | There is really no way to reliably over time work around this issue in ``archinstall``. -| Instead, efforts to the upstream service should be considered the way forward. And/or keys not expiring betwene a sane ammount of ISO's. +| Instead, efforts to the upstream service should be considered the way forward. And/or keys not expiring between a sane amount of ISO's. .. note:: @@ -99,4 +99,4 @@ AUR packages .. _archlinux-keyring-wkd-sync.service: https://gitlab.archlinux.org/archlinux/archlinux-keyring/-/blob/7e672dad10652a80d1cc575d75cdb46442cd7f96/wkd_sync/archlinux-keyring-wkd-sync.service.in .. _ZFS: https://aur.archlinux.org/packages/zfs-linux .. _archinstall: https://github.com/archlinux/archinstall/ -.. _timedatectl show: https://github.com/archlinux/archinstall/blob/e6344f93f7e476d05bbcd642f2ed91fdde545870/archinstall/lib/installer.py#L136 \ No newline at end of file +.. _timedatectl show: https://github.com/archlinux/archinstall/blob/e6344f93f7e476d05bbcd642f2ed91fdde545870/archinstall/lib/installer.py#L136 diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index dcedfc10..90abedb4 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -26,7 +26,7 @@ To start the installer, run the following in the latest Arch Linux ISO: archinstall -Since the `Guided Installer`_ is the default script, this is the equvilant of running :code:`archinstall guided` +Since the `Guided Installer`_ is the default script, this is the equivalent of running :code:`archinstall guided` The guided installation also supports installing with pre-configured answers to all the guided steps. This can be a quick and convenient way to re-run one or several installations. @@ -284,4 +284,4 @@ Options for ``--creds`` The key's start with ``!`` because internal log functions will mask any keys starting with explamation from logs and unrestricted configurations. .. _scripts: https://github.com/archlinux/archinstall/tree/master/archinstall/scripts -.. _Guided Installer: https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py \ No newline at end of file +.. _Guided Installer: https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py -- cgit v1.2.3-70-g09d2 From 08a6d402c4792cae95aec196460dc67aadd86f3c Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 8 Mar 2024 00:43:51 +1100 Subject: Fix 2215 | Display installed packages for all profile submenus (#2355) * Display all packages to be installed * Display all packages to be installed --- archinstall/default_profiles/desktops/bspwm.py | 25 --------------------- archinstall/default_profiles/profile.py | 31 ++++++++++++++++++-------- archinstall/default_profiles/xorg.py | 5 ++++- archinstall/lib/hardware.py | 12 +++++++++- archinstall/lib/interactions/system_conf.py | 7 +++--- archinstall/lib/menu/menu.py | 4 +++- archinstall/lib/profile/profile_menu.py | 22 ++++++++++++++++-- archinstall/lib/utils/util.py | 8 +++---- 8 files changed, 68 insertions(+), 46 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/default_profiles/desktops/bspwm.py b/archinstall/default_profiles/desktops/bspwm.py index 2a29f41b..61eeba43 100644 --- a/archinstall/default_profiles/desktops/bspwm.py +++ b/archinstall/default_profiles/desktops/bspwm.py @@ -25,28 +25,3 @@ class BspwmProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() - - # The wiki specified xinit, but we already use greeter? - # https://wiki.archlinux.org/title/Bspwm#Starting - # - # # TODO: check if we selected a greeter, else run this: - # with open(f"{install_session.target}/etc/X11/xinit/xinitrc", 'r') as xinitrc: - # xinitrc_data = xinitrc.read() - - # for line in xinitrc_data.split('\n'): - # if "twm &" in line: - # xinitrc_data = xinitrc_data.replace(line, f"# {line}") - # if "xclock" in line: - # xinitrc_data = xinitrc_data.replace(line, f"# {line}") - # if "xterm" in line: - # xinitrc_data = xinitrc_data.replace(line, f"# {line}") - - # xinitrc_data += '\n' - # xinitrc_data += 'exec bspwn\n' - - # with open(f"{install_session.target}/etc/X11/xinit/xinitrc", 'w') as xinitrc: - # xinitrc.write(xinitrc_data) diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index 49a9c19d..4c85b0c7 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -178,15 +178,28 @@ class Profile: def preview_text(self) -> Optional[str]: """ - Used for preview text in profiles_bck. If a description is set for a - profile it will automatically display that one in the preview. - If no preview or a different text should be displayed just + Override this method to provide a preview text for the profile """ - if self.description: - return self.description - return None + return self.packages_text() - def packages_text(self) -> str: + def packages_text(self, include_sub_packages: bool = False) -> Optional[str]: header = str(_('Installed packages')) - output = format_cols(self.packages, header) - return output + + text = '' + packages = [] + + if self.packages: + packages = self.packages + + if include_sub_packages: + for p in self.current_selection: + if p.packages: + packages += p.packages + + text += format_cols(sorted(set(packages))) + + if text: + text = f'{header}: \n{text}' + return text + + return None diff --git a/archinstall/default_profiles/xorg.py b/archinstall/default_profiles/xorg.py index c9abf4da..88ba55a6 100644 --- a/archinstall/default_profiles/xorg.py +++ b/archinstall/default_profiles/xorg.py @@ -22,7 +22,10 @@ class XorgProfile(Profile): def preview_text(self) -> Optional[str]: text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() + if packages := self.packages_text(): + text += f'\n{packages}' + + return text @property def packages(self) -> List[str]: diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index efdae430..c8001c19 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -2,12 +2,16 @@ import os from enum import Enum from functools import cached_property from pathlib import Path -from typing import Optional, Dict, List +from typing import Optional, Dict, List, TYPE_CHECKING, Any from .exceptions import SysCallError from .general import SysCommand from .networking import list_interfaces, enrich_iface_types from .output import debug +from .utils.util import format_cols + +if TYPE_CHECKING: + _: Any class CpuVendor(Enum): @@ -73,6 +77,12 @@ class GfxDriver(Enum): case _: return False + def packages_text(self) -> str: + text = str(_('Installed packages')) + ':\n' + pkg_names = [p.value for p in self.gfx_packages()] + text += format_cols(sorted(pkg_names)) + return text + def gfx_packages(self) -> List[GfxPackage]: packages = [GfxPackage.XorgServer, GfxPackage.XorgXinit] diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py index aa72748e..35ba5a8b 100644 --- a/archinstall/lib/interactions/system_conf.py +++ b/archinstall/lib/interactions/system_conf.py @@ -103,14 +103,15 @@ def select_driver(options: List[GfxDriver] = [], current_value: Optional[GfxDriv if SysInfo.has_nvidia_graphics(): title += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')) - title += str(_('\nSelect a graphics driver or leave blank to install all open-source drivers')) - preset = current_value.value if current_value else None + choice = Menu( title, drivers, preset_values=preset, - default_option=GfxDriver.AllOpenSource.value + default_option=GfxDriver.AllOpenSource.value, + preview_command=lambda x: GfxDriver(x).packages_text(), + preview_size=0.3 ).run() if choice.type_ != MenuSelectionType.Selection: diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 3bd31b88..f14b855d 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -235,7 +235,9 @@ class Menu(TerminalMenu): if preview_command: if self._default_option is not None and self._default_menu_value == selection: selection = self._default_option - return preview_command(selection) + + if res := preview_command(selection): + return res.rstrip('\n') return None diff --git a/archinstall/lib/profile/profile_menu.py b/archinstall/lib/profile/profile_menu.py index d9e47190..aba75a88 100644 --- a/archinstall/lib/profile/profile_menu.py +++ b/archinstall/lib/profile/profile_menu.py @@ -40,6 +40,7 @@ class ProfileMenu(AbstractSubMenu): lambda preset: self._select_gfx_driver(preset), display_func=lambda x: x.value if x else None, dependencies=['profile'], + preview_func=self._preview_gfx, default=self._preset.gfx_driver if self._preset.profile and self._preset.profile.is_graphic_driver_supported() else None, enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False ) @@ -67,6 +68,7 @@ class ProfileMenu(AbstractSubMenu): def _select_profile(self, preset: Optional[Profile]) -> Optional[Profile]: profile = select_profile(preset) + if profile is not None: if not profile.is_graphic_driver_supported(): self._menu_options['gfx_driver'].set_enabled(False) @@ -105,12 +107,28 @@ class ProfileMenu(AbstractSubMenu): return driver + def _preview_gfx(self) -> Optional[str]: + driver: Optional[GfxDriver] = self._menu_options['gfx_driver'].current_selection + + if driver: + return driver.packages_text() + + return None + def _preview_profile(self) -> Optional[str]: profile: Optional[Profile] = self._menu_options['profile'].current_selection + text = '' if profile: - names = profile.current_selection_names() - return '\n'.join(names) + if (sub_profiles := profile.current_selection) is not None: + text += str(_('Selected profiles: ')) + text += ', '.join([p.name for p in sub_profiles]) + '\n' + + if packages := profile.packages_text(include_sub_packages=True): + text += f'{packages}' + + if text: + return text return None diff --git a/archinstall/lib/utils/util.py b/archinstall/lib/utils/util.py index 8df75ab1..2e42b3cf 100644 --- a/archinstall/lib/utils/util.py +++ b/archinstall/lib/utils/util.py @@ -31,18 +31,18 @@ def is_subpath(first: Path, second: Path): return False -def format_cols(items: List[str], header: Optional[str]) -> str: +def format_cols(items: List[str], header: Optional[str] = None) -> str: if header: text = f'{header}:\n' else: text = '' nr_items = len(items) - if nr_items <= 5: + if nr_items <= 4: col = 1 - elif nr_items <= 10: + elif nr_items <= 8: col = 2 - elif nr_items <= 15: + elif nr_items <= 12: col = 3 else: col = 4 -- cgit v1.2.3-70-g09d2 From fef9269d38335908199e4b94cb3e0252dc504d9c Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Fri, 8 Mar 2024 05:24:35 -0500 Subject: Fix home partition start and length (#2391) --- archinstall/lib/interactions/disk_conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index bbd8957d..b8c6adad 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -310,8 +310,8 @@ def suggest_single_disk_layout( # If we don't want to use subvolumes, # But we want to be able to reuse data between re-installs.. # A second partition for /home would be nice if we have the space for it - home_start = root_partition.length - home_length = device.device_info.total_size - root_partition.length + home_start = root_partition.start + root_partition.length + home_length = device.device_info.total_size - home_start if using_gpt: home_length -= align_buffer -- cgit v1.2.3-70-g09d2 From 1064f74846035afb3ffcf05e49d968d5da6d5521 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Sun, 10 Mar 2024 10:20:13 -0400 Subject: Increase ESP size to 1 GiB (#2401) --- archinstall/lib/interactions/disk_conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index b8c6adad..72a32311 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -180,7 +180,7 @@ def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.Parti flags = [disk.PartitionFlag.Boot] if using_gpt: start = disk.Size(1, disk.Unit.MiB, sector_size) - size = disk.Size(512, disk.Unit.MiB, sector_size) + size = disk.Size(1, disk.Unit.GiB, sector_size) flags.append(disk.PartitionFlag.ESP) else: start = disk.Size(3, disk.Unit.MiB, sector_size) -- cgit v1.2.3-70-g09d2 From c210cdcb8f0883ac13a6ee22aebb8f01f3043e09 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:09:26 -0400 Subject: Fix Btrfs mount options (#2404) --- archinstall/lib/disk/device_handler.py | 26 +++++++-------- archinstall/lib/disk/device_model.py | 36 ++++----------------- archinstall/lib/disk/partitioning_menu.py | 39 +++++++++++++++++----- archinstall/lib/disk/subvolume_menu.py | 27 ++-------------- archinstall/lib/installer.py | 52 +++++++++--------------------- archinstall/lib/interactions/disk_conf.py | 35 +++++++++++++------- docs/cli_parameters/config/disk_config.rst | 10 ------ 7 files changed, 91 insertions(+), 134 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 59ee150d..c06247e6 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -437,9 +437,19 @@ class DeviceHandler(object): if not luks_handler.mapper_dev: raise DiskError('Failed to unlock luks device') - self.mount(luks_handler.mapper_dev, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + self.mount( + luks_handler.mapper_dev, + self._TMP_BTRFS_MOUNT, + create_target_mountpoint=True, + options=part_mod.mount_options + ) else: - self.mount(part_mod.safe_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + self.mount( + part_mod.safe_dev_path, + self._TMP_BTRFS_MOUNT, + create_target_mountpoint=True, + options=part_mod.mount_options + ) for sub_vol in part_mod.btrfs_subvols: debug(f'Creating subvolume: {sub_vol.name}') @@ -451,18 +461,6 @@ class DeviceHandler(object): SysCommand(f"btrfs subvolume create {subvol_path}") - if sub_vol.nodatacow: - try: - SysCommand(f'chattr +C {subvol_path}') - except SysCallError as err: - raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {err}') - - if sub_vol.compress: - try: - SysCommand(f'chattr +c {subvol_path}') - except SysCallError as err: - raise DiskError(f'Could not set compress attribute at {subvol_path}: {err}') - if luks_handler is not None and luks_handler.mapper_dev is not None: self.umount(luks_handler.mapper_dev) luks_handler.lock() diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index d4563faa..423c65e4 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -315,6 +315,11 @@ class Size: return self._normalize() >= other._normalize() +class BtrfsMountOption(Enum): + compress = 'compress=zstd' + nodatacow = 'nodatacow' + + @dataclass class _BtrfsSubvolumeInfo: name: Path @@ -458,8 +463,6 @@ class _DeviceInfo: class SubvolumeModification: name: Path mountpoint: Optional[Path] = None - compress: bool = False - nodatacow: bool = False @classmethod def from_existing_subvol_info(cls, info: _BtrfsSubvolumeInfo) -> SubvolumeModification: @@ -475,30 +478,10 @@ class SubvolumeModification: mountpoint = Path(entry['mountpoint']) if entry['mountpoint'] else None - compress = entry.get('compress', False) - nodatacow = entry.get('nodatacow', False) - - if compress and nodatacow: - raise ValueError('compress and nodatacow flags cannot be enabled simultaneously on a btfrs subvolume') - - mods.append( - SubvolumeModification( - entry['name'], - mountpoint, - compress, - nodatacow - ) - ) + mods.append(SubvolumeModification(entry['name'], mountpoint)) return mods - @property - def mount_options(self) -> List[str]: - options = [] - options += ['compress'] if self.compress else [] - options += ['nodatacow'] if self.nodatacow else [] - return options - @property def relative_mountpoint(self) -> Path: """ @@ -516,12 +499,7 @@ class SubvolumeModification: return False def json(self) -> Dict[str, Any]: - return { - 'name': str(self.name), - 'mountpoint': str(self.mountpoint), - 'compress': self.compress, - 'nodatacow': self.nodatacow - } + return {'name': str(self.name), 'mountpoint': str(self.mountpoint)} def table_data(self) -> Dict[str, Any]: return self.json() diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index a9478158..823605e3 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ - ModificationStatus, DeviceGeometry, SectorSize + ModificationStatus, DeviceGeometry, SectorSize, BtrfsMountOption from ..hardware import SysInfo from ..menu import Menu, ListManager, MenuSelection, TextInput from ..output import FormattedOutput, warn @@ -30,6 +30,7 @@ class PartitioningList(ListManager): 'mark_bootable': str(_('Mark/Unmark as bootable')), 'set_filesystem': str(_('Change filesystem')), 'btrfs_mark_compressed': str(_('Mark/Unmark as compressed')), # btrfs only + 'btrfs_mark_nodatacow': str(_('Mark/Unmark as nodatacow')), # btrfs only 'btrfs_set_subvolumes': str(_('Set subvolumes')), # btrfs only 'delete_partition': str(_('Delete partition')) } @@ -71,12 +72,17 @@ class PartitioningList(ListManager): self._actions['set_filesystem'], self._actions['mark_bootable'], self._actions['btrfs_mark_compressed'], + self._actions['btrfs_mark_nodatacow'], self._actions['btrfs_set_subvolumes'] ] # non btrfs partitions shouldn't get btrfs options if selection.fs_type != FilesystemType.Btrfs: - not_filter += [self._actions['btrfs_mark_compressed'], self._actions['btrfs_set_subvolumes']] + not_filter += [ + self._actions['btrfs_mark_compressed'], + self._actions['btrfs_mark_nodatacow'], + self._actions['btrfs_set_subvolumes'] + ] else: not_filter += [self._actions['assign_mountpoint']] @@ -122,7 +128,9 @@ class PartitioningList(ListManager): if fs_type == FilesystemType.Btrfs: entry.mountpoint = None case 'btrfs_mark_compressed' if entry: - self._set_compressed(entry) + self._toggle_mount_option(entry, BtrfsMountOption.compress) + case 'btrfs_mark_nodatacow' if entry: + self._toggle_mount_option(entry, BtrfsMountOption.nodatacow) case 'btrfs_set_subvolumes' if entry: self._set_btrfs_subvolumes(entry) case 'delete_partition' if entry: @@ -141,13 +149,28 @@ class PartitioningList(ListManager): else: return [d for d in data if d != entry] - def _set_compressed(self, partition: PartitionModification): - compression = 'compress=zstd' + def _toggle_mount_option( + self, + partition: PartitionModification, + option: BtrfsMountOption + ): + if option.value not in partition.mount_options: + if option == BtrfsMountOption.compress: + partition.mount_options = [ + o for o in partition.mount_options + if o != BtrfsMountOption.nodatacow.value + ] + + partition.mount_options = [ + o for o in partition.mount_options + if not o.startswith(BtrfsMountOption.compress.name) + ] - if compression in partition.mount_options: - partition.mount_options = [o for o in partition.mount_options if o != compression] + partition.mount_options.append(option.value) else: - partition.mount_options.append(compression) + partition.mount_options = [ + o for o in partition.mount_options if o != option.value + ] def _set_btrfs_subvolumes(self, partition: PartitionModification): partition.btrfs_subvols = SubvolumeMenu( diff --git a/archinstall/lib/disk/subvolume_menu.py b/archinstall/lib/disk/subvolume_menu.py index 2b70d7b2..48afa829 100644 --- a/archinstall/lib/disk/subvolume_menu.py +++ b/archinstall/lib/disk/subvolume_menu.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Dict, List, Optional, Any, TYPE_CHECKING from .device_model import SubvolumeModification -from ..menu import Menu, TextInput, MenuSelectionType, ListManager +from ..menu import TextInput, ListManager from ..output import FormattedOutput if TYPE_CHECKING: @@ -36,23 +36,6 @@ class SubvolumeMenu(ListManager): def selected_action_display(self, subvolume: SubvolumeModification) -> str: return str(subvolume.name) - def _prompt_options(self, editing: Optional[SubvolumeModification] = None) -> List[str]: - preset_options = [] - if editing: - preset_options = editing.mount_options - - choice = Menu( - str(_("Select the desired subvolume options ")), - ['nodatacow', 'compress'], - skip=True, - preset_values=preset_options, - ).run() - - if choice.type_ == MenuSelectionType.Selection: - return choice.value # type: ignore - - return [] - def _add_subvolume(self, editing: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]: name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run() @@ -64,13 +47,7 @@ class SubvolumeMenu(ListManager): if not mountpoint: return None - options = self._prompt_options(editing) - - subvolume = SubvolumeModification(Path(name), Path(mountpoint)) - subvolume.compress = 'compress' in options - subvolume.nodatacow = 'nodatacow' in options - - return subvolume + return SubvolumeModification(Path(name), Path(mountpoint)) def handle_action( self, diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index d5ea889b..c53e922d 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -240,7 +240,11 @@ class Installer: disk.device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options) if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.dev_path: - self._mount_btrfs_subvol(part_mod.dev_path, part_mod.btrfs_subvols) + self._mount_btrfs_subvol( + part_mod.dev_path, + part_mod.btrfs_subvols, + part_mod.mount_options + ) def _mount_luks_partition(self, part_mod: disk.PartitionModification, luks_handler: Luks2): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined @@ -251,11 +255,18 @@ class Installer: if part_mod.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev: self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols) - def _mount_btrfs_subvol(self, dev_path: Path, subvolumes: List[disk.SubvolumeModification]): + def _mount_btrfs_subvol( + self, + dev_path: Path, + subvolumes: List[disk.SubvolumeModification], + mount_options: List[str] = [] + ): for subvol in subvolumes: - mountpoint = self.target / subvol.relative_mountpoint - mount_options = subvol.mount_options + [f'subvol={subvol.name}'] - disk.device_handler.mount(dev_path, mountpoint, options=mount_options) + disk.device_handler.mount( + dev_path, + self.target / subvol.relative_mountpoint, + options=mount_options + [f'subvol={subvol.name}'] + ) def generate_key_files(self): for part_mod in self._disk_encryption.partitions: @@ -382,37 +393,6 @@ class Installer: for entry in self._fstab_entries: fp.write(f'{entry}\n') - for mod in self._disk_config.device_modifications: - for part_mod in mod.partitions: - if part_mod.fs_type != disk.FilesystemType.Btrfs: - continue - - with fstab_path.open('r') as fp: - fstab = fp.readlines() - - # Replace the {installation}/etc/fstab with entries - # using the compress=zstd where the mountpoint has compression set. - for index, line in enumerate(fstab): - # So first we grab the mount options by using subvol=.*? as a locator. - # And we also grab the mountpoint for the entry, for instance /var/log - subvoldef = re.findall(',.*?subvol=.*?[\t ]', line) - mountpoint = re.findall('[\t ]/.*?[\t ]', line) - - if not subvoldef or not mountpoint: - continue - - for sub_vol in part_mod.btrfs_subvols: - # We then locate the correct subvolume and check if it's compressed, - # and skip entries where compression is already defined - # We then sneak in the compress=zstd option if it doesn't already exist: - if sub_vol.compress and str(sub_vol.mountpoint) == Path( - mountpoint[0].strip()) and ',compress=zstd,' not in line: - fstab[index] = line.replace(subvoldef[0], f',compress=zstd{subvoldef[0]}') - break - - with fstab_path.open('w') as fp: - fp.writelines(fstab) - def set_hostname(self, hostname: str, *args: str, **kwargs: str) -> None: with open(f'{self.target}/etc/hostname', 'w') as fh: fh.write(hostname + '\n') diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 72a32311..9d0042d6 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -5,6 +5,7 @@ from typing import Any, TYPE_CHECKING from typing import Optional, List, Tuple from .. import disk +from ..disk.device_model import BtrfsMountOption from ..hardware import SysInfo from ..menu import Menu from ..menu import TableMenu @@ -214,6 +215,20 @@ def select_main_filesystem_format(advanced_options=False) -> disk.FilesystemType return options[choice.single_value] +def select_mount_options() -> List[str]: + prompt = str(_('Would you like to use compression or disable CoW?')) + options = [str(_('Use compression')), str(_('Disable Copy-on-Write'))] + choice = Menu(prompt, options, sort=False).run() + + if choice.type_ == MenuSelectionType.Selection: + if choice.single_value == options[0]: + return [BtrfsMountOption.compress.value] + else: + return [BtrfsMountOption.nodatacow.value] + + return [] + + def suggest_single_disk_layout( device: disk.BDevice, filesystem_type: Optional[disk.FilesystemType] = None, @@ -228,7 +243,7 @@ def suggest_single_disk_layout( root_partition_size = disk.Size(20, disk.Unit.GiB, sector_size) using_subvolumes = False using_home_partition = False - compression = False + mount_options = [] device_size_gib = device.device_info.total_size if filesystem_type == disk.FilesystemType.Btrfs: @@ -236,9 +251,7 @@ def suggest_single_disk_layout( choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() using_subvolumes = choice.value == Menu.yes() - prompt = str(_('Would you like to use BTRFS compression?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - compression = choice.value == Menu.yes() + mount_options = select_mount_options() device_modification = disk.DeviceModification(device, wipe=True) @@ -290,7 +303,7 @@ def suggest_single_disk_layout( length=root_length, mountpoint=Path('/') if not using_subvolumes else None, fs_type=filesystem_type, - mount_options=['compress=zstd'] if compression else [], + mount_options=mount_options ) device_modification.add_partition(root_partition) @@ -323,7 +336,7 @@ def suggest_single_disk_layout( length=home_length, mountpoint=Path('/home'), fs_type=filesystem_type, - mount_options=['compress=zstd'] if compression else [] + mount_options=mount_options ) device_modification.add_partition(home_partition) @@ -344,7 +357,7 @@ def suggest_multi_disk_layout( min_home_partition_size = disk.Size(40, disk.Unit.GiB, disk.SectorSize.default()) # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? desired_root_partition_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default()) - compression = False + mount_options = [] if not filesystem_type: filesystem_type = select_main_filesystem_format(advanced_options) @@ -371,9 +384,7 @@ def suggest_multi_disk_layout( return [] if filesystem_type == disk.FilesystemType.Btrfs: - prompt = str(_('Would you like to use BTRFS compression?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - compression = choice.value == Menu.yes() + mount_options = select_mount_options() device_paths = ', '.join([str(d.device_info.path) for d in devices]) @@ -409,7 +420,7 @@ def suggest_multi_disk_layout( start=root_start, length=root_length, mountpoint=Path('/'), - mount_options=['compress=zstd'] if compression else [], + mount_options=mount_options, fs_type=filesystem_type ) root_device_modification.add_partition(root_partition) @@ -427,7 +438,7 @@ def suggest_multi_disk_layout( start=home_start, length=home_length, mountpoint=Path('/home'), - mount_options=['compress=zstd'] if compression else [], + mount_options=mount_options, fs_type=filesystem_type, ) home_device_modification.add_partition(home_partition) diff --git a/docs/cli_parameters/config/disk_config.rst b/docs/cli_parameters/config/disk_config.rst index 3dc01fb2..b09d0dc0 100644 --- a/docs/cli_parameters/config/disk_config.rst +++ b/docs/cli_parameters/config/disk_config.rst @@ -186,34 +186,24 @@ This example contains both subvolumes and compression. { "btrfs": [ { - "compress": false, "mountpoint": "/", "name": "@", - "nodatacow": false }, { - "compress": false, "mountpoint": "/home", "name": "@home", - "nodatacow": false }, { - "compress": false, "mountpoint": "/var/log", "name": "@log", - "nodatacow": false }, { - "compress": false, "mountpoint": "/var/cache/pacman/pkg", "name": "@pkg", - "nodatacow": false }, { - "compress": false, "mountpoint": "/.snapshots", "name": "@.snapshots", - "nodatacow": false } ], "dev_path": null, -- cgit v1.2.3-70-g09d2 From b470b16ec923260cfd9c5b9f2b88e0a39611b463 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 15 Apr 2024 18:49:00 +1000 Subject: LVM support (#2104) * Submenu for disk configuration * Update * Add LVM manual config * PV selection * LVM volume menu * Update * Fix mypy * Update * Update * Update * Update * Update * Update * Update * Update * Update LVM * Update * Update * Btrfs support * Refactor * LVM on Luks * Luks on LVM * Update * LVM on Luks * Update * Update * mypy * Update * Fix bug with LuksOnLvm and Btrfs * Update * Update * Info -> Debug output --- archinstall/lib/disk/__init__.py | 9 +- archinstall/lib/disk/device_handler.py | 264 ++++++++++--- archinstall/lib/disk/device_model.py | 408 +++++++++++++++++++-- archinstall/lib/disk/disk_menu.py | 140 +++++++ archinstall/lib/disk/encryption_menu.py | 131 +++++-- archinstall/lib/disk/fido.py | 8 +- archinstall/lib/disk/filesystem.py | 295 ++++++++++++++- archinstall/lib/disk/partitioning_menu.py | 18 +- archinstall/lib/disk/subvolume_menu.py | 18 +- archinstall/lib/global_menu.py | 70 ++-- archinstall/lib/installer.py | 427 +++++++++++++++++----- archinstall/lib/interactions/disk_conf.py | 134 ++++++- archinstall/lib/interactions/manage_users_conf.py | 18 +- archinstall/lib/luks.py | 30 +- archinstall/lib/menu/abstract_menu.py | 93 ++--- archinstall/lib/menu/list_manager.py | 28 +- archinstall/lib/menu/menu.py | 8 +- archinstall/lib/menu/table_selection_menu.py | 4 +- archinstall/lib/mirrors.py | 15 - archinstall/scripts/guided.py | 2 +- examples/interactive_installation.py | 2 +- 21 files changed, 1711 insertions(+), 411 deletions(-) create mode 100644 archinstall/lib/disk/disk_menu.py (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/disk/__init__.py b/archinstall/lib/disk/__init__.py index 24dafef5..7f881273 100644 --- a/archinstall/lib/disk/__init__.py +++ b/archinstall/lib/disk/__init__.py @@ -11,6 +11,11 @@ from .device_model import ( BDevice, DiskLayoutType, DiskLayoutConfiguration, + LvmLayoutType, + LvmConfiguration, + LvmVolumeGroup, + LvmVolume, + LvmVolumeStatus, PartitionTable, Unit, Size, @@ -30,7 +35,7 @@ from .device_model import ( CleanType, get_lsblk_info, get_all_lsblk_info, - get_lsblk_by_mountpoint + get_lsblk_by_mountpoint, ) from .encryption_menu import ( select_encryption_type, @@ -39,3 +44,5 @@ from .encryption_menu import ( select_partitions_to_encrypt, DiskEncryptionMenu, ) + +from .disk_menu import DiskLayoutConfigurationMenu diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 6e91ac2e..7ba70382 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -3,8 +3,9 @@ from __future__ import annotations import json import os import logging +import time from pathlib import Path -from typing import List, Dict, Any, Optional, TYPE_CHECKING +from typing import List, Dict, Any, Optional, TYPE_CHECKING, Literal, Iterable from parted import ( # type: ignore Disk, Geometry, FileSystem, @@ -17,11 +18,12 @@ from .device_model import ( BDevice, _DeviceInfo, _PartitionInfo, FilesystemType, Unit, PartitionTable, ModificationStatus, get_lsblk_info, LsblkInfo, - _BtrfsSubvolumeInfo, get_all_lsblk_info, DiskEncryption + _BtrfsSubvolumeInfo, get_all_lsblk_info, DiskEncryption, LvmVolumeGroup, LvmVolume, Size, LvmGroupInfo, + SectorSize, LvmVolumeInfo, LvmPVInfo, SubvolumeModification, BtrfsMountOption ) from ..exceptions import DiskError, UnknownFilesystemFormat -from ..general import SysCommand, SysCallError, JSON +from ..general import SysCommand, SysCallError, JSON, SysCommandWorker from ..luks import Luks2 from ..output import debug, error, info, warn, log from ..utils.util import is_subpath @@ -189,7 +191,7 @@ class DeviceHandler(object): return subvol_infos - def _perform_formatting( + def format( self, fs_type: FilesystemType, path: Path, @@ -234,7 +236,7 @@ class DeviceHandler(object): options += additional_parted_options options_str = ' '.join(options) - info(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') + debug(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') try: SysCommand(f"/usr/bin/{command} {options_str} {path}") @@ -243,7 +245,33 @@ class DeviceHandler(object): error(msg) raise DiskError(msg) from err - def _perform_enc_formatting( + def encrypt( + self, + dev_path: Path, + mapper_name: Optional[str], + enc_password: str, + lock_after_create: bool = True + ) -> Luks2: + luks_handler = Luks2( + dev_path, + mapper_name=mapper_name, + password=enc_password + ) + + key_file = luks_handler.encrypt() + + luks_handler.unlock(key_file=key_file) + + if not luks_handler.mapper_dev: + raise DiskError('Failed to unlock luks device') + + if lock_after_create: + debug(f'luks2 locking device: {dev_path}') + luks_handler.lock() + + return luks_handler + + def format_encrypted( self, dev_path: Path, mapper_name: Optional[str], @@ -258,71 +286,160 @@ class DeviceHandler(object): key_file = luks_handler.encrypt() - debug(f'Unlocking luks2 device: {dev_path}') luks_handler.unlock(key_file=key_file) if not luks_handler.mapper_dev: raise DiskError('Failed to unlock luks device') info(f'luks2 formatting mapper dev: {luks_handler.mapper_dev}') - self._perform_formatting(fs_type, luks_handler.mapper_dev) + self.format(fs_type, luks_handler.mapper_dev) info(f'luks2 locking device: {dev_path}') luks_handler.lock() - def _validate_partitions(self, partitions: List[PartitionModification]): - checks = { - # verify that all partitions have a path set (which implies that they have been created) - lambda x: x.dev_path is None: ValueError('When formatting, all partitions must have a path set'), - # crypto luks is not a valid file system type - lambda x: x.fs_type is FilesystemType.Crypto_luks: ValueError('Crypto luks cannot be set as a filesystem type'), - # file system type must be set - lambda x: x.fs_type is None: ValueError('File system type must be set for modification') - } - - for check, exc in checks.items(): - found = next(filter(check, partitions), None) - if found is not None: - raise exc - - def format( + def _lvm_info( self, - device_mod: DeviceModification, - enc_conf: Optional['DiskEncryption'] = None - ): - """ - Format can be given an overriding path, for instance /dev/null to test - the formatting functionality and in essence the support for the given filesystem. - """ + cmd: str, + info_type: Literal['lv', 'vg', 'pvseg'] + ) -> Optional[Any]: + raw_info = SysCommand(cmd).decode().split('\n') - # only verify partitions that are being created or modified - create_or_modify_parts = [p for p in device_mod.partitions if p.is_create_or_modify()] + # for whatever reason the output sometimes contains + # "File descriptor X leaked leaked on vgs invocation + data = '\n'.join([raw for raw in raw_info if 'File descriptor' not in raw]) - self._validate_partitions(create_or_modify_parts) + debug(f'LVM info: {data}') - # make sure all devices are unmounted - self._umount_all_existing(device_mod.device_path) - - for part_mod in create_or_modify_parts: - # partition will be encrypted - if enc_conf is not None and part_mod in enc_conf.partitions: - self._perform_enc_formatting( - part_mod.safe_dev_path, - part_mod.mapper_name, - part_mod.safe_fs_type, - enc_conf - ) - else: - self._perform_formatting(part_mod.safe_fs_type, part_mod.safe_dev_path) + reports = json.loads(data) + + for report in reports['report']: + if len(report[info_type]) != 1: + raise ValueError(f'Report does not contain any entry') - # synchronize with udev before using lsblk - SysCommand('udevadm settle') + entry = report[info_type][0] - lsblk_info = self._fetch_part_info(part_mod.safe_dev_path) + match info_type: + case 'pvseg': + return LvmPVInfo( + pv_name=Path(entry['pv_name']), + lv_name=entry['lv_name'], + vg_name=entry['vg_name'], + ) + case 'lv': + return LvmVolumeInfo( + lv_name=entry['lv_name'], + vg_name=entry['vg_name'], + lv_size=Size(int(entry[f'lv_size'][:-1]), Unit.B, SectorSize.default()) + ) + case 'vg': + return LvmGroupInfo( + vg_uuid=entry['vg_uuid'], + vg_size=Size(int(entry[f'vg_size'][:-1]), Unit.B, SectorSize.default()) + ) + + return None - part_mod.partn = lsblk_info.partn - part_mod.partuuid = lsblk_info.partuuid - part_mod.uuid = lsblk_info.uuid + def _lvm_info_with_retry(self, cmd: str, info_type: Literal['lv', 'vg', 'pvseg']) -> Optional[Any]: + attempts = 3 + + for attempt_nr in range(attempts): + try: + return self._lvm_info(cmd, info_type) + except ValueError: + time.sleep(attempt_nr + 1) + + raise ValueError(f'Failed to fetch {info_type} information') + + def lvm_vol_info(self, lv_name: str) -> Optional[LvmVolumeInfo]: + cmd = ( + 'lvs --reportformat json ' + '--unit B ' + f'-S lv_name={lv_name}' + ) + + return self._lvm_info_with_retry(cmd, 'lv') + + def lvm_group_info(self, vg_name: str) -> Optional[LvmGroupInfo]: + cmd = ( + 'vgs --reportformat json ' + '--unit B ' + '-o vg_name,vg_uuid,vg_size ' + f'-S vg_name={vg_name}' + ) + + return self._lvm_info_with_retry(cmd, 'vg') + + def lvm_pvseg_info(self, vg_name: str, lv_name: str) -> Optional[LvmPVInfo]: + cmd = ( + 'pvs ' + '--segments -o+lv_name,vg_name ' + f'-S vg_name={vg_name},lv_name={lv_name} ' + '--reportformat json ' + ) + + return self._lvm_info_with_retry(cmd, 'pvseg') + + def lvm_vol_change(self, vol: LvmVolume, activate: bool): + active_flag = 'y' if activate else 'n' + cmd = f'lvchange -a {active_flag} {vol.safe_dev_path}' + + debug(f'lvchange volume: {cmd}') + SysCommand(cmd) + + def lvm_export_vg(self, vg: LvmVolumeGroup): + cmd = f'vgexport {vg.name}' + + debug(f'vgexport: {cmd}') + SysCommand(cmd) + + def lvm_import_vg(self, vg: LvmVolumeGroup): + cmd = f'vgimport {vg.name}' + + debug(f'vgimport: {cmd}') + SysCommand(cmd) + + def lvm_vol_reduce(self, vol_path: Path, amount: Size): + val = amount.format_size(Unit.B, include_unit=False) + cmd = f'lvreduce -L -{val}B {vol_path}' + + debug(f'Reducing LVM volume size: {cmd}') + SysCommand(cmd) + + def lvm_pv_create(self, pvs: Iterable[Path]): + cmd = 'pvcreate ' + ' '.join([str(pv) for pv in pvs]) + debug(f'Creating LVM PVS: {cmd}') + + worker = SysCommandWorker(cmd) + worker.poll() + worker.write(b'y\n', line_ending=False) + + def lvm_vg_create(self, pvs: Iterable[Path], vg_name: str): + pvs_str = ' '.join([str(pv) for pv in pvs]) + cmd = f'vgcreate --yes {vg_name} {pvs_str}' + + debug(f'Creating LVM group: {cmd}') + + worker = SysCommandWorker(cmd) + worker.poll() + worker.write(b'y\n', line_ending=False) + + def lvm_vol_create(self, vg_name: str, volume: LvmVolume, offset: Optional[Size] = None): + if offset is not None: + length = volume.length - offset + else: + length = volume.length + + length_str = length.format_size(Unit.B, include_unit=False) + cmd = f'lvcreate --yes -L {length_str}B {vg_name} -n {volume.name}' + + debug(f'Creating volume: {cmd}') + + worker = SysCommandWorker(cmd) + worker.poll() + worker.write(b'y\n', line_ending=False) + + volume.vg_name = vg_name + volume.dev_path = Path(f'/dev/{vg_name}/{volume.name}') def _setup_partition( self, @@ -385,7 +502,7 @@ class DeviceHandler(object): # the partition has a path now that it has been added part_mod.dev_path = Path(partition.path) - def _fetch_part_info(self, path: Path) -> LsblkInfo: + def fetch_part_info(self, path: Path) -> LsblkInfo: lsblk_info = get_lsblk_info(path) if not lsblk_info.partn: @@ -404,6 +521,37 @@ class DeviceHandler(object): return lsblk_info + def create_lvm_btrfs_subvolumes( + self, + path: Path, + btrfs_subvols: List[SubvolumeModification], + mount_options: List[str] + ): + info(f'Creating subvolumes: {path}') + + self.mount(path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + + for sub_vol in btrfs_subvols: + debug(f'Creating subvolume: {sub_vol.name}') + + subvol_path = self._TMP_BTRFS_MOUNT / sub_vol.name + + SysCommand(f"btrfs subvolume create {subvol_path}") + + if BtrfsMountOption.nodatacow.value in mount_options: + try: + SysCommand(f'chattr +C {subvol_path}') + except SysCallError as err: + raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {err}') + + if BtrfsMountOption.compress.value in mount_options: + try: + SysCommand(f'chattr +c {subvol_path}') + except SysCallError as err: + raise DiskError(f'Could not set compress attribute at {subvol_path}: {err}') + + self.umount(path) + def create_btrfs_volumes( self, part_mod: PartitionModification, @@ -468,8 +616,8 @@ class DeviceHandler(object): return luks_handler - def _umount_all_existing(self, device_path: Path): - info(f'Unmounting all existing partitions: {device_path}') + def umount_all_existing(self, device_path: Path): + debug(f'Unmounting all existing partitions: {device_path}') existing_partitions = self._devices[device_path].partition_infos @@ -498,7 +646,7 @@ class DeviceHandler(object): raise DiskError('Too many partitions on disk, MBR disks can only have 3 primary partitions') # make sure all devices are unmounted - self._umount_all_existing(modification.device_path) + self.umount_all_existing(modification.device_path) # WARNING: the entire device will be wiped and all data lost if modification.wipe: diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index fe96203c..1cd3d674 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -41,6 +41,8 @@ class DiskLayoutType(Enum): class DiskLayoutConfiguration: config_type: DiskLayoutType device_modifications: List[DeviceModification] = field(default_factory=list) + lvm_config: Optional[LvmConfiguration] = None + # used for pre-mounted config mountpoint: Optional[Path] = None @@ -51,13 +53,18 @@ class DiskLayoutConfiguration: 'mountpoint': str(self.mountpoint) } else: - return { + config: Dict[str, Any] = { 'config_type': self.config_type.value, - 'device_modifications': [mod.json() for mod in self.device_modifications] + 'device_modifications': [mod.json() for mod in self.device_modifications], } + if self.lvm_config: + config['lvm_config'] = self.lvm_config.json() + + return config + @classmethod - def parse_arg(cls, disk_config: Dict[str, List[Dict[str, Any]]]) -> Optional[DiskLayoutConfiguration]: + def parse_arg(cls, disk_config: Dict[str, Any]) -> Optional[DiskLayoutConfiguration]: from .device_handler import device_handler device_modifications: List[DeviceModification] = [] @@ -124,6 +131,10 @@ class DiskLayoutConfiguration: device_modification.partitions = device_partitions device_modifications.append(device_modification) + # Parse LVM configuration from settings + if (lvm_arg := disk_config.get('lvm_config', None)) is not None: + config.lvm_config = LvmConfiguration.parse_arg(lvm_arg, config) + return config @@ -133,24 +144,24 @@ class PartitionTable(Enum): class Unit(Enum): - B = 1 # byte - kB = 1000**1 # kilobyte - MB = 1000**2 # megabyte - GB = 1000**3 # gigabyte - TB = 1000**4 # terabyte - PB = 1000**5 # petabyte - EB = 1000**6 # exabyte - ZB = 1000**7 # zettabyte - YB = 1000**8 # yottabyte - - KiB = 1024**1 # kibibyte - MiB = 1024**2 # mebibyte - GiB = 1024**3 # gibibyte - TiB = 1024**4 # tebibyte - PiB = 1024**5 # pebibyte - EiB = 1024**6 # exbibyte - ZiB = 1024**7 # zebibyte - YiB = 1024**8 # yobibyte + B = 1 # byte + kB = 1000 ** 1 # kilobyte + MB = 1000 ** 2 # megabyte + GB = 1000 ** 3 # gigabyte + TB = 1000 ** 4 # terabyte + PB = 1000 ** 5 # petabyte + EB = 1000 ** 6 # exabyte + ZB = 1000 ** 7 # zettabyte + YB = 1000 ** 8 # yottabyte + + KiB = 1024 ** 1 # kibibyte + MiB = 1024 ** 2 # mebibyte + GiB = 1024 ** 3 # gibibyte + TiB = 1024 ** 4 # tebibyte + PiB = 1024 ** 5 # pebibyte + EiB = 1024 ** 6 # exbibyte + ZiB = 1024 ** 7 # zebibyte + YiB = 1024 ** 8 # yobibyte sectors = 'sectors' # size in sector @@ -575,7 +586,7 @@ class PartitionFlag(Enum): Which is the way libparted checks for its flags: https://git.savannah.gnu.org/gitweb/?p=parted.git;a=blob;f=libparted/labels/gpt.c;hb=4a0e468ed63fff85a1f9b923189f20945b32f4f1#l183 """ Boot = _ped.PARTITION_BOOT - XBOOTLDR = _ped.PARTITION_BLS_BOOT # Note: parted calls this bls_boot + XBOOTLDR = _ped.PARTITION_BLS_BOOT # Note: parted calls this bls_boot ESP = _ped.PARTITION_ESP @@ -658,6 +669,10 @@ class PartitionModification: flags: List[PartitionFlag] = field(default_factory=list) btrfs_subvols: List[SubvolumeModification] = field(default_factory=list) + # only set when modification was created from an existing + # partition info object to be able to reference it back + part_info: Optional[_PartitionInfo] = None + # only set if the device was created or exists dev_path: Optional[Path] = None partn: Optional[int] = None @@ -724,7 +739,8 @@ class PartitionModification: uuid=partition_info.uuid, flags=partition_info.flags, mountpoint=mountpoint, - btrfs_subvols=subvol_mods + btrfs_subvols=subvol_mods, + part_info=partition_info ) @property @@ -832,6 +848,270 @@ class PartitionModification: return part_mod +class LvmLayoutType(Enum): + Default = 'default' + + # Manual = 'manual_lvm' + + def display_msg(self) -> str: + match self: + case LvmLayoutType.Default: + return str(_('Default layout')) + # case LvmLayoutType.Manual: + # return str(_('Manual configuration')) + + raise ValueError(f'Unknown type: {self}') + + +@dataclass +class LvmVolumeGroup: + name: str + pvs: List[PartitionModification] + volumes: List[LvmVolume] = field(default_factory=list) + + def json(self) -> Dict[str, Any]: + return { + 'name': self.name, + 'lvm_pvs': [p.obj_id for p in self.pvs], + 'volumes': [vol.json() for vol in self.volumes] + } + + @staticmethod + def parse_arg(arg: Dict[str, Any], disk_config: DiskLayoutConfiguration) -> LvmVolumeGroup: + lvm_pvs = [] + for mod in disk_config.device_modifications: + for part in mod.partitions: + if part.obj_id in arg.get('lvm_pvs', []): + lvm_pvs.append(part) + + return LvmVolumeGroup( + arg['name'], + lvm_pvs, + [LvmVolume.parse_arg(vol) for vol in arg['volumes']] + ) + + def contains_lv(self, lv: LvmVolume) -> bool: + return lv in self.volumes + + +class LvmVolumeStatus(Enum): + Exist = 'existing' + Modify = 'modify' + Delete = 'delete' + Create = 'create' + + +@dataclass +class LvmVolume: + status: LvmVolumeStatus + name: str + fs_type: FilesystemType + length: Size + mountpoint: Optional[Path] + mount_options: List[str] = field(default_factory=list) + btrfs_subvols: List[SubvolumeModification] = field(default_factory=list) + + # volume group name + vg_name: Optional[str] = None + # mapper device path /dev// + dev_path: Optional[Path] = None + + def __post_init__(self): + # needed to use the object as a dictionary key due to hash func + if not hasattr(self, '_obj_id'): + self._obj_id = uuid.uuid4() + + def __hash__(self): + return hash(self._obj_id) + + @property + def obj_id(self) -> str: + if hasattr(self, '_obj_id'): + return str(self._obj_id) + return '' + + @property + def mapper_name(self) -> Optional[str]: + if self.dev_path: + return f'{storage.get("ENC_IDENTIFIER", "ai")}{self.safe_dev_path.name}' + return None + + @property + def mapper_path(self) -> Path: + if self.mapper_name: + return Path(f'/dev/mapper/{self.mapper_name}') + + raise ValueError('No mapper path set') + + @property + def safe_dev_path(self) -> Path: + if self.dev_path: + return self.dev_path + raise ValueError('No device path for volume defined') + + @property + def safe_fs_type(self) -> FilesystemType: + if self.fs_type is None: + raise ValueError('File system type is not set') + return self.fs_type + + @property + def relative_mountpoint(self) -> Path: + """ + Will return the relative path based on the anchor + e.g. Path('/mnt/test') -> Path('mnt/test') + """ + if self.mountpoint is not None: + return self.mountpoint.relative_to(self.mountpoint.anchor) + + raise ValueError('Mountpoint is not specified') + + @staticmethod + def parse_arg(arg: Dict[str, Any]) -> LvmVolume: + volume = LvmVolume( + status=LvmVolumeStatus(arg['status']), + name=arg['name'], + fs_type=FilesystemType(arg['fs_type']), + length=Size.parse_args(arg['length']), + mountpoint=Path(arg['mountpoint']) if arg['mountpoint'] else None, + mount_options=arg.get('mount_options', []), + btrfs_subvols=SubvolumeModification.parse_args(arg.get('btrfs', [])) + ) + + setattr(volume, '_obj_id', arg['obj_id']) + + return volume + + def json(self) -> Dict[str, Any]: + return { + 'obj_id': self.obj_id, + 'status': self.status.value, + 'name': self.name, + 'fs_type': self.fs_type.value, + 'length': self.length.json(), + 'mountpoint': str(self.mountpoint) if self.mountpoint else None, + 'mount_options': self.mount_options, + 'btrfs': [vol.json() for vol in self.btrfs_subvols] + } + + def table_data(self) -> Dict[str, Any]: + part_mod = { + 'Type': self.status.value, + 'Name': self.name, + 'Size': self.length.format_highest(), + 'FS type': self.fs_type.value, + 'Mountpoint': str(self.mountpoint) if self.mountpoint else '', + 'Mount options': ', '.join(self.mount_options), + 'Btrfs': '{} {}'.format(str(len(self.btrfs_subvols)), 'vol') + } + return part_mod + + def is_modify(self) -> bool: + return self.status == LvmVolumeStatus.Modify + + def exists(self) -> bool: + return self.status == LvmVolumeStatus.Exist + + def is_exists_or_modify(self) -> bool: + return self.status in [LvmVolumeStatus.Exist, LvmVolumeStatus.Modify] + + def is_root(self) -> bool: + if self.mountpoint is not None: + return Path('/') == self.mountpoint + else: + for subvol in self.btrfs_subvols: + if subvol.is_root(): + return True + + return False + + +@dataclass +class LvmGroupInfo: + vg_size: Size + vg_uuid: str + + +@dataclass +class LvmVolumeInfo: + lv_name: str + vg_name: str + lv_size: Size + + +@dataclass +class LvmPVInfo: + pv_name: Path + lv_name: str + vg_name: str + + +@dataclass +class LvmConfiguration: + config_type: LvmLayoutType + vol_groups: List[LvmVolumeGroup] + + def __post_init__(self): + # make sure all volume groups have unique PVs + pvs = [] + for group in self.vol_groups: + for pv in group.pvs: + if pv in pvs: + raise ValueError('A PV cannot be used in multiple volume groups') + pvs.append(pv) + + def json(self) -> Dict[str, Any]: + return { + 'config_type': self.config_type.value, + 'vol_groups': [vol_gr.json() for vol_gr in self.vol_groups] + } + + @staticmethod + def parse_arg(arg: Dict[str, Any], disk_config: DiskLayoutConfiguration) -> LvmConfiguration: + lvm_pvs = [] + for mod in disk_config.device_modifications: + for part in mod.partitions: + if part.obj_id in arg.get('lvm_pvs', []): + lvm_pvs.append(part) + + return LvmConfiguration( + config_type=LvmLayoutType(arg['config_type']), + vol_groups=[LvmVolumeGroup.parse_arg(vol_group, disk_config) for vol_group in arg['vol_groups']], + ) + + def get_all_pvs(self) -> List[PartitionModification]: + pvs = [] + for vg in self.vol_groups: + pvs += vg.pvs + + return pvs + + def get_all_volumes(self) -> List[LvmVolume]: + volumes = [] + + for vg in self.vol_groups: + volumes += vg.volumes + + return volumes + + def get_root_volume(self) -> Optional[LvmVolume]: + for vg in self.vol_groups: + filtered = next(filter(lambda x: x.is_root(), vg.volumes), None) + if filtered: + return filtered + + return None + + +# def get_lv_crypt_uuid(self, lv: LvmVolume, encryption: EncryptionType) -> str: +# """ +# Find the LUKS superblock UUID for the device that +# contains the given logical volume +# """ +# for vg in self.vol_groups: +# if vg.contains_lv(lv): + + @dataclass class DeviceModification: device: BDevice @@ -885,11 +1165,16 @@ class DeviceModification: class EncryptionType(Enum): NoEncryption = "no_encryption" Luks = "luks" + LvmOnLuks = 'lvm_on_luks' + LuksOnLvm = 'luks_on_lvm' @classmethod def _encryption_type_mapper(cls) -> Dict[str, 'EncryptionType']: return { - 'Luks': EncryptionType.Luks + str(_('No Encryption')): EncryptionType.NoEncryption, + str(_('LUKS')): EncryptionType.Luks, + str(_('LVM on LUKS')): EncryptionType.LvmOnLuks, + str(_('LUKS on LVM')): EncryptionType.LuksOnLvm } @classmethod @@ -906,18 +1191,31 @@ class EncryptionType(Enum): @dataclass class DiskEncryption: - encryption_type: EncryptionType = EncryptionType.Luks + encryption_type: EncryptionType = EncryptionType.NoEncryption encryption_password: str = '' partitions: List[PartitionModification] = field(default_factory=list) + lvm_volumes: List[LvmVolume] = field(default_factory=list) hsm_device: Optional[Fido2Device] = None - def should_generate_encryption_file(self, part_mod: PartitionModification) -> bool: - return part_mod in self.partitions and part_mod.mountpoint != Path('/') + def __post_init__(self): + if self.encryption_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and not self.partitions: + raise ValueError('Luks or LvmOnLuks encryption require partitions to be defined') + + if self.encryption_type == EncryptionType.LuksOnLvm and not self.lvm_volumes: + raise ValueError('LuksOnLvm encryption require LMV volumes to be defined') + + def should_generate_encryption_file(self, dev: PartitionModification | LvmVolume) -> bool: + if isinstance(dev, PartitionModification): + return dev in self.partitions and dev.mountpoint != Path('/') + elif isinstance(dev, LvmVolume): + return dev in self.lvm_volumes and dev.mountpoint != Path('/') + return False def json(self) -> Dict[str, Any]: obj: Dict[str, Any] = { 'encryption_type': self.encryption_type.value, - 'partitions': [p.obj_id for p in self.partitions] + 'partitions': [p.obj_id for p in self.partitions], + 'lvm_volumes': [vol.obj_id for vol in self.lvm_volumes] } if self.hsm_device: @@ -925,23 +1223,47 @@ class DiskEncryption: return obj + @classmethod + def validate_enc(cls, disk_config: DiskLayoutConfiguration) -> bool: + partitions = [] + + for mod in disk_config.device_modifications: + for part in mod.partitions: + partitions.append(part) + + if len(partitions) > 2: # assume one boot and at least 2 additional + if disk_config.lvm_config: + return False + + return True + @classmethod def parse_arg( cls, disk_config: DiskLayoutConfiguration, arg: Dict[str, Any], password: str = '' - ) -> 'DiskEncryption': + ) -> Optional['DiskEncryption']: + if not cls.validate_enc(disk_config): + return None + enc_partitions = [] for mod in disk_config.device_modifications: for part in mod.partitions: if part.obj_id in arg.get('partitions', []): enc_partitions.append(part) + volumes = [] + if disk_config.lvm_config: + for vol in disk_config.lvm_config.get_all_volumes(): + if vol.obj_id in arg.get('lvm_volumes', []): + volumes.append(vol) + enc = DiskEncryption( EncryptionType(arg['encryption_type']), password, - enc_partitions + enc_partitions, + volumes ) if hsm := arg.get('hsm_device', None): @@ -992,7 +1314,7 @@ class LsblkInfo: tran: Optional[str] = None partn: Optional[int] = None partuuid: Optional[str] = None - parttype :Optional[str] = None + parttype: Optional[str] = None uuid: Optional[str] = None fstype: Optional[str] = None fsver: Optional[str] = None @@ -1017,7 +1339,7 @@ class LsblkInfo: 'tran': self.tran, 'partn': self.partn, 'partuuid': self.partuuid, - 'parttype' : self.parttype, + 'parttype': self.parttype, 'uuid': self.uuid, 'fstype': self.fstype, 'fsver': self.fsver, @@ -1102,13 +1424,24 @@ def _clean_field(name: str, clean_type: CleanType) -> str: return name.replace('_percentage', '%').replace('_', '-') -def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None) -> List[LsblkInfo]: +def _fetch_lsblk_info( + dev_path: Optional[Union[Path, str]] = None, + reverse: bool = False, + full_dev_path: bool = False, + retry: int = 3 +) -> List[LsblkInfo]: fields = [_clean_field(f, CleanType.Lsblk) for f in LsblkInfo.fields()] cmd = ['lsblk', '--json', '--bytes', '--output', '+' + ','.join(fields)] if dev_path: cmd.append(str(dev_path)) + if reverse: + cmd.append('--inverse') + + if full_dev_path: + cmd.append('--paths') + try: result = SysCommand(cmd).decode() except SysCallError as err: @@ -1132,8 +1465,12 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None) -> List[Lsblk return [LsblkInfo.from_json(device) for device in blockdevices] -def get_lsblk_info(dev_path: Union[Path, str]) -> LsblkInfo: - if infos := _fetch_lsblk_info(dev_path): +def get_lsblk_info( + dev_path: Union[Path, str], + reverse: bool = False, + full_dev_path: bool = False +) -> LsblkInfo: + if infos := _fetch_lsblk_info(dev_path, reverse=reverse, full_dev_path=full_dev_path): return infos[0] raise DiskError(f'lsblk failed to retrieve information for "{dev_path}"') @@ -1142,6 +1479,7 @@ def get_lsblk_info(dev_path: Union[Path, str]) -> LsblkInfo: def get_all_lsblk_info() -> List[LsblkInfo]: return _fetch_lsblk_info() + def get_lsblk_by_mountpoint(mountpoint: Path, as_prefix: bool = False) -> List[LsblkInfo]: def _check(infos: List[LsblkInfo]) -> List[LsblkInfo]: devices = [] diff --git a/archinstall/lib/disk/disk_menu.py b/archinstall/lib/disk/disk_menu.py new file mode 100644 index 00000000..a7d9ccc3 --- /dev/null +++ b/archinstall/lib/disk/disk_menu.py @@ -0,0 +1,140 @@ +from typing import Dict, Optional, Any, TYPE_CHECKING, List + +from . import DiskLayoutConfiguration, DiskLayoutType +from .device_model import LvmConfiguration +from ..disk import ( + DeviceModification +) +from ..interactions import select_disk_config +from ..interactions.disk_conf import select_lvm_config +from ..menu import ( + Selector, + AbstractSubMenu +) +from ..output import FormattedOutput + +if TYPE_CHECKING: + _: Any + + +class DiskLayoutConfigurationMenu(AbstractSubMenu): + def __init__( + self, + disk_layout_config: Optional[DiskLayoutConfiguration], + data_store: Dict[str, Any], + advanced: bool = False + ): + self._disk_layout_config = disk_layout_config + self._advanced = advanced + + super().__init__(data_store=data_store, preview_size=0.5) + + def setup_selection_menu_options(self): + self._menu_options['disk_config'] = \ + Selector( + _('Partitioning'), + lambda x: self._select_disk_layout_config(x), + display_func=lambda x: self._display_disk_layout(x), + preview_func=self._prev_disk_layouts, + default=self._disk_layout_config, + enabled=True + ) + self._menu_options['lvm_config'] = \ + Selector( + _('Logical Volume Management (LVM)'), + lambda x: self._select_lvm_config(x), + display_func=lambda x: self.defined_text if x else '', + preview_func=self._prev_lvm_config, + default=self._disk_layout_config.lvm_config if self._disk_layout_config else None, + dependencies=[self._check_dep_lvm], + enabled=True + ) + + def run(self, allow_reset: bool = True) -> Optional[DiskLayoutConfiguration]: + super().run(allow_reset=allow_reset) + + disk_layout_config: Optional[DiskLayoutConfiguration] = self._data_store.get('disk_config', None) + + if disk_layout_config: + disk_layout_config.lvm_config = self._data_store.get('lvm_config', None) + + return disk_layout_config + + def _check_dep_lvm(self) -> bool: + disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection + + if disk_layout_conf and disk_layout_conf.config_type == DiskLayoutType.Default: + return True + + return False + + def _select_disk_layout_config( + self, + preset: Optional[DiskLayoutConfiguration] + ) -> Optional[DiskLayoutConfiguration]: + disk_config = select_disk_config(preset, advanced_option=self._advanced) + + if disk_config != preset: + self._menu_options['lvm_config'].set_current_selection(None) + + return disk_config + + def _select_lvm_config(self, preset: Optional[LvmConfiguration]) -> Optional[LvmConfiguration]: + disk_config: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection + if disk_config: + return select_lvm_config(disk_config, preset=preset) + return preset + + def _display_disk_layout(self, current_value: Optional[DiskLayoutConfiguration] = None) -> str: + if current_value: + return current_value.config_type.display_msg() + return '' + + def _prev_disk_layouts(self) -> Optional[str]: + disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection + + if disk_layout_conf: + device_mods: List[DeviceModification] = \ + list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications)) + + if device_mods: + output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg()) + output_btrfs = '' + + for mod in device_mods: + # create partition table + partition_table = FormattedOutput.as_table(mod.partitions) + + output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n' + output_partition += partition_table + '\n' + + # create btrfs table + btrfs_partitions = list( + filter(lambda p: len(p.btrfs_subvols) > 0, mod.partitions) + ) + for partition in btrfs_partitions: + output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n' + + output = output_partition + output_btrfs + return output.rstrip() + + return None + + def _prev_lvm_config(self) -> Optional[str]: + lvm_config: Optional[LvmConfiguration] = self._menu_options['lvm_config'].current_selection + + if lvm_config: + output = '{}: {}\n'.format(str(_('Configuration')), lvm_config.config_type.display_msg()) + + for vol_gp in lvm_config.vol_groups: + pv_table = FormattedOutput.as_table(vol_gp.pvs) + output += '{}:\n{}'.format(str(_('Physical volumes')), pv_table) + + output += f'\nVolume Group: {vol_gp.name}' + + lvm_volumes = FormattedOutput.as_table(vol_gp.volumes) + output += '\n\n{}:\n{}'.format(str(_('Volumes')), lvm_volumes) + + return output + + return None diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index c3a1c32f..b0e292ce 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -1,6 +1,7 @@ from pathlib import Path from typing import Dict, Optional, Any, TYPE_CHECKING, List +from . import LvmConfiguration, LvmVolume from ..disk import ( DeviceModification, DiskLayoutConfiguration, @@ -40,31 +41,41 @@ class DiskEncryptionMenu(AbstractSubMenu): super().__init__(data_store=data_store) def setup_selection_menu_options(self): + self._menu_options['encryption_type'] = \ + Selector( + _('Encryption type'), + func=lambda preset: select_encryption_type(self._disk_config, preset), + display_func=lambda x: EncryptionType.type_to_text(x) if x else None, + default=self._preset.encryption_type, + enabled=True, + ) self._menu_options['encryption_password'] = \ Selector( _('Encryption password'), lambda x: select_encrypted_password(), + dependencies=[self._check_dep_enc_type], display_func=lambda x: secret(x) if x else '', default=self._preset.encryption_password, enabled=True ) - self._menu_options['encryption_type'] = \ - Selector( - _('Encryption type'), - func=lambda preset: select_encryption_type(preset), - display_func=lambda x: EncryptionType.type_to_text(x) if x else None, - dependencies=['encryption_password'], - default=self._preset.encryption_type, - enabled=True - ) self._menu_options['partitions'] = \ Selector( _('Partitions'), func=lambda preset: select_partitions_to_encrypt(self._disk_config.device_modifications, preset), display_func=lambda x: f'{len(x)} {_("Partitions")}' if x else None, - dependencies=['encryption_password'], + dependencies=[self._check_dep_partitions], default=self._preset.partitions, - preview_func=self._prev_disk_layouts, + preview_func=self._prev_partitions, + enabled=True + ) + self._menu_options['lvm_vols'] = \ + Selector( + _('LVM volumes'), + func=lambda preset: self._select_lvm_vols(preset), + display_func=lambda x: f'{len(x)} {_("LVM volumes")}' if x else None, + dependencies=[self._check_dep_lvm_vols], + default=self._preset.lvm_volumes, + preview_func=self._prev_lvm_vols, enabled=True ) self._menu_options['HSM'] = \ @@ -73,19 +84,54 @@ class DiskEncryptionMenu(AbstractSubMenu): func=lambda preset: select_hsm(preset), display_func=lambda x: self._display_hsm(x), preview_func=self._prev_hsm, - dependencies=['encryption_password'], + dependencies=[self._check_dep_enc_type], default=self._preset.hsm_device, enabled=True ) + def _select_lvm_vols(self, preset: List[LvmVolume]) -> List[LvmVolume]: + if self._disk_config.lvm_config: + return select_lvm_vols_to_encrypt(self._disk_config.lvm_config, preset=preset) + return [] + + def _check_dep_enc_type(self) -> bool: + enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection + if enc_type and enc_type != EncryptionType.NoEncryption: + return True + return False + + def _check_dep_partitions(self) -> bool: + enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection + if enc_type and enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks]: + return True + return False + + def _check_dep_lvm_vols(self) -> bool: + enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection + if enc_type and enc_type == EncryptionType.LuksOnLvm: + return True + return False + def run(self, allow_reset: bool = True) -> Optional[DiskEncryption]: super().run(allow_reset=allow_reset) - if self._data_store.get('encryption_password', None): + enc_type = self._data_store.get('encryption_type', None) + enc_password = self._data_store.get('encryption_password', None) + enc_partitions = self._data_store.get('partitions', None) + enc_lvm_vols = self._data_store.get('lvm_vols', None) + + if enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and enc_partitions: + enc_lvm_vols = [] + + if enc_type == EncryptionType.LuksOnLvm: + enc_partitions = [] + + if enc_type != EncryptionType.NoEncryption and enc_password and (enc_partitions or enc_lvm_vols): return DiskEncryption( - encryption_password=self._data_store.get('encryption_password', None), - encryption_type=self._data_store['encryption_type'], - partitions=self._data_store.get('partitions', None), + encryption_password=enc_password, + encryption_type=enc_type, + partitions=enc_partitions, + lvm_volumes=enc_lvm_vols, hsm_device=self._data_store.get('HSM', None) ) @@ -97,7 +143,7 @@ class DiskEncryptionMenu(AbstractSubMenu): return None - def _prev_disk_layouts(self) -> Optional[str]: + def _prev_partitions(self) -> Optional[str]: partitions: Optional[List[PartitionModification]] = self._menu_options['partitions'].current_selection if partitions: output = str(_('Partitions to be encrypted')) + '\n' @@ -106,6 +152,15 @@ class DiskEncryptionMenu(AbstractSubMenu): return None + def _prev_lvm_vols(self) -> Optional[str]: + volumes: Optional[List[PartitionModification]] = self._menu_options['lvm_vols'].current_selection + if volumes: + output = str(_('LVM volumes to be encrypted')) + '\n' + output += FormattedOutput.as_table(volumes) + return output.rstrip() + + return None + def _prev_hsm(self) -> Optional[str]: try: Fido2.get_fido2_devices() @@ -123,13 +178,19 @@ class DiskEncryptionMenu(AbstractSubMenu): return None -def select_encryption_type(preset: EncryptionType) -> Optional[EncryptionType]: +def select_encryption_type(disk_config: DiskLayoutConfiguration, preset: EncryptionType) -> Optional[EncryptionType]: title = str(_('Select disk encryption option')) - options = [ - EncryptionType.type_to_text(EncryptionType.Luks) - ] + + if disk_config.lvm_config: + options = [ + EncryptionType.type_to_text(EncryptionType.LvmOnLuks), + EncryptionType.type_to_text(EncryptionType.LuksOnLvm) + ] + else: + options = [EncryptionType.type_to_text(EncryptionType.Luks)] preset_value = EncryptionType.type_to_text(preset) + choice = Menu(title, options, preset_values=preset_value).run() match choice.type_: @@ -197,3 +258,31 @@ def select_partitions_to_encrypt( case MenuSelectionType.Selection: return choice.multi_value return [] + + +def select_lvm_vols_to_encrypt( + lvm_config: LvmConfiguration, + preset: List[LvmVolume] +) -> List[LvmVolume]: + volumes: List[LvmVolume] = lvm_config.get_all_volumes() + + if volumes: + title = str(_('Select which LVM volumes to encrypt')) + partition_table = FormattedOutput.as_table(volumes) + + choice = TableMenu( + title, + table_data=(volumes, partition_table), + preset=preset, + multi=True + ).run() + + match choice.type_: + case MenuSelectionType.Reset: + return [] + case MenuSelectionType.Skip: + return preset + case MenuSelectionType.Selection: + return choice.multi_value + + return [] diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 49904c17..5a139534 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -4,7 +4,7 @@ import getpass from pathlib import Path from typing import List -from .device_model import PartitionModification, Fido2Device +from .device_model import Fido2Device from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes from ..output import error, info from ..exceptions import SysCallError @@ -72,16 +72,16 @@ class Fido2: def fido2_enroll( cls, hsm_device: Fido2Device, - part_mod: PartitionModification, + dev_path: Path, password: str ): - worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device.path} {part_mod.dev_path}", peek_output=True) + worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device.path} {dev_path}", peek_output=True) pw_inputted = False pin_inputted = False while worker.is_alive(): if pw_inputted is False: - if bytes(f"please enter current passphrase for disk {part_mod.dev_path}", 'UTF-8') in worker._trace_log.lower(): + if bytes(f"please enter current passphrase for disk {dev_path}", 'UTF-8') in worker._trace_log.lower(): worker.write(bytes(password, 'UTF-8')) pw_inputted = True elif pin_inputted is False: diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 9c6e6d35..5c11896e 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -3,13 +3,21 @@ from __future__ import annotations import signal import sys import time -from typing import Any, Optional, TYPE_CHECKING +from pathlib import Path +from typing import Any, Optional, TYPE_CHECKING, List, Dict, Set -from .device_model import DiskLayoutConfiguration, DiskLayoutType, PartitionTable, FilesystemType, DiskEncryption from .device_handler import device_handler +from .device_model import ( + DiskLayoutConfiguration, DiskLayoutType, PartitionTable, + FilesystemType, DiskEncryption, LvmVolumeGroup, + Size, Unit, SectorSize, PartitionModification, EncryptionType, + LvmVolume, LvmConfiguration +) from ..hardware import SysInfo -from ..output import debug +from ..luks import Luks2 from ..menu import Menu +from ..output import debug, info +from ..general import SysCommand if TYPE_CHECKING: _: Any @@ -52,13 +60,288 @@ class FilesystemHandler: for mod in device_mods: device_handler.partition(mod, partition_table=partition_table) - device_handler.format(mod, enc_conf=self._enc_config) - for part_mod in mod.partitions: - if part_mod.is_create_or_modify(): + if self._disk_config.lvm_config: + for mod in device_mods: + if boot_part := mod.get_boot_partition(): + debug(f'Formatting boot partition: {boot_part.dev_path}') + self._format_partitions( + [boot_part], + mod.device_path + ) + + self.perform_lvm_operations() + else: + for mod in device_mods: + self._format_partitions( + mod.partitions, + mod.device_path + ) + + for part_mod in mod.partitions: if part_mod.fs_type == FilesystemType.Btrfs: device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config) + def _format_partitions( + self, + partitions: List[PartitionModification], + device_path: Path + ): + """ + Format can be given an overriding path, for instance /dev/null to test + the formatting functionality and in essence the support for the given filesystem. + """ + + # don't touch existing partitions + create_or_modify_parts = [p for p in partitions if p.is_create_or_modify()] + + self._validate_partitions(create_or_modify_parts) + + # make sure all devices are unmounted + device_handler.umount_all_existing(device_path) + + for part_mod in create_or_modify_parts: + # partition will be encrypted + if self._enc_config is not None and part_mod in self._enc_config.partitions: + device_handler.format_encrypted( + part_mod.safe_dev_path, + part_mod.mapper_name, + part_mod.safe_fs_type, + self._enc_config + ) + else: + device_handler.format(part_mod.safe_fs_type, part_mod.safe_dev_path) + + # synchronize with udev before using lsblk + SysCommand('udevadm settle') + + lsblk_info = device_handler.fetch_part_info(part_mod.safe_dev_path) + + part_mod.partn = lsblk_info.partn + part_mod.partuuid = lsblk_info.partuuid + part_mod.uuid = lsblk_info.uuid + + def _validate_partitions(self, partitions: List[PartitionModification]): + checks = { + # verify that all partitions have a path set (which implies that they have been created) + lambda x: x.dev_path is None: ValueError('When formatting, all partitions must have a path set'), + # crypto luks is not a valid file system type + lambda x: x.fs_type is FilesystemType.Crypto_luks: ValueError( + 'Crypto luks cannot be set as a filesystem type'), + # file system type must be set + lambda x: x.fs_type is None: ValueError('File system type must be set for modification') + } + + for check, exc in checks.items(): + found = next(filter(check, partitions), None) + if found is not None: + raise exc + + def perform_lvm_operations(self): + info('Setting up LVM config...') + + if not self._disk_config.lvm_config: + return + + if self._enc_config: + self._setup_lvm_encrypted( + self._disk_config.lvm_config, + self._enc_config + ) + else: + self._setup_lvm(self._disk_config.lvm_config) + self._format_lvm_vols(self._disk_config.lvm_config) + + def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption): + if enc_config.encryption_type == EncryptionType.LvmOnLuks: + enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False) + + self._setup_lvm(lvm_config, enc_mods) + self._format_lvm_vols(lvm_config) + + # export the lvm group safely otherwise the Luks cannot be closed + self._safely_close_lvm(lvm_config) + + for luks in enc_mods.values(): + luks.lock() + elif enc_config.encryption_type == EncryptionType.LuksOnLvm: + self._setup_lvm(lvm_config) + enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False) + self._format_lvm_vols(lvm_config, enc_vols) + + for luks in enc_vols.values(): + luks.lock() + + self._safely_close_lvm(lvm_config) + + def _safely_close_lvm(self, lvm_config: LvmConfiguration): + for vg in lvm_config.vol_groups: + for vol in vg.volumes: + device_handler.lvm_vol_change(vol, False) + + device_handler.lvm_export_vg(vg) + + def _setup_lvm( + self, + lvm_config: LvmConfiguration, + enc_mods: Dict[PartitionModification, Luks2] = {} + ): + self._lvm_create_pvs(lvm_config, enc_mods) + + for vg in lvm_config.vol_groups: + pv_dev_paths = self._get_all_pv_dev_paths(vg.pvs, enc_mods) + + device_handler.lvm_vg_create(pv_dev_paths, vg.name) + + # figure out what the actual available size in the group is + vg_info = device_handler.lvm_group_info(vg.name) + + if not vg_info: + raise ValueError('Unable to fetch VG info') + + # the actual available LVM Group size will be smaller than the + # total PVs size due to reserved metadata storage etc. + # so we'll have a look at the total avail. size, check the delta + # to the desired sizes and subtract some equally from the actually + # created volume + avail_size = vg_info.vg_size + desired_size = sum([vol.length for vol in vg.volumes], Size(0, Unit.B, SectorSize.default())) + + delta = desired_size - avail_size + max_vol_offset = delta.convert(Unit.B) + + max_vol = max(vg.volumes, key=lambda x: x.length) + + for lv in vg.volumes: + offset = max_vol_offset if lv == max_vol else None + + debug(f'vg: {vg.name}, vol: {lv.name}, offset: {offset}') + device_handler.lvm_vol_create(vg.name, lv, offset) + + while True: + debug('Fetching LVM volume info') + lv_info = device_handler.lvm_vol_info(lv.name) + if lv_info is not None: + break + + time.sleep(1) + + self._lvm_vol_handle_e2scrub(vg) + + def _format_lvm_vols( + self, + lvm_config: LvmConfiguration, + enc_vols: Dict[LvmVolume, Luks2] = {} + ): + for vol in lvm_config.get_all_volumes(): + if enc_vol := enc_vols.get(vol, None): + if not enc_vol.mapper_dev: + raise ValueError('No mapper device defined') + path = enc_vol.mapper_dev + else: + path = vol.safe_dev_path + + # wait a bit otherwise the mkfs will fail as it can't + # find the mapper device yet + device_handler.format(vol.fs_type, path) + + if vol.fs_type == FilesystemType.Btrfs: + device_handler.create_lvm_btrfs_subvolumes(path, vol.btrfs_subvols, vol.mount_options) + + def _lvm_create_pvs( + self, + lvm_config: LvmConfiguration, + enc_mods: Dict[PartitionModification, Luks2] = {} + ): + pv_paths: Set[Path] = set() + + for vg in lvm_config.vol_groups: + pv_paths |= self._get_all_pv_dev_paths(vg.pvs, enc_mods) + + device_handler.lvm_pv_create(pv_paths) + + def _get_all_pv_dev_paths( + self, + pvs: List[PartitionModification], + enc_mods: Dict[PartitionModification, Luks2] = {} + ) -> Set[Path]: + pv_paths: Set[Path] = set() + + for pv in pvs: + if enc_pv := enc_mods.get(pv, None): + if mapper := enc_pv.mapper_dev: + pv_paths.add(mapper) + else: + pv_paths.add(pv.safe_dev_path) + + return pv_paths + + def _encrypt_lvm_vols( + self, + lvm_config: LvmConfiguration, + enc_config: DiskEncryption, + lock_after_create: bool = True + ) -> Dict[LvmVolume, Luks2]: + enc_vols: Dict[LvmVolume, Luks2] = {} + + for vol in lvm_config.get_all_volumes(): + if vol in enc_config.lvm_volumes: + luks_handler = device_handler.encrypt( + vol.safe_dev_path, + vol.mapper_name, + enc_config.encryption_password, + lock_after_create + ) + + enc_vols[vol] = luks_handler + + return enc_vols + + def _encrypt_partitions( + self, + enc_config: DiskEncryption, + lock_after_create: bool = True + ) -> Dict[PartitionModification, Luks2]: + enc_mods: Dict[PartitionModification, Luks2] = {} + + for mod in self._disk_config.device_modifications: + partitions = mod.partitions + + # don't touch existing partitions + filtered_part = [p for p in partitions if not p.exists()] + + self._validate_partitions(filtered_part) + + # make sure all devices are unmounted + device_handler.umount_all_existing(mod.device_path) + + enc_mods = {} + + for part_mod in filtered_part: + if part_mod in enc_config.partitions: + luks_handler = device_handler.encrypt( + part_mod.safe_dev_path, + part_mod.mapper_name, + enc_config.encryption_password, + lock_after_create=lock_after_create + ) + + enc_mods[part_mod] = luks_handler + + return enc_mods + + def _lvm_vol_handle_e2scrub(self, vol_gp: LvmVolumeGroup): + # from arch wiki: + # If a logical volume will be formatted with ext4, leave at least 256 MiB + # free space in the volume group to allow using e2scrub + if any([vol.fs_type == FilesystemType.Ext4 for vol in vol_gp.volumes]): + largest_vol = max(vol_gp.volumes, key=lambda x: x.length) + + device_handler.lvm_vol_reduce( + largest_vol.safe_dev_path, + Size(256, Unit.MiB, SectorSize.default()) + ) + def _do_countdown(self) -> bool: SIG_TRIGGER = False diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 823605e3..330f61a3 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -2,7 +2,7 @@ from __future__ import annotations import re from pathlib import Path -from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple +from typing import Any, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ ModificationStatus, DeviceGeometry, SectorSize, BtrfsMountOption @@ -38,21 +38,6 @@ class PartitioningList(ListManager): display_actions = list(self._actions.values()) super().__init__(prompt, device_partitions, display_actions[:2], display_actions[3:]) - def reformat(self, data: List[PartitionModification]) -> Dict[str, Optional[PartitionModification]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[PartitionModification]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, user in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = user - - return display_data - def selected_action_display(self, partition: PartitionModification) -> str: return str(_('Partition')) @@ -258,7 +243,6 @@ class PartitioningList(ListManager): while True: value = TextInput(prompt).run().strip() size: Optional[Size] = None - if not value: size = default else: diff --git a/archinstall/lib/disk/subvolume_menu.py b/archinstall/lib/disk/subvolume_menu.py index 48afa829..ea77149d 100644 --- a/archinstall/lib/disk/subvolume_menu.py +++ b/archinstall/lib/disk/subvolume_menu.py @@ -1,9 +1,8 @@ from pathlib import Path -from typing import Dict, List, Optional, Any, TYPE_CHECKING +from typing import List, Optional, Any, TYPE_CHECKING from .device_model import SubvolumeModification from ..menu import TextInput, ListManager -from ..output import FormattedOutput if TYPE_CHECKING: _: Any @@ -18,21 +17,6 @@ class SubvolumeMenu(ListManager): ] super().__init__(prompt, btrfs_subvols, [self._actions[0]], self._actions[1:]) - def reformat(self, data: List[SubvolumeModification]) -> Dict[str, Optional[SubvolumeModification]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[SubvolumeModification]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, subvol in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = subvol - - return display_data - def selected_action_display(self, subvolume: SubvolumeModification) -> str: return str(subvolume.name) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index e65915db..1b5e779b 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -14,7 +14,6 @@ from .models.audio_configuration import Audio, AudioConfiguration from .models.users import User from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration -from .storage import storage from .configuration import save_config from .interactions import add_number_of_parallel_downloads from .interactions import ask_additional_packages_to_install @@ -30,7 +29,6 @@ from .interactions import select_additional_repositories from .interactions import select_kernel from .utils.util import format_cols from .interactions import ask_ntp -from .interactions.disk_conf import select_disk_config if TYPE_CHECKING: _: Any @@ -38,7 +36,6 @@ if TYPE_CHECKING: class GlobalMenu(AbstractMenu): def __init__(self, data_store: Dict[str, Any]): - self._defined_text = str(_('Defined')) super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3) def setup_selection_menu_options(self): @@ -54,20 +51,20 @@ class GlobalMenu(AbstractMenu): _('Locales'), lambda preset: self._locale_selection(preset), preview_func=self._prev_locale, - display_func=lambda x: self._defined_text if x else '') + display_func=lambda x: self.defined_text if x else '') self._menu_options['mirror_config'] = \ Selector( _('Mirrors'), lambda preset: self._mirror_configuration(preset), - display_func=lambda x: self._defined_text if x else '', + display_func=lambda x: self.defined_text if x else '', preview_func=self._prev_mirror_config ) self._menu_options['disk_config'] = \ Selector( _('Disk configuration'), lambda preset: self._select_disk_config(preset), - preview_func=self._prev_disk_layouts, - display_func=lambda x: self._display_disk_layout(x), + preview_func=self._prev_disk_config, + display_func=lambda x: self.defined_text if x else '', ) self._menu_options['disk_encryption'] = \ Selector( @@ -75,7 +72,8 @@ class GlobalMenu(AbstractMenu): lambda preset: self._disk_encryption(preset), preview_func=self._prev_disk_encryption, display_func=lambda x: self._display_disk_encryption(x), - dependencies=['disk_config']) + dependencies=['disk_config'] + ) self._menu_options['swap'] = \ Selector( _('Swap'), @@ -140,7 +138,7 @@ class GlobalMenu(AbstractMenu): Selector( _('Additional packages'), lambda preset: ask_additional_packages_to_install(preset), - display_func=lambda x: self._defined_text if x else '', + display_func=lambda x: self.defined_text if x else '', preview_func=self._prev_additional_pkgs, default=[]) self._menu_options['additional-repositories'] = \ @@ -247,14 +245,17 @@ class GlobalMenu(AbstractMenu): return config.type.display_msg() def _disk_encryption(self, preset: Optional[disk.DiskEncryption]) -> Optional[disk.DiskEncryption]: - mods: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection + disk_config: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection - if not mods: + if not disk_config: # this should not happen as the encryption menu has the disk_config as dependency raise ValueError('No disk layout specified') + if not disk.DiskEncryption.validate_enc(disk_config): + return None + data_store: Dict[str, Any] = {} - disk_encryption = disk.DiskEncryptionMenu(mods, data_store, preset=preset).run() + disk_encryption = disk.DiskEncryptionMenu(disk_config, data_store, preset=preset).run() return disk_encryption def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration: @@ -287,44 +288,35 @@ class GlobalMenu(AbstractMenu): return format_cols(packages, None) return None - def _prev_disk_layouts(self) -> Optional[str]: + def _prev_disk_config(self) -> Optional[str]: selector = self._menu_options['disk_config'] disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = selector.current_selection + output = '' if disk_layout_conf: - device_mods: List[disk.DeviceModification] = \ - list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications)) - - if device_mods: - output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg()) - output_btrfs = '' + output += str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg()) - for mod in device_mods: - # create partition table - partition_table = FormattedOutput.as_table(mod.partitions) - - output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n' - output_partition += partition_table + '\n' - - # create btrfs table - btrfs_partitions = list( - filter(lambda p: len(p.btrfs_subvols) > 0, mod.partitions) - ) - for partition in btrfs_partitions: - output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n' + if disk_layout_conf.lvm_config: + output += '\n{}: {}'.format(str(_('LVM configuration type')), disk_layout_conf.lvm_config.config_type.display_msg()) - output = output_partition + output_btrfs - return output.rstrip() + if output: + return output return None - def _display_disk_layout(self, current_value: Optional[disk.DiskLayoutConfiguration] = None) -> str: + def _display_disk_config(self, current_value: Optional[disk.DiskLayoutConfiguration] = None) -> str: if current_value: return current_value.config_type.display_msg() return '' def _prev_disk_encryption(self) -> Optional[str]: + disk_config: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection + + if disk_config and not disk.DiskEncryption.validate_enc(disk_config): + return str(_('LVM disk encryption with more than 2 partitions is currently not supported')) + encryption: Optional[disk.DiskEncryption] = self._menu_options['disk_encryption'].current_selection + if encryption: enc_type = disk.EncryptionType.type_to_text(encryption.encryption_type) output = str(_('Encryption type')) + f': {enc_type}\n' @@ -332,6 +324,8 @@ class GlobalMenu(AbstractMenu): if encryption.partitions: output += 'Partitions: {} selected'.format(len(encryption.partitions)) + '\n' + elif encryption.lvm_volumes: + output += 'LVM volumes: {} selected'.format(len(encryption.lvm_volumes)) + '\n' if encryption.hsm_device: output += f'HSM: {encryption.hsm_device.manufacturer}' @@ -425,10 +419,8 @@ class GlobalMenu(AbstractMenu): self, preset: Optional[disk.DiskLayoutConfiguration] = None ) -> Optional[disk.DiskLayoutConfiguration]: - disk_config = select_disk_config( - preset, - storage['arguments'].get('advanced', False) - ) + data_store: Dict[str, Any] = {} + disk_config = disk.DiskLayoutConfigurationMenu(preset, data_store).run() if disk_config != preset: self._menu_options['disk_encryption'].set_current_selection(None) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 37121118..8292a3be 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -52,7 +52,7 @@ class Installer: `Installer()` is the wrapper for most basic installation steps. It also wraps :py:func:`~archinstall.Installer.pacstrap` among other things. """ - self.base_packages = base_packages or __packages__[:3] + self._base_packages = base_packages or __packages__[:3] self.kernels = kernels or ['linux'] self._disk_config = disk_config @@ -64,11 +64,11 @@ class Installer: self.helper_flags: Dict[str, Any] = {'base': False, 'bootloader': None} for kernel in self.kernels: - self.base_packages.append(kernel) + self._base_packages.append(kernel) # If using accessibility tools in the live environment, append those to the packages list if accessibility_tools_in_use(): - self.base_packages.extend(__accessibility_packages__) + self._base_packages.extend(__accessibility_packages__) self.post_base_install: List[Callable] = [] @@ -90,6 +90,8 @@ class Installer: self._fstab_entries: List[str] = [] self._zram_enabled = False + self._disable_fstrim = False + self.pacman = Pacman(self.target, storage['arguments'].get('silent', False)) def __enter__(self) -> 'Installer': @@ -198,31 +200,71 @@ class Installer: self._verify_service_stop() def mount_ordered_layout(self): - info('Mounting partitions in order') + debug('Mounting ordered layout') + + luks_handlers: Dict[Any, Luks2] = {} + + match self._disk_encryption.encryption_type: + case disk.EncryptionType.NoEncryption: + self._mount_lvm_layout() + case disk.EncryptionType.Luks: + luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions) + case disk.EncryptionType.LvmOnLuks: + luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions) + self._import_lvm() + self._mount_lvm_layout(luks_handlers) + case disk.EncryptionType.LuksOnLvm: + self._import_lvm() + luks_handlers = self._prepare_luks_lvm(self._disk_encryption.lvm_volumes) + self._mount_lvm_layout(luks_handlers) + + # mount all regular partitions + self._mount_partition_layout(luks_handlers) + + def _mount_partition_layout(self, luks_handlers: Dict[Any, Luks2]): + debug('Mounting partition layout') + + # do not mount any PVs part of the LVM configuration + pvs = [] + if self._disk_config.lvm_config: + pvs = self._disk_config.lvm_config.get_all_pvs() for mod in self._disk_config.device_modifications: + not_pv_part_mods = list(filter(lambda x: x not in pvs, mod.partitions)) + # partitions have to mounted in the right order on btrfs the mountpoint will # be empty as the actual subvolumes are getting mounted instead so we'll use # '/' just for sorting - sorted_part_mods = sorted(mod.partitions, key=lambda x: x.mountpoint or Path('/')) - - enc_partitions = [] - if self._disk_encryption.encryption_type is not disk.EncryptionType.NoEncryption: - enc_partitions = list(set(sorted_part_mods) & set(self._disk_encryption.partitions)) - - # attempt to decrypt all luks partitions - luks_handlers = self._prepare_luks_partitions(enc_partitions) + sorted_part_mods = sorted(not_pv_part_mods, key=lambda x: x.mountpoint or Path('/')) for part_mod in sorted_part_mods: if luks_handler := luks_handlers.get(part_mod): - # mount encrypted partition self._mount_luks_partition(part_mod, luks_handler) else: - # partition is not encrypted self._mount_partition(part_mod) - def _prepare_luks_partitions(self, partitions: List[disk.PartitionModification]) -> Dict[ - disk.PartitionModification, Luks2]: + def _mount_lvm_layout(self, luks_handlers: Dict[Any, Luks2] = {}): + lvm_config = self._disk_config.lvm_config + + if not lvm_config: + debug('No lvm config defined to be mounted') + return + + debug('Mounting LVM layout') + + for vg in lvm_config.vol_groups: + sorted_vol = sorted(vg.volumes, key=lambda x: x.mountpoint or Path('/')) + + for vol in sorted_vol: + if luks_handler := luks_handlers.get(vol): + self._mount_luks_volume(vol, luks_handler) + else: + self._mount_lvm_vol(vol) + + def _prepare_luks_partitions( + self, + partitions: List[disk.PartitionModification] + ) -> Dict[disk.PartitionModification, Luks2]: return { part_mod: disk.device_handler.unlock_luks2_dev( part_mod.dev_path, @@ -233,6 +275,33 @@ class Installer: if part_mod.mapper_name and part_mod.dev_path } + def _import_lvm(self): + lvm_config = self._disk_config.lvm_config + + if not lvm_config: + debug('No lvm config defined to be imported') + return + + for vg in lvm_config.vol_groups: + disk.device_handler.lvm_import_vg(vg) + + for vol in vg.volumes: + disk.device_handler.lvm_vol_change(vol, True) + + def _prepare_luks_lvm( + self, + lvm_volumes: List[disk.LvmVolume] + ) -> Dict[disk.LvmVolume, Luks2]: + return { + vol: disk.device_handler.unlock_luks2_dev( + vol.dev_path, + vol.mapper_name, + self._disk_encryption.encryption_password + ) + for vol in lvm_volumes + if vol.mapper_name and vol.dev_path + } + def _mount_partition(self, part_mod: disk.PartitionModification): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined if part_mod.mountpoint and part_mod.dev_path: @@ -246,14 +315,32 @@ class Installer: part_mod.mount_options ) + def _mount_lvm_vol(self, volume: disk.LvmVolume): + if volume.fs_type != disk.FilesystemType.Btrfs: + if volume.mountpoint and volume.dev_path: + target = self.target / volume.relative_mountpoint + disk.device_handler.mount(volume.dev_path, target, options=volume.mount_options) + + if volume.fs_type == disk.FilesystemType.Btrfs and volume.dev_path: + self._mount_btrfs_subvol(volume.dev_path, volume.btrfs_subvols, volume.mount_options) + def _mount_luks_partition(self, part_mod: disk.PartitionModification, luks_handler: Luks2): - # it would be none if it's btrfs as the subvolumes will have the mountpoints defined - if part_mod.mountpoint and luks_handler.mapper_dev: - target = self.target / part_mod.relative_mountpoint - disk.device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options) + if part_mod.fs_type != disk.FilesystemType.Btrfs: + if part_mod.mountpoint and luks_handler.mapper_dev: + target = self.target / part_mod.relative_mountpoint + disk.device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options) if part_mod.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev: - self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols) + self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols, part_mod.mount_options) + + def _mount_luks_volume(self, volume: disk.LvmVolume, luks_handler: Luks2): + if volume.fs_type != disk.FilesystemType.Btrfs: + if volume.mountpoint and luks_handler.mapper_dev: + target = self.target / volume.relative_mountpoint + disk.device_handler.mount(luks_handler.mapper_dev, target, options=volume.mount_options) + + if volume.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev: + self._mount_btrfs_subvol(luks_handler.mapper_dev, volume.btrfs_subvols, volume.mount_options) def _mount_btrfs_subvol( self, @@ -262,13 +349,23 @@ class Installer: mount_options: List[str] = [] ): for subvol in subvolumes: - disk.device_handler.mount( - dev_path, - self.target / subvol.relative_mountpoint, - options=mount_options + [f'subvol={subvol.name}'] - ) + mountpoint = self.target / subvol.relative_mountpoint + mount_options = mount_options + [f'subvol={subvol.name}'] + disk.device_handler.mount(dev_path, mountpoint, options=mount_options) def generate_key_files(self): + match self._disk_encryption.encryption_type: + case disk.EncryptionType.Luks: + self._generate_key_files_partitions() + case disk.EncryptionType.LuksOnLvm: + self._generate_key_file_lvm_volumes() + case disk.EncryptionType.LvmOnLuks: + # currently LvmOnLuks only supports a single + # partitioning layout (boot + partition) + # so we won't need any keyfile generation atm + pass + + def _generate_key_files_partitions(self): for part_mod in self._disk_encryption.partitions: gen_enc_file = self._disk_encryption.should_generate_encryption_file(part_mod) @@ -279,14 +376,36 @@ class Installer: ) if gen_enc_file and not part_mod.is_root(): - info(f'Creating key-file: {part_mod.dev_path}') + debug(f'Creating key-file: {part_mod.dev_path}') luks_handler.create_keyfile(self.target) if part_mod.is_root() and not gen_enc_file: if self._disk_encryption.hsm_device: disk.Fido2.fido2_enroll( self._disk_encryption.hsm_device, - part_mod, + part_mod.safe_dev_path, + self._disk_encryption.encryption_password + ) + + def _generate_key_file_lvm_volumes(self): + for vol in self._disk_encryption.lvm_volumes: + gen_enc_file = self._disk_encryption.should_generate_encryption_file(vol) + + luks_handler = Luks2( + vol.safe_dev_path, + mapper_name=vol.mapper_name, + password=self._disk_encryption.encryption_password + ) + + if gen_enc_file and not vol.is_root(): + info(f'Creating key-file: {vol.dev_path}') + luks_handler.create_keyfile(self.target) + + if vol.is_root() and not gen_enc_file: + if self._disk_encryption.hsm_device: + disk.Fido2.fido2_enroll( + self._disk_encryption.hsm_device, + vol.safe_dev_path, self._disk_encryption.encryption_password ) @@ -393,7 +512,7 @@ class Installer: for entry in self._fstab_entries: fp.write(f'{entry}\n') - def set_hostname(self, hostname: str, *args: str, **kwargs: str) -> None: + def set_hostname(self, hostname: str): with open(f'{self.target}/etc/hostname', 'w') as fh: fh.write(hostname + '\n') @@ -444,7 +563,7 @@ class Installer: (self.target / 'etc/locale.conf').write_text(f'LANG={lang_value}\n') return True - def set_timezone(self, zone: str, *args: str, **kwargs: str) -> bool: + def set_timezone(self, zone: str) -> bool: if not zone: return True if not len(zone): @@ -532,7 +651,7 @@ class Installer: if enable_services: # If we haven't installed the base yet (function called pre-maturely) if self.helper_flags.get('base', False) is False: - self.base_packages.append('iwd') + self._base_packages.append('iwd') # This function will be called after minimal_installation() # as a hook for post-installs. This hook is only needed if @@ -608,51 +727,98 @@ class Installer: return vendor.get_ucode() return None - def minimal_installation( - self, - testing: bool = False, - multilib: bool = False, - mkinitcpio: bool = True, - hostname: str = 'archinstall', - locale_config: LocaleConfiguration = LocaleConfiguration.default() - ): - _disable_fstrim = False + def _handle_partition_installation(self): + pvs = [] + if self._disk_config.lvm_config: + pvs = self._disk_config.lvm_config.get_all_pvs() + for mod in self._disk_config.device_modifications: for part in mod.partitions: - if part.fs_type is not None: - if (pkg := part.fs_type.installation_pkg) is not None: - self.base_packages.append(pkg) - if (module := part.fs_type.installation_module) is not None: + if part in pvs or part.fs_type is None: + continue + + if (pkg := part.fs_type.installation_pkg) is not None: + self._base_packages.append(pkg) + if (module := part.fs_type.installation_module) is not None: + self._modules.append(module) + if (binary := part.fs_type.installation_binary) is not None: + self._binaries.append(binary) + + # https://github.com/archlinux/archinstall/issues/1837 + if part.fs_type.fs_type_mount == 'btrfs': + self._disable_fstrim = True + + # There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed. + if part.fs_type.fs_type_mount == 'ntfs3' and part.mountpoint == self.target: + if 'fsck' in self._hooks: + self._hooks.remove('fsck') + + if part in self._disk_encryption.partitions: + if self._disk_encryption.hsm_device: + # Required by mkinitcpio to add support for fido2-device options + self.pacman.strap('libfido2') + + if 'sd-encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('filesystems'), 'sd-encrypt') + else: + if 'encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('filesystems'), 'encrypt') + + def _handle_lvm_installation(self): + if not self._disk_config.lvm_config: + return + + self.add_additional_packages('lvm2') + self._hooks.insert(self._hooks.index('filesystems') - 1, 'lvm2') + + for vg in self._disk_config.lvm_config.vol_groups: + for vol in vg.volumes: + if vol.fs_type is not None: + if (pkg := vol.fs_type.installation_pkg) is not None: + self._base_packages.append(pkg) + if (module := vol.fs_type.installation_module) is not None: self._modules.append(module) - if (binary := part.fs_type.installation_binary) is not None: + if (binary := vol.fs_type.installation_binary) is not None: self._binaries.append(binary) - # https://github.com/archlinux/archinstall/issues/1837 - if part.fs_type.fs_type_mount == 'btrfs': - _disable_fstrim = True + if vol.fs_type.fs_type_mount == 'btrfs': + self._disable_fstrim = True # There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed. - if part.fs_type.fs_type_mount == 'ntfs3' and part.mountpoint == self.target: + if vol.fs_type.fs_type_mount == 'ntfs3' and vol.mountpoint == self.target: if 'fsck' in self._hooks: self._hooks.remove('fsck') - if part in self._disk_encryption.partitions: - if self._disk_encryption.hsm_device: - # Required by mkinitcpio to add support for fido2-device options - self.pacman.strap('libfido2') + if self._disk_encryption.encryption_type in [disk.EncryptionType.LvmOnLuks, disk.EncryptionType.LuksOnLvm]: + if self._disk_encryption.hsm_device: + # Required by mkinitcpio to add support for fido2-device options + self.pacman.strap('libfido2') + + if 'sd-encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('lvm2') - 1, 'sd-encrypt') + else: + if 'encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('lvm2') - 1, 'encrypt') - if 'sd-encrypt' not in self._hooks: - self._hooks.insert(self._hooks.index('filesystems'), 'sd-encrypt') - else: - if 'encrypt' not in self._hooks: - self._hooks.insert(self._hooks.index('filesystems'), 'encrypt') + def minimal_installation( + self, + testing: bool = False, + multilib: bool = False, + mkinitcpio: bool = True, + hostname: str = 'archinstall', + locale_config: LocaleConfiguration = LocaleConfiguration.default() + ): + if self._disk_config.lvm_config: + self._handle_lvm_installation() + else: + self._handle_partition_installation() if not SysInfo.has_uefi(): - self.base_packages.append('grub') + self._base_packages.append('grub') if ucode := self._get_microcode(): (self.target / 'boot' / ucode).unlink(missing_ok=True) - self.base_packages.append(ucode.stem) + self._base_packages.append(ucode.stem) else: debug('Archinstall will not install any ucode.') @@ -673,7 +839,7 @@ class Installer: pacman_conf.apply() - self.pacman.strap(self.base_packages) + self.pacman.strap(self._base_packages) self.helper_flags['base-strapped'] = True pacman_conf.persist() @@ -685,7 +851,7 @@ class Installer: # https://github.com/archlinux/archinstall/issues/880 # https://github.com/archlinux/archinstall/issues/1837 # https://github.com/archlinux/archinstall/issues/1841 - if not _disable_fstrim: + if not self._disable_fstrim: self.enable_periodic_trim() # TODO: Support locale and timezone @@ -742,13 +908,24 @@ class Installer: return boot return None - def _get_root_partition(self) -> Optional[disk.PartitionModification]: - for mod in self._disk_config.device_modifications: - if root := mod.get_root_partition(): - return root + def _get_root(self) -> Optional[disk.PartitionModification | disk.LvmVolume]: + if self._disk_config.lvm_config: + return self._disk_config.lvm_config.get_root_volume() + else: + for mod in self._disk_config.device_modifications: + if root := mod.get_root_partition(): + return root return None - def _get_kernel_params( + def _get_luks_uuid_from_mapper_dev(self, mapper_dev_path: Path) -> str: + lsblk_info = disk.get_lsblk_info(mapper_dev_path, reverse=True, full_dev_path=True) + + if not lsblk_info.children or not lsblk_info.children[0].uuid: + raise ValueError('Unable to determine UUID of luks superblock') + + return lsblk_info.children[0].uuid + + def _get_kernel_params_partition( self, root_partition: disk.PartitionModification, id_root: bool = True, @@ -784,20 +961,74 @@ class Installer: debug(f'Identifying root partition by UUID: {root_partition.uuid}') kernel_parameters.append(f'root=UUID={root_partition.uuid}') + return kernel_parameters + + def _get_kernel_params_lvm( + self, + lvm: disk.LvmVolume + ) -> List[str]: + kernel_parameters = [] + + match self._disk_encryption.encryption_type: + case disk.EncryptionType.LvmOnLuks: + if not lvm.vg_name: + raise ValueError(f'Unable to determine VG name for {lvm.name}') + + pv_seg_info = disk.device_handler.lvm_pvseg_info(lvm.vg_name, lvm.name) + + if not pv_seg_info: + raise ValueError(f'Unable to determine PV segment info for {lvm.vg_name}/{lvm.name}') + + uuid = self._get_luks_uuid_from_mapper_dev(pv_seg_info.pv_name) + + if self._disk_encryption.hsm_device: + debug(f'LvmOnLuks, encrypted root partition, HSM, identifying by UUID: {uuid}') + kernel_parameters.append(f'rd.luks.name={uuid}=cryptlvm root={lvm.safe_dev_path}') + else: + debug(f'LvmOnLuks, encrypted root partition, identifying by UUID: {uuid}') + kernel_parameters.append(f'cryptdevice=UUID={uuid}:cryptlvm root={lvm.safe_dev_path}') + case disk.EncryptionType.LuksOnLvm: + uuid = self._get_luks_uuid_from_mapper_dev(lvm.mapper_path) + + if self._disk_encryption.hsm_device: + debug(f'LuksOnLvm, encrypted root partition, HSM, identifying by UUID: {uuid}') + kernel_parameters.append(f'rd.luks.name={uuid}=root root=/dev/mapper/root') + else: + debug(f'LuksOnLvm, encrypted root partition, identifying by UUID: {uuid}') + kernel_parameters.append(f'cryptdevice=UUID={uuid}:root root=/dev/mapper/root') + case disk.EncryptionType.NoEncryption: + debug(f'Identifying root lvm by mapper device: {lvm.dev_path}') + kernel_parameters.append(f'root={lvm.safe_dev_path}') + + return kernel_parameters + + def _get_kernel_params( + self, + root: disk.PartitionModification | disk.LvmVolume, + id_root: bool = True, + partuuid: bool = True + ) -> List[str]: + kernel_parameters = [] + + if isinstance(root, disk.LvmVolume): + kernel_parameters = self._get_kernel_params_lvm(root) + else: + kernel_parameters = self._get_kernel_params_partition(root, id_root, partuuid) + # Zswap should be disabled when using zram. # https://github.com/archlinux/archinstall/issues/881 if self._zram_enabled: kernel_parameters.append('zswap.enabled=0') if id_root: - for sub_vol in root_partition.btrfs_subvols: + for sub_vol in root.btrfs_subvols: if sub_vol.is_root(): kernel_parameters.append(f'rootflags=subvol={sub_vol.name}') break kernel_parameters.append('rw') - kernel_parameters.append(f'rootfstype={root_partition.safe_fs_type.fs_type_mount}') + kernel_parameters.append(f'rootfstype={root.safe_fs_type.fs_type_mount}') kernel_parameters.extend(self._kernel_params) debug(f'kernel parameters: {" ".join(kernel_parameters)}') @@ -807,10 +1038,12 @@ class Installer: def _add_systemd_bootloader( self, boot_partition: disk.PartitionModification, - root_partition: disk.PartitionModification, + root: disk.PartitionModification | disk.LvmVolume, efi_partition: Optional[disk.PartitionModification], uki_enabled: bool = False ): + debug('Installing systemd bootloader') + self.pacman.strap('efibootmgr') if not SysInfo.has_uefi(): @@ -882,7 +1115,7 @@ class Installer: f'# Created on: {self.init_time}' ) - options = 'options ' + ' '.join(self._get_kernel_params(root_partition)) + options = 'options ' + ' '.join(self._get_kernel_params(root)) for kernel in self.kernels: for variant in ("", "-fallback"): @@ -904,15 +1137,17 @@ class Installer: def _add_grub_bootloader( self, boot_partition: disk.PartitionModification, - root_partition: disk.PartitionModification, + root: disk.PartitionModification | disk.LvmVolume, efi_partition: Optional[disk.PartitionModification] ): + debug('Installing grub bootloader') + self.pacman.strap('grub') # no need? grub_default = self.target / 'etc/default/grub' config = grub_default.read_text() - kernel_parameters = ' '.join(self._get_kernel_params(root_partition, False, False)) + kernel_parameters = ' '.join(self._get_kernel_params(root, False, False)) config = re.sub(r'(GRUB_CMDLINE_LINUX=")("\n)', rf'\1{kernel_parameters}\2', config, 1) grub_default.write_text(config) @@ -934,7 +1169,7 @@ class Installer: info(f"GRUB EFI partition: {efi_partition.dev_path}") - self.pacman.strap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? + self.pacman.strap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? boot_dir_arg = [] if boot_partition.mountpoint and boot_partition.mountpoint != boot_dir: @@ -988,8 +1223,10 @@ class Installer: self, boot_partition: disk.PartitionModification, efi_partition: Optional[disk.PartitionModification], - root_partition: disk.PartitionModification + root: disk.PartitionModification | disk.LvmVolume ): + debug('Installing limine bootloader') + self.pacman.strap('limine') info(f"Limine boot partition: {boot_partition.dev_path}") @@ -1052,7 +1289,7 @@ Exec = /bin/sh -c "{hook_command}" hook_path = hooks_dir / '99-limine.hook' hook_path.write_text(hook_contents) - kernel_params = ' '.join(self._get_kernel_params(root_partition)) + kernel_params = ' '.join(self._get_kernel_params(root)) config_contents = 'TIMEOUT=5\n' for kernel in self.kernels: @@ -1075,9 +1312,11 @@ Exec = /bin/sh -c "{hook_command}" def _add_efistub_bootloader( self, boot_partition: disk.PartitionModification, - root_partition: disk.PartitionModification, + root: disk.PartitionModification | disk.LvmVolume, uki_enabled: bool = False ): + debug('Installing efistub bootloader') + self.pacman.strap('efibootmgr') if not SysInfo.has_uefi(): @@ -1092,7 +1331,7 @@ Exec = /bin/sh -c "{hook_command}" entries = ( 'initrd=/initramfs-{kernel}.img', - *self._get_kernel_params(root_partition) + *self._get_kernel_params(root) ) cmdline = [' '.join(entries)] @@ -1122,7 +1361,7 @@ Exec = /bin/sh -c "{hook_command}" def _config_uki( self, - root_partition: disk.PartitionModification, + root: disk.PartitionModification | disk.LvmVolume, efi_partition: Optional[disk.PartitionModification] ): if not efi_partition or not efi_partition.mountpoint: @@ -1130,7 +1369,7 @@ Exec = /bin/sh -c "{hook_command}" # Set up kernel command line with open(self.target / 'etc/kernel/cmdline', 'w') as cmdline: - kernel_parameters = self._get_kernel_params(root_partition) + kernel_parameters = self._get_kernel_params(root) cmdline.write(' '.join(kernel_parameters) + '\n') diff_mountpoint = None @@ -1191,37 +1430,33 @@ Exec = /bin/sh -c "{hook_command}" efi_partition = self._get_efi_partition() boot_partition = self._get_boot_partition() - root_partition = self._get_root_partition() + root = self._get_root() if boot_partition is None: raise ValueError(f'Could not detect boot at mountpoint {self.target}') - if root_partition is None: + if root is None: raise ValueError(f'Could not detect root at mountpoint {self.target}') info(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}') if uki_enabled: - self._config_uki(root_partition, efi_partition) + self._config_uki(root, efi_partition) match bootloader: case Bootloader.Systemd: - self._add_systemd_bootloader(boot_partition, root_partition, efi_partition, uki_enabled) + self._add_systemd_bootloader(boot_partition, root, efi_partition, uki_enabled) case Bootloader.Grub: - self._add_grub_bootloader(boot_partition, root_partition, efi_partition) + self._add_grub_bootloader(boot_partition, root, efi_partition) case Bootloader.Efistub: - self._add_efistub_bootloader(boot_partition, root_partition, uki_enabled) + self._add_efistub_bootloader(boot_partition, root, uki_enabled) case Bootloader.Limine: - self._add_limine_bootloader(boot_partition, efi_partition, root_partition) + self._add_limine_bootloader(boot_partition, efi_partition, root) def add_additional_packages(self, packages: Union[str, List[str]]) -> bool: return self.pacman.strap(packages) - def _enable_users(self, service: str, users: List[User]): - for user in users: - self.arch_chroot(f'systemctl enable --user {service}', run_as=user.username) - - def enable_sudo(self, entity: str, group :bool = False): + def enable_sudo(self, entity: str, group: bool = False): info(f'Enabling sudo permissions for {entity}') sudoers_dir = f"{self.target}/etc/sudoers.d" @@ -1237,7 +1472,7 @@ Exec = /bin/sh -c "{hook_command}" # We count how many files are there already so we know which number to prefix the file with num_of_rules_already = len(os.listdir(sudoers_dir)) - file_num_str = "{:02d}".format(num_of_rules_already) # We want 00_user1, 01_user2, etc + file_num_str = "{:02d}".format(num_of_rules_already) # We want 00_user1, 01_user2, etc # Guarantees that entity str does not contain invalid characters for a linux file name: # \ / : * ? " < > | @@ -1293,7 +1528,7 @@ Exec = /bin/sh -c "{hook_command}" if sudo and self.enable_sudo(user): self.helper_flags['user'] = True - def user_set_pw(self, user :str, password :str) -> bool: + def user_set_pw(self, user: str, password: str) -> bool: info(f'Setting password for {user}') if user == 'root': @@ -1310,7 +1545,7 @@ Exec = /bin/sh -c "{hook_command}" except SysCallError: return False - def user_set_shell(self, user :str, shell :str) -> bool: + def user_set_shell(self, user: str, shell: str) -> bool: info(f'Setting shell for {user} to {shell}') try: @@ -1319,7 +1554,7 @@ Exec = /bin/sh -c "{hook_command}" except SysCallError: return False - def chown(self, owner :str, path :str, options :List[str] = []) -> bool: + def chown(self, owner: str, path: str, options: List[str] = []) -> bool: cleaned_path = path.replace('\'', '\\\'') try: SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c 'chown {' '.join(options)} {owner} {cleaned_path}'") diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 9d0042d6..f80af9ca 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -58,7 +58,7 @@ def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]: case MenuSelectionType.Reset: return [] case MenuSelectionType.Skip: return preset case MenuSelectionType.Selection: - selected_device_info: List[disk._DeviceInfo] = choice.value # type: ignore + selected_device_info: List[disk._DeviceInfo] = choice.single_value selected_devices = [] for device in devices: @@ -73,7 +73,6 @@ def get_default_partition_layout( filesystem_type: Optional[disk.FilesystemType] = None, advanced_option: bool = False ) -> List[disk.DeviceModification]: - if len(devices) == 1: device_modification = suggest_single_disk_layout( devices[0], @@ -133,7 +132,7 @@ def select_disk_config( case MenuSelectionType.Reset: return None case MenuSelectionType.Selection: if choice.single_value == pre_mount_mode: - output = "You will use whatever drive-setup is mounted at the specified directory\n" + output = 'You will use whatever drive-setup is mounted at the specified directory\n' output += "WARNING: Archinstall won't check the suitability of this setup\n" try: @@ -151,7 +150,6 @@ def select_disk_config( ) preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] - devices = select_devices(preset_devices) if not devices: @@ -177,6 +175,36 @@ def select_disk_config( return None +def select_lvm_config( + disk_config: disk.DiskLayoutConfiguration, + preset: Optional[disk.LvmConfiguration] = None, +) -> Optional[disk.LvmConfiguration]: + default_mode = disk.LvmLayoutType.Default.display_msg() + + options = [default_mode] + + preset_value = preset.config_type.display_msg() if preset else None + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select a LVM option'), + options, + allow_reset=True, + allow_reset_warning_msg=warning, + sort=False, + preview_size=0.2, + preset_values=preset_value + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + case MenuSelectionType.Selection: + if choice.single_value == default_mode: + return suggest_lvm_layout(disk_config) + return preset + + def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.PartitionModification: flags = [disk.PartitionFlag.Boot] if using_gpt: @@ -199,7 +227,7 @@ def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.Parti ) -def select_main_filesystem_format(advanced_options=False) -> disk.FilesystemType: +def select_main_filesystem_format(advanced_options: bool = False) -> disk.FilesystemType: options = { 'btrfs': disk.FilesystemType.Btrfs, 'ext4': disk.FilesystemType.Ext4, @@ -250,7 +278,6 @@ def suggest_single_disk_layout( prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() using_subvolumes = choice.value == Menu.yes() - mount_options = select_mount_options() device_modification = disk.DeviceModification(device, wipe=True) @@ -288,7 +315,11 @@ def suggest_single_disk_layout( root_start = boot_partition.start + boot_partition.length # Set a size for / (/root) - if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: + if ( + using_subvolumes + or device_size_gib < min_size_to_allow_home_part + or not using_home_partition + ): root_length = device.device_info.total_size - root_start else: root_length = min(device.device_info.total_size, root_partition_size) @@ -305,6 +336,7 @@ def suggest_single_disk_layout( fs_type=filesystem_type, mount_options=mount_options ) + device_modification.add_partition(root_partition) if using_subvolumes: @@ -388,9 +420,9 @@ def suggest_multi_disk_layout( device_paths = ', '.join([str(d.device_info.path) for d in devices]) - debug(f"Suggesting multi-disk-layout for devices: {device_paths}") - debug(f"/root: {root_device.device_info.path}") - debug(f"/home: {home_device.device_info.path}") + debug(f'Suggesting multi-disk-layout for devices: {device_paths}') + debug(f'/root: {root_device.device_info.path}') + debug(f'/home: {home_device.device_info.path}') root_device_modification = disk.DeviceModification(root_device, wipe=True) home_device_modification = disk.DeviceModification(home_device, wipe=True) @@ -444,3 +476,85 @@ def suggest_multi_disk_layout( home_device_modification.add_partition(home_partition) return [root_device_modification, home_device_modification] + + +def suggest_lvm_layout( + disk_config: disk.DiskLayoutConfiguration, + filesystem_type: Optional[disk.FilesystemType] = None, + vg_grp_name: str = 'ArchinstallVg', +) -> disk.LvmConfiguration: + if disk_config.config_type != disk.DiskLayoutType.Default: + raise ValueError('LVM suggested volumes are only available for default partitioning') + + using_subvolumes = False + btrfs_subvols = [] + home_volume = True + mount_options = [] + + if not filesystem_type: + filesystem_type = select_main_filesystem_format() + + if filesystem_type == disk.FilesystemType.Btrfs: + prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + using_subvolumes = choice.value == Menu.yes() + + mount_options = select_mount_options() + + if using_subvolumes: + btrfs_subvols = [ + disk.SubvolumeModification(Path('@'), Path('/')), + disk.SubvolumeModification(Path('@home'), Path('/home')), + disk.SubvolumeModification(Path('@log'), Path('/var/log')), + disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), + disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')), + ] + + home_volume = False + + boot_part: Optional[disk.PartitionModification] = None + other_part: List[disk.PartitionModification] = [] + + for mod in disk_config.device_modifications: + for part in mod.partitions: + if part.is_boot(): + boot_part = part + else: + other_part.append(part) + + if not boot_part: + raise ValueError('Unable to find boot partition in partition modifications') + + total_vol_available = sum( + [p.length for p in other_part], + disk.Size(0, disk.Unit.B, disk.SectorSize.default()), + ) + root_vol_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default()) + home_vol_size = total_vol_available - root_vol_size + + lvm_vol_group = disk.LvmVolumeGroup(vg_grp_name, pvs=other_part, ) + + root_vol = disk.LvmVolume( + status=disk.LvmVolumeStatus.Create, + name='root', + fs_type=filesystem_type, + length=root_vol_size, + mountpoint=Path('/'), + btrfs_subvols=btrfs_subvols, + mount_options=mount_options + ) + + lvm_vol_group.volumes.append(root_vol) + + if home_volume: + home_vol = disk.LvmVolume( + status=disk.LvmVolumeStatus.Create, + name='home', + fs_type=filesystem_type, + length=home_vol_size, + mountpoint=Path('/home'), + ) + + lvm_vol_group.volumes.append(home_vol) + + return disk.LvmConfiguration(disk.LvmLayoutType.Default, [lvm_vol_group]) diff --git a/archinstall/lib/interactions/manage_users_conf.py b/archinstall/lib/interactions/manage_users_conf.py index ca912283..886f85b6 100644 --- a/archinstall/lib/interactions/manage_users_conf.py +++ b/archinstall/lib/interactions/manage_users_conf.py @@ -1,12 +1,11 @@ from __future__ import annotations import re -from typing import Any, Dict, TYPE_CHECKING, List, Optional +from typing import Any, TYPE_CHECKING, List, Optional from .utils import get_password from ..menu import Menu, ListManager from ..models.users import User -from ..output import FormattedOutput if TYPE_CHECKING: _: Any @@ -26,21 +25,6 @@ class UserList(ListManager): ] super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) - def reformat(self, data: List[User]) -> Dict[str, Any]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[User]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, user in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = user - - return display_data - def selected_action_display(self, user: User) -> str: return user.username diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index c917420e..50e15cee 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -60,7 +60,7 @@ class Luks2: iter_time: int = 10000, key_file: Optional[Path] = None ) -> Path: - info(f'Luks2 encrypting: {self.luks_dev_path}') + debug(f'Luks2 encrypting: {self.luks_dev_path}') byte_password = self._password_bytes() @@ -87,12 +87,15 @@ class Luks2: 'luksFormat', str(self.luks_dev_path), ]) + debug(f'cryptsetup format: {cryptsetup_args}') + # Retry formatting the volume because archinstall can some times be too quick # which generates a "Device /dev/sdX does not exist or access denied." between # setting up partitions and us trying to encrypt it. for retry_attempt in range(storage['DISK_RETRY_ATTEMPTS'] + 1): try: - SysCommand(cryptsetup_args) + result = SysCommand(cryptsetup_args).decode() + debug(f'cryptsetup luksFormat output: {result}') break except SysCallError as err: time.sleep(storage['DISK_TIMEOUTS']) @@ -106,10 +109,13 @@ class Luks2: self.lock() # Then try again to set up the crypt-device - SysCommand(cryptsetup_args) + result = SysCommand(cryptsetup_args).decode() + debug(f'cryptsetup luksFormat output: {result}') else: raise DiskError(f'Could not encrypt volume "{self.luks_dev_path}": {err}') + self.key_file = key_file + return key_file def _get_luks_uuid(self) -> str: @@ -152,7 +158,15 @@ class Luks2: while Path(self.luks_dev_path).exists() is False and time.time() - wait_timer < 10: time.sleep(0.025) - SysCommand(f'/usr/bin/cryptsetup open {self.luks_dev_path} {self.mapper_name} --key-file {key_file} --type luks2') + result = SysCommand( + '/usr/bin/cryptsetup open ' + f'{self.luks_dev_path} ' + f'{self.mapper_name} ' + f'--key-file {key_file} ' + f'--type luks2' + ).decode() + + debug(f'cryptsetup open output: {result}') if not self.mapper_dev or not self.mapper_dev.is_symlink(): raise DiskError(f'Failed to open luks2 device: {self.luks_dev_path}') @@ -199,8 +213,8 @@ class Luks2: key_file.parent.mkdir(parents=True, exist_ok=True) - with open(key_file, "w") as keyfile: - keyfile.write(generate_password(length=512)) + pwd = generate_password(length=512) + key_file.write_text(pwd) key_file.chmod(0o400) @@ -208,7 +222,7 @@ class Luks2: self._crypttab(crypttab_path, kf_path, options=["luks", "key-slot=1"]) def _add_key(self, key_file: Path): - info(f'Adding additional key-file {key_file}') + debug(f'Adding additional key-file {key_file}') command = f'/usr/bin/cryptsetup -q -v luksAddKey {self.luks_dev_path} {key_file}' worker = SysCommandWorker(command, environment_vars={'LC_ALL': 'C'}) @@ -228,7 +242,7 @@ class Luks2: key_file: Path, options: List[str] ) -> None: - info(f'Adding crypttab entry for key {key_file}') + debug(f'Adding crypttab entry for key {key_file}') with open(crypttab_path, 'a') as crypttab: opt = ','.join(options) diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 14db98ca..ee55f5c9 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -10,6 +10,7 @@ from ..translationhandler import TranslationHandler, Language if TYPE_CHECKING: _: Any + class Selector: def __init__( self, @@ -68,42 +69,19 @@ class Selector: :param no_store: A boolean which determines that the field should or shouldn't be stored in the data storage :type no_store: bool """ - self._description = description - self.func = func self._display_func = display_func - self._current_selection = default + self._no_store = no_store + + self.description = description + self.func = func + self.current_selection = default self.enabled = enabled - self._dependencies = dependencies - self._dependencies_not = dependencies_not + self.dependencies = dependencies + self.dependencies_not = dependencies_not self.exec_func = exec_func - self._preview_func = preview_func + self.preview_func = preview_func self.mandatory = mandatory - self._no_store = no_store - self._default = default - - @property - def default(self) -> Any: - return self._default - - @property - def description(self) -> str: - return self._description - - @property - def dependencies(self) -> List: - return self._dependencies - - @property - def dependencies_not(self) -> List: - return self._dependencies_not - - @property - def current_selection(self) -> Optional[Any]: - return self._current_selection - - @property - def preview_func(self): - return self._preview_func + self.default = default def do_store(self) -> bool: return self._no_store is False @@ -112,45 +90,45 @@ class Selector: self.enabled = status def update_description(self, description: str): - self._description = description + self.description = description def menu_text(self, padding: int = 0) -> str: - if self._description == '': # special menu option for __separator__ + if self.description == '': # special menu option for __separator__ return '' current = '' if self._display_func: - current = self._display_func(self._current_selection) + current = self._display_func(self.current_selection) else: - if self._current_selection is not None: - current = str(self._current_selection) + if self.current_selection is not None: + current = str(self.current_selection) if current: padding += 5 - description = unicode_ljust(str(self._description), padding, ' ') + description = unicode_ljust(str(self.description), padding, ' ') current = current else: - description = self._description + description = self.description current = '' return f'{description} {current}' def set_current_selection(self, current: Optional[Any]): - self._current_selection = current + self.current_selection = current def has_selection(self) -> bool: - if not self._current_selection: + if not self.current_selection: return False return True def get_selection(self) -> Any: - return self._current_selection + return self.current_selection def is_empty(self) -> bool: - if self._current_selection is None: + if self.current_selection is None: return True - elif isinstance(self._current_selection, (str, list, dict)) and len(self._current_selection) == 0: + elif isinstance(self.current_selection, (str, list, dict)) and len(self.current_selection) == 0: return True return False @@ -197,6 +175,8 @@ class AbstractMenu: self._sync_all() self._populate_default_values() + self.defined_text = str(_('Defined')) + @property def last_choice(self): return self._last_choice @@ -382,9 +362,10 @@ class AbstractMenu: result = None if selector.func is not None: - presel_val = self.option(config_name).get_selection() - result = selector.func(presel_val) + cur_value = self.option(config_name).get_selection() + result = selector.func(cur_value) self._menu_options[config_name].set_current_selection(result) + if selector.do_store(): self._data_store[config_name] = result @@ -398,19 +379,23 @@ class AbstractMenu: return True def _verify_selection_enabled(self, selection_name: str) -> bool: - """ general """ if selection := self._menu_options.get(selection_name, None): if not selection.enabled: return False if len(selection.dependencies) > 0: - for d in selection.dependencies: - if not self._verify_selection_enabled(d) or self._menu_options[d].is_empty(): - return False + for dep in selection.dependencies: + if isinstance(dep, str): + if not self._verify_selection_enabled(dep) or self._menu_options[dep].is_empty(): + return False + elif callable(dep): # callable dependency eval + return dep() + else: + raise ValueError(f'Unsupported dependency: {selection_name}') if len(selection.dependencies_not) > 0: - for d in selection.dependencies_not: - if not self._menu_options[d].is_empty(): + for dep in selection.dependencies_not: + if not self._menu_options[dep].is_empty(): return False return True @@ -454,8 +439,8 @@ class AbstractMenu: class AbstractSubMenu(AbstractMenu): - def __init__(self, data_store: Dict[str, Any] = {}): - super().__init__(data_store=data_store) + def __init__(self, data_store: Dict[str, Any] = {}, preview_size: float = 0.2): + super().__init__(data_store=data_store, preview_size=preview_size) self._menu_options['__separator__'] = Selector('') self._menu_options['back'] = \ diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index 54fb6a1b..de18791c 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -3,6 +3,7 @@ from os import system from typing import Any, TYPE_CHECKING, Dict, Optional, Tuple, List from .menu import Menu +from ..output import FormattedOutput if TYPE_CHECKING: _: Any @@ -127,18 +128,29 @@ class ListManager: if choice.value and choice.value != self._cancel_action: self._data = self.handle_action(choice.value, entry, self._data) - def selected_action_display(self, selection: Any) -> str: + def reformat(self, data: List[Any]) -> Dict[str, Optional[Any]]: """ - this will return the value to be displayed in the - "Select an action for '{}'" string + Default implementation of the table to be displayed. + Override if any custom formatting is needed """ - raise NotImplementedError('Please implement me in the child class') + table = FormattedOutput.as_table(data) + rows = table.split('\n') - def reformat(self, data: List[Any]) -> Dict[str, Optional[Any]]: + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[Any]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, entry in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = entry + + return display_data + + def selected_action_display(self, selection: Any) -> str: """ - this should return a dictionary of display string to actual data entry - mapping; if the value for a given display string is None it will be used - in the header value (useful when displaying tables) + this will return the value to be displayed in the + "Select an action for '{}'" string """ raise NotImplementedError('Please implement me in the child class') diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index f14b855d..38301d3a 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -66,7 +66,7 @@ class Menu(TerminalMenu): sort: bool = True, preset_values: Optional[Union[str, List[str]]] = None, cursor_index: Optional[int] = None, - preview_command: Optional[Callable] = None, + preview_command: Optional[Callable[[Any], str | None]] = None, preview_size: float = 0.0, preview_title: str = 'Info', header: Union[List[str], str] = [], @@ -228,7 +228,11 @@ class Menu(TerminalMenu): default_str = str(_('(default)')) return f'{self._default_option} {default_str}' - def _show_preview(self, preview_command: Optional[Callable], selection: str) -> Optional[str]: + def _show_preview( + self, + preview_command: Optional[Callable[[Any], str | None]], + selection: str + ) -> Optional[str]: if selection == self.back(): return None diff --git a/archinstall/lib/menu/table_selection_menu.py b/archinstall/lib/menu/table_selection_menu.py index 4cff7216..fec6ae59 100644 --- a/archinstall/lib/menu/table_selection_menu.py +++ b/archinstall/lib/menu/table_selection_menu.py @@ -19,6 +19,7 @@ class TableMenu(Menu): preview_size: float = 0.0, allow_reset: bool = True, allow_reset_warning_msg: Optional[str] = None, + skip: bool = True ): """ param title: Text that will be displayed above the menu @@ -81,7 +82,8 @@ class TableMenu(Menu): preview_title=preview_title, extra_bottom_space=extra_bottom_space, allow_reset=allow_reset, - allow_reset_warning_msg=allow_reset_warning_msg + allow_reset_warning_msg=allow_reset_warning_msg, + skip=skip ) def _preset_values(self, preset: List[Any]) -> List[str]: diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 61f3c568..c9094669 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -121,21 +121,6 @@ class CustomMirrorList(ListManager): ] super().__init__(prompt, custom_mirrors, [self._actions[0]], self._actions[1:]) - def reformat(self, data: List[CustomMirror]) -> Dict[str, Any]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[CustomMirror]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, user in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = user - - return display_data - def selected_action_display(self, mirror: CustomMirror) -> str: return mirror.name diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index b1fc8fd9..385ff500 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -104,7 +104,7 @@ def perform_installation(mountpoint: Path): Only requirement is that the block devices are formatted and setup prior to entering this function. """ - info('Starting installation') + info('Starting installation...') disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] # Retrieve list of additional repositories and set boolean values appropriately diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 3c9a5876..4513b6f2 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -82,7 +82,7 @@ def perform_installation(mountpoint: Path): Only requirement is that the block devices are formatted and setup prior to entering this function. """ - info('Starting installation') + info('Starting installation...') disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] # Retrieve list of additional repositories and set boolean values appropriately -- cgit v1.2.3-70-g09d2 From 3af850632f5fe94dfe05ffcf865d8697f864433c Mon Sep 17 00:00:00 2001 From: Kevin FERRIER <13919713+kevin-ferrier@users.noreply.github.com> Date: Thu, 9 May 2024 05:37:38 +0200 Subject: change default root partition size (#2415) --- archinstall/lib/interactions/disk_conf.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) (limited to 'archinstall/lib/interactions') diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index f80af9ca..4fce4fe5 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -257,6 +257,21 @@ def select_mount_options() -> List[str]: return [] +def process_root_partition_size(available_space: disk.Size, sector_size: disk.SectorSize) -> disk.Size: + # root partition size processing + total_device_size = available_space.convert(disk.Unit.GiB) + if total_device_size.value > 500: + # maximum size + return disk.Size(value=50, unit=disk.Unit.GiB, sector_size=sector_size) + elif total_device_size.value < 200: + # minimum size + return disk.Size(value=20, unit=disk.Unit.GiB, sector_size=sector_size) + else: + # 10% of total size + length = total_device_size.value // 10 + return disk.Size(value=length, unit=disk.Unit.GiB, sector_size=sector_size) + + def suggest_single_disk_layout( device: disk.BDevice, filesystem_type: Optional[disk.FilesystemType] = None, @@ -267,8 +282,9 @@ def suggest_single_disk_layout( filesystem_type = select_main_filesystem_format(advanced_options) sector_size = device.device_info.sector_size + total_size = device.device_info.total_size min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB, sector_size) - root_partition_size = disk.Size(20, disk.Unit.GiB, sector_size) + root_partition_size = process_root_partition_size(available_space=total_size, sector_size=sector_size) using_subvolumes = False using_home_partition = False mount_options = [] @@ -315,14 +331,10 @@ def suggest_single_disk_layout( root_start = boot_partition.start + boot_partition.length # Set a size for / (/root) - if ( - using_subvolumes - or device_size_gib < min_size_to_allow_home_part - or not using_home_partition - ): - root_length = device.device_info.total_size - root_start + if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: + root_length = total_size - root_start else: - root_length = min(device.device_info.total_size, root_partition_size) + root_length = min(total_size, root_partition_size) if using_gpt and not using_home_partition: root_length -= align_buffer @@ -356,7 +368,7 @@ def suggest_single_disk_layout( # But we want to be able to reuse data between re-installs.. # A second partition for /home would be nice if we have the space for it home_start = root_partition.start + root_partition.length - home_length = device.device_info.total_size - home_start + home_length = total_size - home_start if using_gpt: home_length -= align_buffer -- cgit v1.2.3-70-g09d2