From 2de153003ed5de1018639070fabc9c9e583c49d1 Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Sun, 29 May 2022 15:31:18 +0800 Subject: Fix typos (#1265) --- archinstall/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'archinstall/__init__.py') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 7edeaa80..786be1c5 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -91,7 +91,7 @@ def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, erro --argument=value --argument = value --argument (boolean as default) - the optional paramters to the function alter a bit its behaviour: + the optional parameters to the function alter a bit its behaviour: * multiple allows multivalued arguments, each value separated by whitespace. They're returned as a list * error. If set any non correctly specified argument-value pair to raise an exception. Else, simply notifies the existence of a problem and continues processing. @@ -104,7 +104,7 @@ def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, erro key = None last_key = None while tmp_list: - element = tmp_list.pop(0) # retreive an element of the list + element = tmp_list.pop(0) # retrieve an element of the list if element.startswith('--'): # is an argument ? if '=' in element: # uses the arg=value syntax ? key, value = [x.strip() for x in element[2:].split('=', 1)] -- cgit v1.2.3-70-g09d2 From 7943dd82365fd9fb5034a0f1c05de3ccabda468a Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Thu, 2 Jun 2022 13:32:42 +0200 Subject: Added more offline functionality, such as skipping package search (#1296) * Added more offline functionality, such as skipping package search * Disabled list_mirrors() from going online if --offline is given. Defaults to /etc/pacman.d/mirrorlist instead. * Forgot import of pathlib * Made list_mirrors() open /etc/pacman.d/mirrorlist in byte mode to better emulate the result of urllib response reading. * Forgot variable declaration * Made list_mirrors include activated server definitions --- archinstall/__init__.py | 3 ++ archinstall/lib/mirrors.py | 27 ++++++++++++----- archinstall/lib/user_interaction/general_conf.py | 37 ++++++++++++------------ examples/guided.py | 2 +- 4 files changed, 43 insertions(+), 26 deletions(-) (limited to 'archinstall/__init__.py') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 786be1c5..ee5e5f45 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -81,6 +81,8 @@ 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("--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.") + parser.add_argument("--no-pkg-lookups", action="store_true", default=False, help="Disabled package validation specifically prior to starting installation.") parser.add_argument("--plugin", nargs="?", type=str) def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, error :bool = False) -> dict: @@ -172,6 +174,7 @@ def get_arguments() -> Dict[str, Any]: # avoiding a compatibility issue if 'dry-run' in config: del config['dry-run'] + return config def load_config(): diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 73921cef..d76e0473 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -1,10 +1,12 @@ import logging +import pathlib import urllib.error import urllib.request from typing import Union, Mapping, Iterable, Dict, Any, List from .general import SysCommand from .output import log +from .storage import storage def sort_mirrorlist(raw_data :bytes, sort_order=["https", "http"]) -> bytes: """ @@ -144,16 +146,22 @@ def re_rank_mirrors( def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]: - url = "https://archlinux.org/mirrorlist/?protocol=https&protocol=http&ip_version=4&ip_version=6&use_mirror_status=on" regions = {} - try: - response = urllib.request.urlopen(url) - except urllib.error.URLError as err: - log(f'Could not fetch an active mirror-list: {err}', level=logging.WARNING, fg="orange") - return regions + if storage['arguments']['offline']: + with pathlib.Path('/etc/pacman.d/mirrorlist').open('rb') as fh: + mirrorlist = fh.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: + log(f'Could not fetch an active mirror-list: {err}', level=logging.WARNING, fg="orange") + return regions + + mirrorlist = response.read() - mirrorlist = response.read() if sort_order: mirrorlist = sort_mirrorlist(mirrorlist, sort_order=sort_order) @@ -170,5 +178,10 @@ def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]: url = line.lstrip('#Server = ') regions[region][url] = True + elif line.startswith('Server = '): + regions.setdefault(region, {}) + + url = line.lstrip('Server = ') + regions[region][url] = True return regions diff --git a/archinstall/lib/user_interaction/general_conf.py b/archinstall/lib/user_interaction/general_conf.py index f1d56fc1..70a0e73f 100644 --- a/archinstall/lib/user_interaction/general_conf.py +++ b/archinstall/lib/user_interaction/general_conf.py @@ -3,8 +3,6 @@ from __future__ import annotations import logging from typing import List, Any, Optional, Dict, TYPE_CHECKING -import archinstall - from ..menu.menu import MenuSelectionType from ..menu.text_input import TextInput @@ -17,6 +15,8 @@ from ..mirrors import list_mirrors from ..translation import Translation from ..packages.packages import validate_package_list +from ..storage import storage + if TYPE_CHECKING: _: Any @@ -155,11 +155,11 @@ def select_profile(preset) -> Optional[Profile]: case MenuSelectionType.Selection: return options[selection.value] if selection.value is not None else None case MenuSelectionType.Ctrl_c: - archinstall.storage['profile_minimal'] = False - archinstall.storage['_selected_servers'] = [] - archinstall.storage['_desktop_profile'] = None - archinstall.arguments['desktop-environment'] = None - archinstall.arguments['gfx_driver_packages'] = None + storage['profile_minimal'] = False + storage['_selected_servers'] = [] + storage['_desktop_profile'] = None + storage['arguments']['desktop-environment'] = None + storage['arguments']['gfx_driver_packages'] = None return None case MenuSelectionType.Esc: return None @@ -178,17 +178,18 @@ def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List pre_set_packages = pre_set_packages if pre_set_packages else [] packages = read_packages(pre_set_packages) - 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: - log(f"Some packages could not be found in the repository: {invalid}", level=logging.WARNING, fg='red') - packages = read_packages(valid) - continue - break + 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: + log(f"Some packages could not be found in the repository: {invalid}", level=logging.WARNING, fg='red') + packages = read_packages(valid) + continue + break return packages diff --git a/examples/guided.py b/examples/guided.py index 635baf6a..8693b98f 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -274,7 +274,7 @@ if not (archinstall.check_mirror_reachable() or archinstall.arguments.get('skip- archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") exit(1) -if not archinstall.arguments.get('offline', False): +if not archinstall.arguments['offline']: latest_version_archlinux_keyring = max([k.pkg_version for k in archinstall.find_package('archlinux-keyring')]) # If we want to check for keyring updates -- cgit v1.2.3-70-g09d2 From 2d4b2620462a0fb4c9496ed0629d7ab8930fc73a Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 7 Jun 2022 01:26:27 +1000 Subject: Handle cyrillic characters (#1309) * Handle cyrillic characters * Update Co-authored-by: Daniel Girtler --- .github/workflows/mypy.yaml | 2 +- archinstall/__init__.py | 7 ----- archinstall/lib/menu/selection_menu.py | 10 +------ archinstall/lib/translation.py | 34 +++++++++++++++++++++++- archinstall/lib/user_interaction/general_conf.py | 9 ++++--- archinstall/locales/cyrillic.json | 19 +++++++++++++ 6 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 archinstall/locales/cyrillic.json (limited to 'archinstall/__init__.py') diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index d14d8553..8463afda 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -15,4 +15,4 @@ jobs: # one day this will be enabled # run: mypy --strict --module archinstall || exit 0 - name: run mypy - run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py + run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py archinstall/lib/translation.py diff --git a/archinstall/__init__.py b/archinstall/__init__.py index ee5e5f45..abcad3ba 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -58,10 +58,6 @@ storage['__version__'] = __version__ DeferredTranslation.install() -def set_unicode_font(): - SysCommand('setfont UniCyr_8x16') - - def define_arguments(): """ Define which explicit arguments do we allow. @@ -249,9 +245,6 @@ def post_process_arguments(arguments): load_config() -# to ensure that cyrillic characters work in the installer -# set_unicode_font() - define_arguments() arguments = get_arguments() post_process_arguments(arguments) diff --git a/archinstall/lib/menu/selection_menu.py b/archinstall/lib/menu/selection_menu.py index d4a7ceef..8dd6fcce 100644 --- a/archinstall/lib/menu/selection_menu.py +++ b/archinstall/lib/menu/selection_menu.py @@ -15,15 +15,6 @@ if TYPE_CHECKING: _: Any -def select_archinstall_language(preset_value: str) -> Optional[Any]: - """ - copied from user_interaction/general_conf.py as a temporary measure - """ - languages = Translation.get_available_lang() - language = Menu(_('Archinstall language'), languages, preset_values=preset_value).run() - return language.value - - class Selector: def __init__( self, @@ -462,6 +453,7 @@ class GeneralMenu: return mandatory_fields, mandatory_waiting def _select_archinstall_language(self, preset_value: str) -> str: + from ... import select_archinstall_language language = select_archinstall_language(preset_value) if language is not None: self._translation.activate(language) diff --git a/archinstall/lib/translation.py b/archinstall/lib/translation.py index 79e0198a..c20a4285 100644 --- a/archinstall/lib/translation.py +++ b/archinstall/lib/translation.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import logging import os import gettext @@ -13,12 +14,19 @@ if TYPE_CHECKING: class LanguageDefinitions: + _languages = 'languages.json' + _cyrillic = 'cyrillic.json' + def __init__(self): self._mappings = self._get_language_mappings() + self._cyrillic_languages = self._get_cyrillic_languages() + + def is_cyrillic(self, language: str) -> bool: + return language in self._cyrillic_languages def _get_language_mappings(self) -> List[Dict[str, str]]: locales_dir = Translation.get_locales_dir() - languages = Path.joinpath(locales_dir, 'languages.json') + languages = Path.joinpath(locales_dir, self._languages) with open(languages, 'r') as fp: return json.load(fp) @@ -30,6 +38,14 @@ class LanguageDefinitions: raise ValueError(f'No language with abbreviation "{abbr}" found') + def _get_cyrillic_languages(self) -> List[str]: + locales_dir = Translation.get_locales_dir() + languages = Path.joinpath(locales_dir, self._cyrillic) + + with open(languages, 'r') as fp: + data = json.load(fp) + return data['languages'] + class DeferredTranslation: def __init__(self, message: str): @@ -78,10 +94,26 @@ class Translation: def activate(self, name): if language := self._languages.get(name, None): + languages = LanguageDefinitions() + + if languages.is_cyrillic(name): + self._set_font('UniCyr_8x16') + else: + # this will reset a possible previously set font to a default font + self._set_font('') + language.install() else: raise ValueError(f'Language not supported: {name}') + def _set_font(self, font: str): + from archinstall import SysCommand, log + try: + log(f'Setting new font: {font}', level=logging.DEBUG) + SysCommand(f'setfont {font}') + except Exception: + log(f'Unable to set font {font}', level=logging.ERROR) + @classmethod def load_nationalization(cls) -> Translation: locales_dir = cls.get_locales_dir() diff --git a/archinstall/lib/user_interaction/general_conf.py b/archinstall/lib/user_interaction/general_conf.py index 70a0e73f..15c42b86 100644 --- a/archinstall/lib/user_interaction/general_conf.py +++ b/archinstall/lib/user_interaction/general_conf.py @@ -118,10 +118,13 @@ def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: case _: return {selected: mirrors[selected] for selected in selected_mirror.value} -def select_archinstall_language(default='English'): +def select_archinstall_language(preset_values: str): languages = Translation.get_available_lang() - language = Menu(_('Archinstall language'), languages, default_option=default).run() - return language + choice = Menu(_('Archinstall language'), languages, default_option=preset_values).run() + + match choice.type_: + case MenuSelectionType.Esc: return preset_values + case MenuSelectionType.Selection: return choice.value def select_profile(preset) -> Optional[Profile]: diff --git a/archinstall/locales/cyrillic.json b/archinstall/locales/cyrillic.json new file mode 100644 index 00000000..13f11ad0 --- /dev/null +++ b/archinstall/locales/cyrillic.json @@ -0,0 +1,19 @@ +{ + "languages": [ + "Abkhazian", + "Azerbaijani", + "Bashkir", + "Belarusian", + "Bulgarian", + "Chuvash", + "Komi", + "Macedonian", + "Mongolian", + "Russian", + "Serbo-Croatian", + "Tajik", + "Tatar", + "Ukrainian", + "Uzbek" + ] +} -- cgit v1.2.3-70-g09d2 From a7ca037a26de53fd242f89bc6a90fd53337b4d13 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 7 Jun 2022 01:28:46 +1000 Subject: Update the subvolume menu - fix for #1278 (#1297) * Update subvolume * Add mypy compliance Co-authored-by: Daniel Girtler Co-authored-by: Anton Hvornum --- .github/workflows/mypy.yaml | 2 +- archinstall/__init__.py | 6 +- archinstall/lib/disk/btrfs/__init__.py | 130 +------------ archinstall/lib/disk/btrfs/btrfs_helpers.py | 94 ++++------ archinstall/lib/disk/btrfs/btrfspartition.py | 6 +- archinstall/lib/disk/btrfs/btrfssubvolume.py | 191 -------------------- archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py | 192 ++++++++++++++++++++ archinstall/lib/disk/helpers.py | 9 +- archinstall/lib/disk/mapperdev.py | 10 +- archinstall/lib/disk/partition.py | 10 +- archinstall/lib/disk/user_guides.py | 19 +- archinstall/lib/installer.py | 55 ++---- archinstall/lib/menu/global_menu.py | 33 ++-- archinstall/lib/menu/list_manager.py | 39 ++-- archinstall/lib/models/subvolume.py | 68 +++++++ archinstall/lib/models/users.py | 6 +- .../lib/user_interaction/partitioning_conf.py | 12 +- .../lib/user_interaction/subvolume_config.py | 201 +++++++-------------- 18 files changed, 462 insertions(+), 621 deletions(-) delete mode 100644 archinstall/lib/disk/btrfs/btrfssubvolume.py create mode 100644 archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py create mode 100644 archinstall/lib/models/subvolume.py (limited to 'archinstall/__init__.py') diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index 8463afda..b0901b38 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -15,4 +15,4 @@ jobs: # one day this will be enabled # run: mypy --strict --module archinstall || exit 0 - name: run mypy - run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py archinstall/lib/translation.py + run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py archinstall/lib/user_interaction/subvolume_config.py archinstall/lib/disk/btrfs/btrfs_helpers.py archinstall/lib/translation.py diff --git a/archinstall/__init__.py b/archinstall/__init__.py index abcad3ba..1a360c67 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -226,8 +226,6 @@ def post_process_arguments(arguments): load_plugin(arguments['plugin']) if arguments.get('disk_layouts', None) is not None: - # if 'disk_layouts' not in storage: - # storage['disk_layouts'] = {} layout_storage = {} if not json_stream_to_structure('--disk_layouts',arguments['disk_layouts'],layout_storage): exit(1) @@ -236,10 +234,12 @@ def post_process_arguments(arguments): arguments['harddrives'] = [disk for disk in layout_storage] # backward compatibility. Change partition.format for partition.wipe for disk in layout_storage: - for i,partition in enumerate(layout_storage[disk].get('partitions',[])): + for i, partition in enumerate(layout_storage[disk].get('partitions',[])): if 'format' in partition: partition['wipe'] = partition['format'] del partition['format'] + elif 'btrfs' in partition: + partition['btrfs']['subvolumes'] = Subvolume.parse_arguments(partition['btrfs']['subvolumes']) arguments['disk_layouts'] = layout_storage load_config() diff --git a/archinstall/lib/disk/btrfs/__init__.py b/archinstall/lib/disk/btrfs/__init__.py index 90c58145..3c183112 100644 --- a/archinstall/lib/disk/btrfs/__init__.py +++ b/archinstall/lib/disk/btrfs/__init__.py @@ -2,8 +2,7 @@ from __future__ import annotations import pathlib import glob import logging -import re -from typing import Union, Dict, TYPE_CHECKING, Any, Iterator +from typing import Union, Dict, TYPE_CHECKING # https://stackoverflow.com/a/39757388/929999 if TYPE_CHECKING: @@ -15,30 +14,15 @@ from .btrfs_helpers import ( setup_subvolumes as setup_subvolumes, mount_subvolume as mount_subvolume ) -from .btrfssubvolume import BtrfsSubvolume as BtrfsSubvolume +from .btrfssubvolumeinfo import BtrfsSubvolumeInfo as BtrfsSubvolume from .btrfspartition import BTRFSPartition as BTRFSPartition -from ..helpers import get_mount_info from ...exceptions import DiskError, Deprecated from ...general import SysCommand from ...output import log -from ...exceptions import SysCallError -def get_subvolume_info(path :pathlib.Path) -> Dict[str, Any]: - try: - output = SysCommand(f"btrfs subvol show {path}").decode() - except SysCallError as error: - print('Error:', error) - result = {} - for line in output.replace('\r\n', '\n').split('\n'): - if ':' in line: - key, val = line.replace('\t', '').split(':', 1) - result[key.strip().lower().replace(' ', '_')] = val.strip() - - return result - -def create_subvolume(installation :Installer, subvolume_location :Union[pathlib.Path, str]) -> bool: +def create_subvolume(installation: Installer, subvolume_location :Union[pathlib.Path, str]) -> bool: """ This function uses btrfs to create a subvolume. @@ -71,112 +55,6 @@ def create_subvolume(installation :Installer, subvolume_location :Union[pathlib. if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0: raise DiskError(f"Could not create a subvolume at {target}: {cmd}") -def _has_option(option :str,options :list) -> bool: - """ auxiliary routine to check if an option is present in a list. - we check if the string appears in one of the options, 'cause it can appear in several forms (option, option=val,...) - """ - if not options: - return False - - for item in options: - if option in item: - return True - - return False - -def manage_btrfs_subvolumes(installation :Installer, - partition :Dict[str, str],) -> list: +def manage_btrfs_subvolumes(installation :Installer, partition :Dict[str, str]) -> list: raise Deprecated("Use setup_subvolumes() instead.") - - from copy import deepcopy - """ we do the magic with subvolumes in a centralized place - parameters: - * the installation object - * the partition dictionary entry which represents the physical partition - returns - * mountpoinst, the list which contains all the "new" partititon to be mounted - - We expect the partition has been mounted as / , and it to be unmounted after the processing - Then we create all the subvolumes inside btrfs as demand - We clone then, both the partition dictionary and the object inside it and adapt it to the subvolume needs - Then we return a list of "new" partitions to be processed as "normal" partitions - # TODO For encrypted devices we need some special processing prior to it - """ - # We process each of the pairs - # th mount info dict has an entry for the path of the mountpoint (named 'mountpoint') and 'options' which is a list - # of mount options (or similar used by brtfs) - mountpoints = [] - subvolumes = partition['btrfs']['subvolumes'] - for name, right_hand in subvolumes.items(): - try: - # we normalize the subvolume name (getting rid of slash at the start if exists. In our implementation has no semantic load - every subvolume is created from the top of the hierarchy- and simplifies its further use - if name.startswith('/'): - name = name[1:] - # renormalize the right hand. - location = None - subvol_options = [] - # no contents, so it is not to be mounted - if not right_hand: - location = None - # just a string. per backward compatibility the mount point - elif isinstance(right_hand,str): - location = right_hand - # a dict. two elements 'mountpoint' (obvious) and and a mount options list ¿? - elif isinstance(right_hand,dict): - location = right_hand.get('mountpoint',None) - subvol_options = right_hand.get('options',[]) - # we create the subvolume - create_subvolume(installation,name) - # Make the nodatacow processing now - # It will be the main cause of creation of subvolumes which are not to be mounted - # it is not an options which can be established by subvolume (but for whole file systems), and can be - # set up via a simple attribute change in a directory (if empty). And here the directories are brand new - if 'nodatacow' in subvol_options: - if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0: - raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}") - # entry is deleted so nodatacow doesn't propagate to the mount options - del subvol_options[subvol_options.index('nodatacow')] - # Make the compress processing now - # it is not an options which can be established by subvolume (but for whole file systems), and can be - # set up via a simple attribute change in a directory (if empty). And here the directories are brand new - # in this way only zstd compression is activaded - # TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated - if 'compress' in subvol_options: - if not _has_option('compress',partition.get('filesystem',{}).get('mount_options',[])): - if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0: - raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}") - # entry is deleted so compress doesn't propagate to the mount options - del subvol_options[subvol_options.index('compress')] - # END compress processing. - # we do not mount if THE basic partition will be mounted or if we exclude explicitly this subvolume - if not partition['mountpoint'] and location is not None: - # we begin to create a fake partition entry. First we copy the original -the one that corresponds to - # the primary partition. We make a deepcopy to avoid altering the original content in any case - fake_partition = deepcopy(partition) - # we start to modify entries in the "fake partition" to match the needs of the subvolumes - # to avoid any chance of entering in a loop (not expected) we delete the list of subvolumes in the copy - del fake_partition['btrfs'] - fake_partition['encrypted'] = False - fake_partition['generate-encryption-key-file'] = False - # Mount destination. As of now the right hand part - fake_partition['mountpoint'] = location - # we load the name in an attribute called subvolume, but i think it is not needed anymore, 'cause the mount logic uses a different path. - fake_partition['subvolume'] = name - # here we add the special mount options for the subvolume, if any. - # if the original partition['options'] is not a list might give trouble - if fake_partition.get('filesystem',{}).get('mount_options',[]): - fake_partition['filesystem']['mount_options'].extend(subvol_options) - else: - fake_partition['filesystem']['mount_options'] = subvol_options - # Here comes the most exotic part. The dictionary attribute 'device_instance' contains an instance of Partition. This instance will be queried along the mount process at the installer. - # As the rest will query there the path of the "partition" to be mounted, we feed it with the bind name needed to mount subvolumes - # As we made a deepcopy we have a fresh instance of this object we can manipulate problemless - fake_partition['device_instance'].path = f"{partition['device_instance'].path}[/{name}]" - - # Well, now that this "fake partition" is ready, we add it to the list of the ones which are to be mounted, - # as "normal" ones - mountpoints.append(fake_partition) - except Exception as e: - raise e - return mountpoints diff --git a/archinstall/lib/disk/btrfs/btrfs_helpers.py b/archinstall/lib/disk/btrfs/btrfs_helpers.py index 5fa94314..ab528388 100644 --- a/archinstall/lib/disk/btrfs/btrfs_helpers.py +++ b/archinstall/lib/disk/btrfs/btrfs_helpers.py @@ -1,72 +1,42 @@ -import pathlib import logging -from typing import Optional +from pathlib import Path +from typing import Optional, Dict, Any, TYPE_CHECKING +from ...models.subvolume import Subvolume from ...exceptions import SysCallError, DiskError from ...general import SysCommand from ...output import log from ..helpers import get_mount_info -from .btrfssubvolume import BtrfsSubvolume +from .btrfssubvolumeinfo import BtrfsSubvolumeInfo +if TYPE_CHECKING: + from .btrfspartition import BTRFSPartition + from ...installer import Installer -def mount_subvolume(installation, device, name, subvolume_information): - # we normalize the subvolume name (getting rid of slash at the start if exists. In our implementation has no semantic load. - # Every subvolume is created from the top of the hierarchy- and simplifies its further use - name = name.lstrip('/') - - # renormalize the right hand. - mountpoint = subvolume_information.get('mountpoint', None) - if not mountpoint: - return None - - if type(mountpoint) == str: - mountpoint = pathlib.Path(mountpoint) - installation_target = installation.target - if type(installation_target) == str: - installation_target = pathlib.Path(installation_target) +def mount_subvolume(installation: 'Installer', device: 'BTRFSPartition', subvolume: Subvolume): + # we normalize the subvolume name (getting rid of slash at the start if exists. + # In our implementation has no semantic load. + # Every subvolume is created from the top of the hierarchy- and simplifies its further use + name = subvolume.name.lstrip('/') + mountpoint = Path(subvolume.mountpoint) + installation_target = Path(installation.target) mountpoint = installation_target / mountpoint.relative_to(mountpoint.anchor) mountpoint.mkdir(parents=True, exist_ok=True) - - mount_options = subvolume_information.get('options', []) - if not any('subvol=' in x for x in mount_options): - mount_options += [f'subvol={name}'] + mount_options = subvolume.options + [f'subvol={name}'] log(f"Mounting subvolume {name} on {device} to {mountpoint}", level=logging.INFO, fg="gray") SysCommand(f"mount {device.path} {mountpoint} -o {','.join(mount_options)}") -def setup_subvolumes(installation, partition_dict): - """ - Taken from: ..user_guides.py - - partition['btrfs'] = { - "subvolumes" : { - "@": "/", - "@home": "/home", - "@log": "/var/log", - "@pkg": "/var/cache/pacman/pkg", - "@.snapshots": "/.snapshots" - } - } - """ +def setup_subvolumes(installation: 'Installer', partition_dict: Dict[str, Any]): log(f"Setting up subvolumes: {partition_dict['btrfs']['subvolumes']}", level=logging.INFO, fg="gray") - for name, right_hand in partition_dict['btrfs']['subvolumes'].items(): + + for subvolume in partition_dict['btrfs']['subvolumes']: # we normalize the subvolume name (getting rid of slash at the start if exists. In our implementation has no semantic load. # Every subvolume is created from the top of the hierarchy- and simplifies its further use - name = name.lstrip('/') - - # renormalize the right hand. - # mountpoint = None - subvol_options = [] - - match right_hand: - # case str(): # backwards-compatability - # mountpoint = right_hand - case dict(): - # mountpoint = right_hand.get('mountpoint', None) - subvol_options = right_hand.get('options', []) + name = subvolume.name.lstrip('/') # We create the subvolume using the BTRFSPartition instance. # That way we ensure not only easy access, but also accurate mount locations etc. @@ -76,27 +46,25 @@ def setup_subvolumes(installation, partition_dict): # It will be the main cause of creation of subvolumes which are not to be mounted # it is not an options which can be established by subvolume (but for whole file systems), and can be # set up via a simple attribute change in a directory (if empty). And here the directories are brand new - if 'nodatacow' in subvol_options: + if subvolume.nodatacow: if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0: raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}") - # entry is deleted so nodatacow doesn't propagate to the mount options - del subvol_options[subvol_options.index('nodatacow')] + # Make the compress processing now # it is not an options which can be established by subvolume (but for whole file systems), and can be # set up via a simple attribute change in a directory (if empty). And here the directories are brand new # in this way only zstd compression is activaded # TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated - if 'compress' in subvol_options: + if subvolume.compress: if not any(['compress' in filesystem_option for filesystem_option in partition_dict.get('filesystem', {}).get('mount_options', [])]): if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0: raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}") - # entry is deleted so compress doesn't propagate to the mount options - del subvol_options[subvol_options.index('compress')] -def subvolume_info_from_path(path :pathlib.Path) -> Optional[BtrfsSubvolume]: + +def subvolume_info_from_path(path: Path) -> Optional[BtrfsSubvolumeInfo]: try: - subvolume_name = None + subvolume_name = '' result = {} for index, line in enumerate(SysCommand(f"btrfs subvolume show {path}")): if index == 0: @@ -110,14 +78,14 @@ def subvolume_info_from_path(path :pathlib.Path) -> Optional[BtrfsSubvolume]: # allows for hooking in a pre-processor to do this we have to do it here: result[key.lower().replace(' ', '_').replace('(s)', 's')] = value.strip() - return BtrfsSubvolume(**{'full_path' : path, 'name' : subvolume_name, **result}) - + return BtrfsSubvolumeInfo(**{'full_path' : path, 'name' : subvolume_name, **result}) # type: ignore except SysCallError as error: log(f"Could not retrieve subvolume information from {path}: {error}", level=logging.WARNING, fg="orange") return None -def find_parent_subvolume(path :pathlib.Path, filters=[]): + +def find_parent_subvolume(path: Path, filters=[]) -> Optional[BtrfsSubvolumeInfo]: # A root path cannot have a parent if str(path) == '/': return None @@ -127,6 +95,8 @@ def find_parent_subvolume(path :pathlib.Path, filters=[]): if found_mount['target'] == '/': return None - return find_parent_subvolume(path.parent, traverse=True, filters=[*filters, found_mount['target']]) + return find_parent_subvolume(path.parent, filters=[*filters, found_mount['target']]) + + return subvolume - return subvolume \ No newline at end of file + return None diff --git a/archinstall/lib/disk/btrfs/btrfspartition.py b/archinstall/lib/disk/btrfs/btrfspartition.py index 6f7487e4..a05f1527 100644 --- a/archinstall/lib/disk/btrfs/btrfspartition.py +++ b/archinstall/lib/disk/btrfs/btrfspartition.py @@ -15,7 +15,7 @@ from .btrfs_helpers import ( if TYPE_CHECKING: from ...installer import Installer - from .btrfssubvolume import BtrfsSubvolume + from .btrfssubvolumeinfo import BtrfsSubvolumeInfo class BTRFSPartition(Partition): def __init__(self, *args, **kwargs): @@ -50,7 +50,7 @@ class BTRFSPartition(Partition): for child in iterate_children(filesystem): yield child - def create_subvolume(self, subvolume :pathlib.Path, installation :Optional['Installer'] = None) -> 'BtrfsSubvolume': + def create_subvolume(self, subvolume :pathlib.Path, installation :Optional['Installer'] = None) -> 'BtrfsSubvolumeInfo': """ Subvolumes have to be created within a mountpoint. This means we need to get the current installation target. @@ -117,4 +117,4 @@ class BTRFSPartition(Partition): # And deal with it here: SysCommand(f"btrfs subvolume create {subvolume}") - return subvolume_info_from_path(subvolume) \ No newline at end of file + return subvolume_info_from_path(subvolume) diff --git a/archinstall/lib/disk/btrfs/btrfssubvolume.py b/archinstall/lib/disk/btrfs/btrfssubvolume.py deleted file mode 100644 index bc7db612..00000000 --- a/archinstall/lib/disk/btrfs/btrfssubvolume.py +++ /dev/null @@ -1,191 +0,0 @@ -import pathlib -import datetime -import logging -import string -import random -import shutil -from dataclasses import dataclass -from typing import Optional, List# , TYPE_CHECKING -from functools import cached_property - -# if TYPE_CHECKING: -# from ..blockdevice import BlockDevice - -from ...exceptions import DiskError -from ...general import SysCommand -from ...output import log -from ...storage import storage - -@dataclass -class BtrfsSubvolume: - full_path :pathlib.Path - name :str - uuid :str - parent_uuid :str - creation_time :datetime.datetime - subvolume_id :int - generation :int - gen_at_creation :int - parent_id :int - top_level_id :int - send_transid :int - send_time :datetime.datetime - receive_transid :int - received_uuid :Optional[str] = None - flags :Optional[str] = None - receive_time :Optional[datetime.datetime] = None - snapshots :Optional[List] = None - - def __post_init__(self): - self.full_path = pathlib.Path(self.full_path) - - # Convert "-" entries to `None` - if self.parent_uuid == "-": - self.parent_uuid = None - if self.received_uuid == "-": - self.received_uuid = None - if self.flags == "-": - self.flags = None - if self.receive_time == "-": - self.receive_time = None - if self.snapshots == "": - self.snapshots = [] - - # Convert timestamps into datetime workable objects (and preserve timezone by using ISO formats) - self.creation_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.creation_time)) - self.send_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.send_time)) - if self.receive_time: - self.receive_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.receive_time)) - - @property - def parent_subvolume(self): - from .btrfs_helpers import find_parent_subvolume - - return find_parent_subvolume(self.full_path) - - @property - def root(self) -> bool: - from .btrfs_helpers import subvolume_info_from_path - - # TODO: Make this function traverse storage['MOUNT_POINT'] and find the first - # occurrence of a mountpoint that is a btrfs volume instead of lazy assume / is a subvolume. - # It would also be nice if it could use findmnt(self.full_path) and traverse backwards - # finding the last occurrence of a subvolume which 'self' belongs to. - if volume := subvolume_info_from_path(storage['MOUNT_POINT']): - return self.full_path == volume.full_path - - return False - - @cached_property - def partition(self): - from ..helpers import findmnt, get_parent_of_partition, all_blockdevices - from ..partition import Partition - from ..blockdevice import BlockDevice - from ..mapperdev import MapperDev - from .btrfspartition import BTRFSPartition - from .btrfs_helpers import subvolume_info_from_path - - try: - # If the subvolume is mounted, it's pretty trivial to lookup the partition (parent) device. - if filesystem := findmnt(self.full_path).get('filesystems', []): - if source := filesystem[0].get('source', None): - # Strip away subvolume definitions from findmnt - if '[' in source: - source = source[:source.find('[')] - - if filesystem[0].get('fstype', '') == 'btrfs': - return BTRFSPartition(source, BlockDevice(get_parent_of_partition(pathlib.Path(source)))) - elif filesystem[0].get('source', '').startswith('/dev/mapper'): - return MapperDev(source) - else: - return Partition(source, BlockDevice(get_parent_of_partition(pathlib.Path(source)))) - except DiskError: - # Subvolume has never been mounted, we have no reliable way of finding where it is. - # But we have the UUID of the partition, and can begin looking for it by mounting - # all blockdevices that we can reliably support.. This is taxing tho and won't cover all devices. - - log(f"Looking up {self}, this might take time.", fg="orange", level=logging.WARNING) - for blockdevice, instance in all_blockdevices(mappers=True, partitions=True, error=True).items(): - if type(instance) in (Partition, MapperDev): - we_mounted_it = False - detection_mountpoint = instance.mountpoint - if not detection_mountpoint: - if type(instance) == Partition and instance.encrypted: - # TODO: Perhaps support unlocking encrypted volumes? - # This will cause a lot of potential user interactions tho. - log(f"Ignoring {blockdevice} because it's encrypted.", fg="gray", level=logging.DEBUG) - continue - - detection_mountpoint = pathlib.Path(f"/tmp/{''.join([random.choice(string.ascii_letters) for x in range(20)])}") - detection_mountpoint.mkdir(parents=True, exist_ok=True) - - instance.mount(str(detection_mountpoint)) - we_mounted_it = True - - if (filesystem := findmnt(detection_mountpoint)) and (filesystem := filesystem.get('filesystems', [])): - if subvolume := subvolume_info_from_path(filesystem[0]['target']): - if subvolume.uuid == self.uuid: - # The top level subvolume matched of ourselves, - # which means the instance we're iterating has the subvol we're looking for. - log(f"Found the subvolume on device {instance}", level=logging.DEBUG, fg="gray") - return instance - - def iterate_children(struct): - for child in struct.get('children', []): - if '[' in child.get('source', ''): - yield subvolume_info_from_path(child['target']) - - for sub_child in iterate_children(child): - yield sub_child - - for child in iterate_children(filesystem[0]): - if child.uuid == self.uuid: - # We found a child within the instance that has the subvol we're looking for. - log(f"Found the subvolume on device {instance}", level=logging.DEBUG, fg="gray") - return instance - - if we_mounted_it: - instance.unmount() - shutil.rmtree(detection_mountpoint) - - @cached_property - def mount_options(self) -> Optional[List[str]]: - from ..helpers import findmnt - - if filesystem := findmnt(self.full_path).get('filesystems', []): - return filesystem[0].get('options').split(',') - - def convert_to_ISO_format(self, time_string): - time_string_almost_done = time_string.replace(' ', 'T', 1).replace(' ', '') - iso_string = f"{time_string_almost_done[:-2]}:{time_string_almost_done[-2:]}" - return iso_string - - def mount(self, mountpoint :pathlib.Path, options=None, include_previously_known_options=True): - from ..helpers import findmnt - - try: - if mnt_info := findmnt(pathlib.Path(mountpoint), traverse=False): - log(f"Unmounting {mountpoint} as it was already mounted using {mnt_info}") - SysCommand(f"umount {mountpoint}") - except DiskError: - # No previously mounted device at the mountpoint - pass - - if not options: - options = [] - - try: - if include_previously_known_options and (cached_options := self.mount_options): - options += cached_options - except DiskError: - pass - - if not any('subvol=' in x for x in options): - options += f'subvol={self.name}' - - SysCommand(f"mount {self.partition.path} {mountpoint} -o {','.join(options)}") - log(f"{self} has successfully been mounted to {mountpoint}", level=logging.INFO, fg="gray") - - def unmount(self, recurse :bool = True): - SysCommand(f"umount {'-R' if recurse else ''} {self.full_path}") - log(f"Successfully unmounted {self}", level=logging.INFO, fg="gray") \ No newline at end of file diff --git a/archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py b/archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py new file mode 100644 index 00000000..5f5bdea6 --- /dev/null +++ b/archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py @@ -0,0 +1,192 @@ +import pathlib +import datetime +import logging +import string +import random +import shutil +from dataclasses import dataclass +from typing import Optional, List# , TYPE_CHECKING +from functools import cached_property + +# if TYPE_CHECKING: +# from ..blockdevice import BlockDevice + +from ...exceptions import DiskError +from ...general import SysCommand +from ...output import log +from ...storage import storage + + +@dataclass +class BtrfsSubvolumeInfo: + full_path :pathlib.Path + name :str + uuid :str + parent_uuid :str + creation_time :datetime.datetime + subvolume_id :int + generation :int + gen_at_creation :int + parent_id :int + top_level_id :int + send_transid :int + send_time :datetime.datetime + receive_transid :int + received_uuid :Optional[str] = None + flags :Optional[str] = None + receive_time :Optional[datetime.datetime] = None + snapshots :Optional[List] = None + + def __post_init__(self): + self.full_path = pathlib.Path(self.full_path) + + # Convert "-" entries to `None` + if self.parent_uuid == "-": + self.parent_uuid = None + if self.received_uuid == "-": + self.received_uuid = None + if self.flags == "-": + self.flags = None + if self.receive_time == "-": + self.receive_time = None + if self.snapshots == "": + self.snapshots = [] + + # Convert timestamps into datetime workable objects (and preserve timezone by using ISO formats) + self.creation_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.creation_time)) + self.send_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.send_time)) + if self.receive_time: + self.receive_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.receive_time)) + + @property + def parent_subvolume(self): + from .btrfs_helpers import find_parent_subvolume + + return find_parent_subvolume(self.full_path) + + @property + def root(self) -> bool: + from .btrfs_helpers import subvolume_info_from_path + + # TODO: Make this function traverse storage['MOUNT_POINT'] and find the first + # occurrence of a mountpoint that is a btrfs volume instead of lazy assume / is a subvolume. + # It would also be nice if it could use findmnt(self.full_path) and traverse backwards + # finding the last occurrence of a subvolume which 'self' belongs to. + if volume := subvolume_info_from_path(storage['MOUNT_POINT']): + return self.full_path == volume.full_path + + return False + + @cached_property + def partition(self): + from ..helpers import findmnt, get_parent_of_partition, all_blockdevices + from ..partition import Partition + from ..blockdevice import BlockDevice + from ..mapperdev import MapperDev + from .btrfspartition import BTRFSPartition + from .btrfs_helpers import subvolume_info_from_path + + try: + # If the subvolume is mounted, it's pretty trivial to lookup the partition (parent) device. + if filesystem := findmnt(self.full_path).get('filesystems', []): + if source := filesystem[0].get('source', None): + # Strip away subvolume definitions from findmnt + if '[' in source: + source = source[:source.find('[')] + + if filesystem[0].get('fstype', '') == 'btrfs': + return BTRFSPartition(source, BlockDevice(get_parent_of_partition(pathlib.Path(source)))) + elif filesystem[0].get('source', '').startswith('/dev/mapper'): + return MapperDev(source) + else: + return Partition(source, BlockDevice(get_parent_of_partition(pathlib.Path(source)))) + except DiskError: + # Subvolume has never been mounted, we have no reliable way of finding where it is. + # But we have the UUID of the partition, and can begin looking for it by mounting + # all blockdevices that we can reliably support.. This is taxing tho and won't cover all devices. + + log(f"Looking up {self}, this might take time.", fg="orange", level=logging.WARNING) + for blockdevice, instance in all_blockdevices(mappers=True, partitions=True, error=True).items(): + if type(instance) in (Partition, MapperDev): + we_mounted_it = False + detection_mountpoint = instance.mountpoint + if not detection_mountpoint: + if type(instance) == Partition and instance.encrypted: + # TODO: Perhaps support unlocking encrypted volumes? + # This will cause a lot of potential user interactions tho. + log(f"Ignoring {blockdevice} because it's encrypted.", fg="gray", level=logging.DEBUG) + continue + + detection_mountpoint = pathlib.Path(f"/tmp/{''.join([random.choice(string.ascii_letters) for x in range(20)])}") + detection_mountpoint.mkdir(parents=True, exist_ok=True) + + instance.mount(str(detection_mountpoint)) + we_mounted_it = True + + if (filesystem := findmnt(detection_mountpoint)) and (filesystem := filesystem.get('filesystems', [])): + if subvolume := subvolume_info_from_path(filesystem[0]['target']): + if subvolume.uuid == self.uuid: + # The top level subvolume matched of ourselves, + # which means the instance we're iterating has the subvol we're looking for. + log(f"Found the subvolume on device {instance}", level=logging.DEBUG, fg="gray") + return instance + + def iterate_children(struct): + for child in struct.get('children', []): + if '[' in child.get('source', ''): + yield subvolume_info_from_path(child['target']) + + for sub_child in iterate_children(child): + yield sub_child + + for child in iterate_children(filesystem[0]): + if child.uuid == self.uuid: + # We found a child within the instance that has the subvol we're looking for. + log(f"Found the subvolume on device {instance}", level=logging.DEBUG, fg="gray") + return instance + + if we_mounted_it: + instance.unmount() + shutil.rmtree(detection_mountpoint) + + @cached_property + def mount_options(self) -> Optional[List[str]]: + from ..helpers import findmnt + + if filesystem := findmnt(self.full_path).get('filesystems', []): + return filesystem[0].get('options').split(',') + + def convert_to_ISO_format(self, time_string): + time_string_almost_done = time_string.replace(' ', 'T', 1).replace(' ', '') + iso_string = f"{time_string_almost_done[:-2]}:{time_string_almost_done[-2:]}" + return iso_string + + def mount(self, mountpoint :pathlib.Path, options=None, include_previously_known_options=True): + from ..helpers import findmnt + + try: + if mnt_info := findmnt(pathlib.Path(mountpoint), traverse=False): + log(f"Unmounting {mountpoint} as it was already mounted using {mnt_info}") + SysCommand(f"umount {mountpoint}") + except DiskError: + # No previously mounted device at the mountpoint + pass + + if not options: + options = [] + + try: + if include_previously_known_options and (cached_options := self.mount_options): + options += cached_options + except DiskError: + pass + + if not any('subvol=' in x for x in options): + options += f'subvol={self.name}' + + SysCommand(f"mount {self.partition.path} {mountpoint} -o {','.join(options)}") + log(f"{self} has successfully been mounted to {mountpoint}", level=logging.INFO, fg="gray") + + def unmount(self, recurse :bool = True): + SysCommand(f"umount {'-R' if recurse else ''} {self.full_path}") + log(f"Successfully unmounted {self}", level=logging.INFO, fg="gray") diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py index 85c0390f..660594ed 100644 --- a/archinstall/lib/disk/helpers.py +++ b/archinstall/lib/disk/helpers.py @@ -8,6 +8,8 @@ import time import glob from typing import Union, List, Iterator, Dict, Optional, Any, TYPE_CHECKING # https://stackoverflow.com/a/39757388/929999 +from ..models.subvolume import Subvolume + if TYPE_CHECKING: from .partition import Partition @@ -469,6 +471,7 @@ def convert_device_to_uuid(path :str) -> str: raise DiskError(f"Could not retrieve the UUID of {path} within a timely manner.") + def has_mountpoint(partition: Union[dict,Partition,MapperDev], target: str, strict: bool = True) -> bool: """ Determine if a certain partition is mounted (or has a mountpoint) as specific target (path) Coded for clarity rather than performance @@ -485,10 +488,12 @@ def has_mountpoint(partition: Union[dict,Partition,MapperDev], target: str, stri """ # we create the mountpoint list if isinstance(partition,dict): - subvols = partition.get('btrfs',{}).get('subvolumes',{}) - mountpoints = [partition.get('mountpoint'),] + [subvols[subvol] if isinstance(subvols[subvol],str) or not subvols[subvol] else subvols[subvol].get('mountpoint') for subvol in subvols] + subvolumes: List[Subvolume] = partition.get('btrfs',{}).get('subvolumes', []) + mountpoints = [partition.get('mountpoint')] + mountpoints += [volume.mountpoint for volume in subvolumes] else: mountpoints = [partition.mountpoint,] + [subvol.target for subvol in partition.subvolumes] + # we check if strict or target == '/': if target in mountpoints: diff --git a/archinstall/lib/disk/mapperdev.py b/archinstall/lib/disk/mapperdev.py index 913dbc13..49137ae9 100644 --- a/archinstall/lib/disk/mapperdev.py +++ b/archinstall/lib/disk/mapperdev.py @@ -10,7 +10,7 @@ from ..general import SysCommand from ..output import log if TYPE_CHECKING: - from .btrfs import BtrfsSubvolume + from .btrfs import BtrfsSubvolumeInfo @dataclass class MapperDev: @@ -37,12 +37,12 @@ class MapperDev: for slave in glob.glob(f"/sys/class/block/{dm_device.name}/slaves/*"): partition_belonging_to_dmcrypt_device = pathlib.Path(slave).name - + try: uevent_data = SysCommand(f"blkid -o export /dev/{partition_belonging_to_dmcrypt_device}").decode() except SysCallError as error: log(f"Could not get information on device /dev/{partition_belonging_to_dmcrypt_device}: {error}", level=logging.ERROR, fg="red") - + information = uevent(uevent_data) block_device = BlockDevice(get_parent_of_partition('/dev/' / pathlib.Path(information['DEVNAME']))) @@ -75,10 +75,10 @@ class MapperDev: return get_filesystem_type(self.path) @property - def subvolumes(self) -> Iterator['BtrfsSubvolume']: + def subvolumes(self) -> Iterator['BtrfsSubvolumeInfo']: from .btrfs import subvolume_info_from_path for mountpoint in self.mount_information: if target := mountpoint.get('target'): if subvolume := subvolume_info_from_path(pathlib.Path(target)): - yield subvolume \ No newline at end of file + yield subvolume diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py index 2c9f50c2..6f25a5f7 100644 --- a/archinstall/lib/disk/partition.py +++ b/archinstall/lib/disk/partition.py @@ -14,7 +14,7 @@ from ..exceptions import DiskError, SysCallError, UnknownFilesystemFormat from ..output import log from ..general import SysCommand from .btrfs.btrfs_helpers import subvolume_info_from_path -from .btrfs.btrfssubvolume import BtrfsSubvolume +from .btrfs.btrfssubvolumeinfo import BtrfsSubvolumeInfo class Partition: def __init__(self, @@ -185,7 +185,7 @@ class Partition: for i in range(storage['DISK_RETRY_ATTEMPTS']): if not self.partprobe(): raise DiskError(f"Could not perform partprobe on {self.device_path}") - + time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i)) partuuid = self._safe_part_uuid @@ -294,9 +294,9 @@ class Partition: return bind_name @property - def subvolumes(self) -> Iterator[BtrfsSubvolume]: + def subvolumes(self) -> Iterator[BtrfsSubvolumeInfo]: from .helpers import findmnt - + def iterate_children_recursively(information): for child in information.get('children', []): if target := child.get('target'): @@ -452,7 +452,7 @@ class Partition: if retry is True: log(f"Retrying in {storage.get('DISK_TIMEOUTS', 1)} seconds.", level=logging.WARNING, fg="orange") time.sleep(storage.get('DISK_TIMEOUTS', 1)) - + return self.format(filesystem, path, log_formatting, options, retry=False) if get_filesystem_type(path) == 'crypto_LUKS' or get_filesystem_type(self.real_device) == 'crypto_LUKS': diff --git a/archinstall/lib/disk/user_guides.py b/archinstall/lib/disk/user_guides.py index 5fa6bfdc..5809c073 100644 --- a/archinstall/lib/disk/user_guides.py +++ b/archinstall/lib/disk/user_guides.py @@ -3,6 +3,8 @@ import logging from typing import Optional, Dict, Any, List, TYPE_CHECKING # https://stackoverflow.com/a/39757388/929999 +from ..models.subvolume import Subvolume + if TYPE_CHECKING: from .blockdevice import BlockDevice _: Any @@ -107,17 +109,14 @@ def suggest_single_disk_layout(block_device :BlockDevice, # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh layout[block_device.path]['partitions'][1]['btrfs'] = { - "subvolumes" : { - "@":"/", - "@home": "/home", - "@log": "/var/log", - "@pkg": "/var/cache/pacman/pkg", - "@.snapshots": "/.snapshots" - } + 'subvolumes': [ + Subvolume('@', '/'), + Subvolume('@home', '/home'), + Subvolume('@log', '/var/log'), + Subvolume('@pkg', '/var/cache/pacman/pkg'), + Subvolume('@.snapshots', '/.snapshots') + ] } - # else: - # pass # ... implement a guided setup - 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.. diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index bf296c2e..97c2492d 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -24,6 +24,7 @@ from .disk.partition import get_mount_fs_type from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .hsm import fido2_enroll from .models.users import User +from .models.subvolume import Subvolume if TYPE_CHECKING: _: Any @@ -263,47 +264,25 @@ class Installer: hsm_device_path = storage['arguments']['HSM'] fido2_enroll(hsm_device_path, partition['device_instance'], password) - # we manage the btrfs partitions - if any(btrfs_subvolumes := [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', {})]): - for partition in btrfs_subvolumes: - if mount_options := ','.join(partition.get('filesystem',{}).get('mount_options',[])): - self.mount(partition['device_instance'], "/", options=mount_options) - else: - self.mount(partition['device_instance'], "/") - - setup_subvolumes( - installation=self, - partition_dict=partition - ) + btrfs_subvolumes = [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', [])] - partition['device_instance'].unmount() + for partition in btrfs_subvolumes: + device_instance = partition['device_instance'] + mount_options = partition.get('filesystem', {}).get('mount_options', []) + self.mount(device_instance, "/", options=','.join(mount_options)) + setup_subvolumes(installation=self, partition_dict=partition) + device_instance.unmount() # We then handle any special cases, such as btrfs - if any(btrfs_subvolumes := [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', {})]): - for partition_information in btrfs_subvolumes: - for name, mountpoint in sorted(partition_information['btrfs']['subvolumes'].items(), key=lambda item: item[1]): - btrfs_subvolume_information = {} - - match mountpoint: - case str(): # backwards-compatability - btrfs_subvolume_information['mountpoint'] = mountpoint - btrfs_subvolume_information['options'] = [] - case dict(): - btrfs_subvolume_information['mountpoint'] = mountpoint.get('mountpoint', None) - btrfs_subvolume_information['options'] = mountpoint.get('options', []) - case _: - continue - - if mountpoint_parsed := btrfs_subvolume_information.get('mountpoint'): - # We cache the mount call for later - mount_queue[mountpoint_parsed] = lambda device=partition_information['device_instance'], \ - name=name, \ - subvolume_information=btrfs_subvolume_information: mount_subvolume( - installation=self, - device=device, - name=name, - subvolume_information=subvolume_information - ) + for partition in btrfs_subvolumes: + subvolumes: List[Subvolume] = partition['btrfs']['subvolumes'] + for subvolume in sorted(subvolumes, key=lambda item: item.mountpoint): + # We cache the mount call for later + mount_queue[subvolume.mountpoint] = lambda sub_vol=subvolume, device=partition['device_instance']: mount_subvolume( + installation=self, + device=device, + subvolume=sub_vol + ) # We mount ordinary partitions, and we sort them by the mountpoint for partition in sorted([entry for entry in list_part if entry.get('mountpoint', False)], key=lambda part: part['mountpoint']): diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py index cb61168d..49083517 100644 --- a/archinstall/lib/menu/global_menu.py +++ b/archinstall/lib/menu/global_menu.py @@ -325,22 +325,23 @@ class GlobalMenu(GeneralMenu): def _select_harddrives(self, old_harddrives : list) -> List: harddrives = select_harddrives(old_harddrives) - if len(harddrives) == 0: - prompt = _( - "You decided to skip harddrive selection\nand will use whatever drive-setup is mounted at {} (experimental)\n" - "WARNING: Archinstall won't check the suitability of this setup\n" - "Do you wish to continue?" - ).format(storage['MOUNT_POINT']) - - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), skip=False).run() - - if choice.value == Menu.no(): - return self._select_harddrives(old_harddrives) - - # in case the harddrives got changed we have to reset the disk layout as well - if old_harddrives != harddrives: - self._menu_options['disk_layouts'].set_current_selection(None) - storage['arguments']['disk_layouts'] = {} + if harddrives: + if len(harddrives) == 0: + prompt = _( + "You decided to skip harddrive selection\nand will use whatever drive-setup is mounted at {} (experimental)\n" + "WARNING: Archinstall won't check the suitability of this setup\n" + "Do you wish to continue?" + ).format(storage['MOUNT_POINT']) + + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), skip=False).run() + + if choice.value == Menu.no(): + return self._select_harddrives(old_harddrives) + + # in case the harddrives got changed we have to reset the disk layout as well + if old_harddrives != harddrives: + self._menu_options['disk_layouts'].set_current_selection(None) + storage['arguments']['disk_layouts'] = {} return harddrives diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index 7e051528..40d01ce3 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -137,34 +137,35 @@ class ListManager: else: self._default_action = [str(default_action),] - self.header = header if header else None - self.cancel_action = str(_('Cancel')) - self.confirm_action = str(_('Confirm and exit')) - self.separator = '' - self.bottom_list = [self.confirm_action,self.cancel_action] - self.bottom_item = [self.cancel_action] - self.base_actions = base_actions if base_actions else [str(_('Add')),str(_('Copy')),str(_('Edit')),str(_('Delete'))] + self._header = header if header else None + self._cancel_action = str(_('Cancel')) + self._confirm_action = str(_('Confirm and exit')) + self._separator = '' + self._bottom_list = [self._confirm_action, self._cancel_action] + self._bottom_item = [self._cancel_action] + self._base_actions = base_actions if base_actions else [str(_('Add')), str(_('Copy')), str(_('Edit')), str(_('Delete'))] self._original_data = copy.deepcopy(base_list) self._data = copy.deepcopy(base_list) # as refs, changes are immediate + # default values for the null case self.target: Optional[Any] = None self.action = self._null_action - if len(self._data) == 0 and self._null_action: - self._data = self.exec_action(self._data) - def run(self): while True: # this will return a dictionary with the key as the menu entry to be displayed # and the value is the original value from the self._data container + data_formatted = self.reformat(self._data) options = list(data_formatted.keys()) - options.append(self.separator) + + if len(options) > 0: + options.append(self._separator) if self._default_action: options += self._default_action - options += self.bottom_list + options += self._bottom_list system('clear') @@ -174,12 +175,12 @@ class ListManager: sort=False, clear_screen=False, clear_menu_on_exit=False, - header=self.header, + header=self._header, skip_empty_entries=True, skip=False ).run() - if not target.value or target.value in self.bottom_list: + if not target.value or target.value in self._bottom_list: self.action = target break @@ -201,13 +202,13 @@ class ListManager: # Possible enhancement. If run_actions returns false a message line indicating the failure self.run_actions(target.value) - if target.value == self.cancel_action: # TODO dubious + if target.value == self._cancel_action: # TODO dubious return self._original_data # return the original list else: return self._data def run_actions(self,prompt_data=None): - options = self.action_list() + self.bottom_item + options = self.action_list() + self._bottom_item prompt = _("Select an action for < {} >").format(prompt_data if prompt_data else self.target) choice = Menu( prompt, @@ -215,13 +216,13 @@ class ListManager: sort=False, clear_screen=False, clear_menu_on_exit=False, - preset_values=self.bottom_item, + preset_values=self._bottom_item, show_search_hint=False ).run() self.action = choice.value - if self.action and self.action != self.cancel_action: + if self.action and self.action != self._cancel_action: self._data = self.exec_action(self._data) """ @@ -243,7 +244,7 @@ class ListManager: can define alternate action list or customize the list for each item. Executed after any item is selected, contained in self.target """ - return self.base_actions + return self._base_actions def exec_action(self, data: Any): """ diff --git a/archinstall/lib/models/subvolume.py b/archinstall/lib/models/subvolume.py new file mode 100644 index 00000000..34a09227 --- /dev/null +++ b/archinstall/lib/models/subvolume.py @@ -0,0 +1,68 @@ +from dataclasses import dataclass +from typing import List, Any, Dict + + +@dataclass +class Subvolume: + name: str + mountpoint: str + compress: bool = False + nodatacow: bool = False + + def display(self) -> str: + options_str = ','.join(self.options) + return f'{_("Subvolume")}: {self.name:15} {_("Mountpoint")}: {self.mountpoint:20} {_("Options")}: {options_str}' + + @property + def options(self) -> List[str]: + options = [ + 'compress' if self.compress else '', + 'nodatacow' if self.nodatacow else '' + ] + return [o for o in options if len(o)] + + def json(self) -> Dict[str, Any]: + return { + 'name': self.name, + 'mountpoint': self.mountpoint, + 'compress': self.compress, + 'nodatacow': self.nodatacow + } + + @classmethod + def _parse(cls, config_subvolumes: List[Dict[str, Any]]) -> List['Subvolume']: + subvolumes = [] + for entry in config_subvolumes: + if not entry.get('name', None) or not entry.get('mountpoint', None): + continue + + subvolumes.append( + Subvolume( + entry['name'], + entry['mountpoint'], + entry.get('compress', False), + entry.get('nodatacow', False) + ) + ) + + return subvolumes + + @classmethod + def _parse_backwards_compatible(cls, config_subvolumes) -> List['Subvolume']: + subvolumes = [] + for name, mountpoint in config_subvolumes.items(): + if not name or not mountpoint: + continue + + subvolumes.append(Subvolume(name, mountpoint)) + + return subvolumes + + @classmethod + def parse_arguments(cls, config_subvolumes: Any) -> List['Subvolume']: + if isinstance(config_subvolumes, list): + return cls._parse(config_subvolumes) + elif isinstance(config_subvolumes, dict): + return cls._parse_backwards_compatible(config_subvolumes) + + raise ValueError('Unknown disk layout btrfs subvolume format') diff --git a/archinstall/lib/models/users.py b/archinstall/lib/models/users.py index f72cabde..a8feb9ef 100644 --- a/archinstall/lib/models/users.py +++ b/archinstall/lib/models/users.py @@ -27,8 +27,10 @@ class User: } def display(self) -> str: - strength = PasswordStrength.strength(self.password) - password = '*' * len(self.password) + f' ({strength.value})' + password = '*' * (len(self.password) if self.password else 0) + if password: + strength = PasswordStrength.strength(self.password) + password += f' ({strength.value})' return f'{_("Username")}: {self.username:16} {_("Password")}: {password:20} sudo: {str(self.sudo)}' @classmethod diff --git a/archinstall/lib/user_interaction/partitioning_conf.py b/archinstall/lib/user_interaction/partitioning_conf.py index caf5f5df..63b7c7df 100644 --- a/archinstall/lib/user_interaction/partitioning_conf.py +++ b/archinstall/lib/user_interaction/partitioning_conf.py @@ -351,18 +351,16 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, if partition is not None: if not block_device_struct["partitions"][partition].get('btrfs', {}): block_device_struct["partitions"][partition]['btrfs'] = {} - if not block_device_struct["partitions"][partition]['btrfs'].get('subvolumes', {}): - block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = {} + if not block_device_struct["partitions"][partition]['btrfs'].get('subvolumes', []): + block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = [] prev = block_device_struct["partitions"][partition]['btrfs']['subvolumes'] - result = SubvolumeList(_("Manage btrfs subvolumes for current partition"),prev).run() - if result: - block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = result - else: - del block_device_struct["partitions"][partition]['btrfs'] + result = SubvolumeList(_("Manage btrfs subvolumes for current partition"), prev).run() + block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = result return block_device_struct + def select_encrypted_partitions( title :str, partitions :List[Partition], diff --git a/archinstall/lib/user_interaction/subvolume_config.py b/archinstall/lib/user_interaction/subvolume_config.py index 94e6f5d7..a54ec891 100644 --- a/archinstall/lib/user_interaction/subvolume_config.py +++ b/archinstall/lib/user_interaction/subvolume_config.py @@ -1,155 +1,94 @@ -from typing import Dict, List +from typing import Dict, List, Optional, Any, TYPE_CHECKING from ..menu.list_manager import ListManager from ..menu.menu import MenuSelectionType -from ..menu.selection_menu import Selector, GeneralMenu from ..menu.text_input import TextInput from ..menu import Menu +from ..models.subvolume import Subvolume + +if TYPE_CHECKING: + _: Any -""" -UI classes -""" class SubvolumeList(ListManager): - def __init__(self,prompt,list): - self.ObjectNullAction = None # str(_('Add')) - self.ObjectDefaultAction = str(_('Add')) - super().__init__(prompt,list,None,self.ObjectNullAction,self.ObjectDefaultAction) - - def reformat(self, data: Dict) -> Dict: - def presentation(key :str, value :Dict): - text = _(" Subvolume :{:16}").format(key) - if isinstance(value,str): - text += _(" mounted at {:16}").format(value) - else: - if value.get('mountpoint'): - text += _(" mounted at {:16}").format(value['mountpoint']) - else: - text += (' ' * 28) - - if value.get('options',[]): - text += _(" with option {}").format(', '.join(value['options'])) - return text - - formatted = {presentation(k, v): k for k, v in data.items()} - return {k: v for k, v in sorted(formatted.items(), key=lambda e: e[0])} + def __init__(self, prompt: str, current_volumes: List[Subvolume]): + self._actions = [ + str(_('Add subvolume')), + str(_('Edit subvolume')), + str(_('Delete subvolume')) + ] + super().__init__(prompt, current_volumes, self._actions, self._actions[0]) - def action_list(self): - return super().action_list() + def reformat(self, data: List[Subvolume]) -> Dict[str, Subvolume]: + return {e.display(): e for e in data} - def exec_action(self, data: Dict): - if self.target: - origkey, origval = list(self.target.items())[0] - else: - origkey = None + def action_list(self): + active_user = self.target if self.target else None - if self.action == str(_('Delete')): - del data[origkey] + if active_user is None: + return [self._actions[0]] else: - if self.action == str(_('Add')): - self.target = {} - print(_('\n Fill the desired values for a new subvolume \n')) - with SubvolumeMenu(self.target,self.action) as add_menu: - for elem in ['name','mountpoint','options']: - add_menu.exec_option(elem) - else: - SubvolumeMenu(self.target,self.action).run() + return self._actions[1:] - data.update(self.target) + def _prompt_options(self, editing: Optional[Subvolume] = None) -> List[str]: + preset_options = [] + if editing: + preset_options = editing.options - return data - - -class SubvolumeMenu(GeneralMenu): - def __init__(self,parameters,action=None): - self.data = parameters - self.action = action - self.ds = {} - self.ds['name'] = None - self.ds['mountpoint'] = None - self.ds['options'] = None - if self.data: - origkey,origval = list(self.data.items())[0] - self.ds['name'] = origkey - if isinstance(origval,str): - self.ds['mountpoint'] = origval - else: - self.ds['mountpoint'] = self.data[origkey].get('mountpoint') - self.ds['options'] = self.data[origkey].get('options') - - super().__init__(data_store=self.ds) - - def _setup_selection_menu_options(self): - self._menu_options['name'] = Selector( - str(_('Subvolume name ')), - self._select_subvolume_name if not self.action or self.action in (str(_('Add')), str(_('Copy'))) else None, - mandatory=True, - enabled=True) - - self._menu_options['mountpoint'] = Selector( - str(_('Subvolume mountpoint')), - self._select_subvolume_mount_point if not self.action or self.action in (str(_('Add')),str(_('Edit'))) else None, - enabled=True) - - self._menu_options['options'] = Selector( - str(_('Subvolume options')), - self._select_subvolume_options if not self.action or self.action in (str(_('Add')),str(_('Edit'))) else None, - enabled=True) - - self._menu_options['save'] = Selector( - str(_('Save')), - exec_func=lambda n,v:True, - enabled=True) - - self._menu_options['cancel'] = Selector( - str(_('Cancel')), - # func = lambda pre:True, - exec_func=lambda n,v:self.fast_exit(n), - enabled=True) - - self.cancel_action = 'cancel' - self.save_action = 'save' - self.bottom_list = [self.save_action,self.cancel_action] - - def fast_exit(self,accion): - if self.option(accion).get_selection(): - for item in self.list_options(): - if self.option(item).is_mandatory(): - self.option(item).set_mandatory(False) - return True - - def exit_callback(self): - # we exit without moving data - if self.option(self.cancel_action).get_selection(): - return - if not self.ds['name']: - return - else: - key = self.ds['name'] - value = {} - if self.ds['mountpoint']: - value['mountpoint'] = self.ds['mountpoint'] - if self.ds['options']: - value['options'] = self.ds['options'] - self.data.update({key : value}) - - def _select_subvolume_name(self,value): - return TextInput(str(_("Subvolume name :")),value).run() - - def _select_subvolume_mount_point(self,value): - return TextInput(str(_("Select a mount point :")),value).run() - - def _select_subvolume_options(self,value) -> List[str]: - # def __init__(self, title, p_options, skip=True, multi=False, default_option=None, sort=True): choice = Menu( str(_("Select the desired subvolume options ")), ['nodatacow','compress'], skip=True, - preset_values=value, + preset_values=preset_options, multi=True ).run() if choice.type_ == MenuSelectionType.Selection: - return choice.value + return choice.value # type: ignore return [] + + def _add_subvolume(self, editing: Optional[Subvolume] = None) -> Optional[Subvolume]: + name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run() + + if not name: + return None + + mountpoint = TextInput(f'\n{_("Subvolume mountpoint")}: ', editing.mountpoint if editing else '').run() + + if not mountpoint: + return None + + options = self._prompt_options(editing) + + subvolume = Subvolume(name, mountpoint) + subvolume.compress = 'compress' in options + subvolume.nodatacow = 'nodatacow' in options + + return subvolume + + def exec_action(self, data: List[Subvolume]) -> List[Subvolume]: + if self.target: + active_subvolume = self.target + else: + active_subvolume = None + + if self.action == self._actions[0]: # add + new_subvolume = self._add_subvolume() + + if new_subvolume 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.name != new_subvolume.name] + data += [new_subvolume] + elif self.action == self._actions[1]: # edit subvolume + new_subvolume = self._add_subvolume(active_subvolume) + + if new_subvolume is not None: + # we'll remove the original subvolume and add the modified version + data = [d for d in data if d.name != active_subvolume.name and d.name != new_subvolume.name] + data += [new_subvolume] + elif self.action == self._actions[2]: # delete + data = [d for d in data if d != active_subvolume] + + return data -- cgit v1.2.3-70-g09d2 From cfea0d6d1a6f6b82fd4b65abd2124c8fc0530949 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 1 Aug 2022 17:44:57 +1000 Subject: Update translations (#1348) * Show translations in own tongue * Fix flake8 * Update * Update * Update * Update * fix mypy * Update * Update Co-authored-by: Daniel Girtler --- .github/workflows/mypy.yaml | 2 +- README.md | 24 +++ archinstall/__init__.py | 2 +- archinstall/lib/menu/global_menu.py | 3 +- archinstall/lib/menu/selection_menu.py | 22 +-- archinstall/lib/translation.py | 144 ------------------ archinstall/lib/translationhandler.py | 165 +++++++++++++++++++++ archinstall/lib/user_interaction/general_conf.py | 21 ++- .../lib/user_interaction/manage_users_conf.py | 4 +- archinstall/locales/README.md | 9 +- archinstall/locales/cyrillic.json | 19 --- archinstall/locales/languages.json | 28 ++-- examples/swiss.py | 34 +++-- 13 files changed, 267 insertions(+), 210 deletions(-) delete mode 100644 archinstall/lib/translation.py create mode 100644 archinstall/lib/translationhandler.py delete mode 100644 archinstall/locales/cyrillic.json (limited to 'archinstall/__init__.py') diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index 01d4741f..c03cd3cf 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -15,4 +15,4 @@ jobs: # one day this will be enabled # run: mypy --strict --module archinstall || exit 0 - name: run mypy - run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py archinstall/lib/disk/blockdevice.py archinstall/lib/user_interaction/subvolume_config.py archinstall/lib/disk/btrfs/btrfs_helpers.py archinstall/lib/translation.py + run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py archinstall/lib/disk/blockdevice.py archinstall/lib/user_interaction/subvolume_config.py archinstall/lib/disk/btrfs/btrfs_helpers.py archinstall/lib/translationhandler.py diff --git a/README.md b/README.md index 20224eea..b1df757a 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,29 @@ Assuming you are on a Arch Linux live-ISO and booted into EFI mode. # archinstall --config --disk-layout --creds +# Available Languages + +Archinstall is available in different languages which have been contributed and are maintained by the community. +Current translations are listed below and vary in the amount of translations per language +``` +English +Deutsch +Española +Française +Italiano +Nederlands +Polskie +Portugues do Brasil +Português +Svenska +Türk +čeština +русский +اردو +``` + +Any contributions to the translations are more than welcome, and to get started please follow [the guide](https://github.com/archlinux/archinstall/blob/master/archinstall/locales/README.md) + # Help? Submit an issue here on GitHub, or submit a post in the discord help channel.
@@ -57,6 +80,7 @@ This library is in turn used by the provided guided installer but is also for an Therefore, Archinstall will try its best to not introduce any breaking changes except for major releases which may break backwards compatibility after notifying about such changes. + # Scripting your own installation You could just copy [guided.py](https://github.com/archlinux/archinstall/blob/master/examples/guided.py) as a starting point. diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 1a360c67..f607a922 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -40,7 +40,7 @@ from .lib.menu.selection_menu import ( Selector, GeneralMenu ) -from .lib.translation import Translation, DeferredTranslation +from .lib.translationhandler import TranslationHandler, DeferredTranslation from .lib.plugins import plugins, load_plugin # This initiates the plugin loading ceremony from .lib.configuration import * from .lib.udev import udevadm_info diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py index 1a292476..1badc052 100644 --- a/archinstall/lib/menu/global_menu.py +++ b/archinstall/lib/menu/global_menu.py @@ -50,7 +50,8 @@ class GlobalMenu(GeneralMenu): Selector( _('Archinstall language'), lambda x: self._select_archinstall_language(x), - default='English') + display_func=lambda x: x.display_name, + default=self.translation_handler.get_language('en')) self._menu_options['keyboard-layout'] = \ Selector( _('Keyboard layout'), diff --git a/archinstall/lib/menu/selection_menu.py b/archinstall/lib/menu/selection_menu.py index c6ac5852..8a08812c 100644 --- a/archinstall/lib/menu/selection_menu.py +++ b/archinstall/lib/menu/selection_menu.py @@ -8,9 +8,11 @@ from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CH from .menu import Menu, MenuSelectionType from ..locale_helpers import set_keyboard_language from ..output import log -from ..translation import Translation +from ..translationhandler import TranslationHandler, Language from ..hsm.fido import get_fido2_devices +from ..user_interaction.general_conf import select_archinstall_language + if TYPE_CHECKING: _: Any @@ -181,7 +183,7 @@ class GeneralMenu: """ self._enabled_order :List[str] = [] - self._translation = Translation.load_nationalization() + self._translation_handler = TranslationHandler() self.is_context_mgr = False self._data_store = data_store if data_store is not None else {} self.auto_cursor = auto_cursor @@ -213,6 +215,10 @@ class GeneralMenu: self.exit_callback() + @property + def translation_handler(self) -> TranslationHandler: + return self._translation_handler + 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() @@ -461,14 +467,10 @@ class GeneralMenu: mandatory_waiting += 1 return mandatory_fields, mandatory_waiting - def _select_archinstall_language(self, preset_value: str) -> str: - from ... import select_archinstall_language - language = select_archinstall_language(preset_value) - if language is not None: - self._translation.activate(language) - return language - - return preset_value + def _select_archinstall_language(self, preset_value: Language) -> Language: + language = select_archinstall_language(self.translation_handler.translated_languages, preset_value) + self._translation_handler.activate(language) + return language def _select_hsm(self, preset :Optional[pathlib.Path] = None) -> Optional[pathlib.Path]: title = _('Select which partitions to mark for formatting:') diff --git a/archinstall/lib/translation.py b/archinstall/lib/translation.py deleted file mode 100644 index c20a4285..00000000 --- a/archinstall/lib/translation.py +++ /dev/null @@ -1,144 +0,0 @@ -from __future__ import annotations - -import json -import logging -import os -import gettext - -from pathlib import Path -from typing import List, Dict, Any, TYPE_CHECKING, Tuple -from .exceptions import TranslationError - -if TYPE_CHECKING: - _: Any - - -class LanguageDefinitions: - _languages = 'languages.json' - _cyrillic = 'cyrillic.json' - - def __init__(self): - self._mappings = self._get_language_mappings() - self._cyrillic_languages = self._get_cyrillic_languages() - - def is_cyrillic(self, language: str) -> bool: - return language in self._cyrillic_languages - - def _get_language_mappings(self) -> List[Dict[str, str]]: - locales_dir = Translation.get_locales_dir() - languages = Path.joinpath(locales_dir, self._languages) - - with open(languages, 'r') as fp: - return json.load(fp) - - def get_language(self, abbr: str) -> str: - for entry in self._mappings: - if entry['abbr'] == abbr: - return entry['lang'] - - raise ValueError(f'No language with abbreviation "{abbr}" found') - - def _get_cyrillic_languages(self) -> List[str]: - locales_dir = Translation.get_locales_dir() - languages = Path.joinpath(locales_dir, self._cyrillic) - - with open(languages, 'r') as fp: - data = json.load(fp) - return data['languages'] - - -class DeferredTranslation: - def __init__(self, message: str): - self.message = message - - def __len__(self) -> int: - return len(self.message) - - def __str__(self) -> str: - translate = _ - if translate is DeferredTranslation: - return self.message - return translate(self.message) - - def __lt__(self, other) -> bool: - return self.message < other - - def __gt__(self, other) -> bool: - return self.message > other - - def __add__(self, other) -> DeferredTranslation: - if isinstance(other, str): - other = DeferredTranslation(other) - - concat = self.message + other.message - return DeferredTranslation(concat) - - def format(self, *args) -> str: - return self.message.format(*args) - - @classmethod - def install(cls): - import builtins - builtins._ = cls - - -class Translation: - def __init__(self, locales_dir): - self._languages = {} - - for names in self._get_translation_lang(): - try: - self._languages[names[0]] = gettext.translation('base', localedir=locales_dir, languages=names) - except FileNotFoundError as error: - raise TranslationError(f"Could not locate language file for '{names}': {error}") - - def activate(self, name): - if language := self._languages.get(name, None): - languages = LanguageDefinitions() - - if languages.is_cyrillic(name): - self._set_font('UniCyr_8x16') - else: - # this will reset a possible previously set font to a default font - self._set_font('') - - language.install() - else: - raise ValueError(f'Language not supported: {name}') - - def _set_font(self, font: str): - from archinstall import SysCommand, log - try: - log(f'Setting new font: {font}', level=logging.DEBUG) - SysCommand(f'setfont {font}') - except Exception: - log(f'Unable to set font {font}', level=logging.ERROR) - - @classmethod - def load_nationalization(cls) -> Translation: - locales_dir = cls.get_locales_dir() - return Translation(locales_dir) - - @classmethod - def get_locales_dir(cls) -> Path: - cur_path = Path(__file__).parent.parent - locales_dir = Path.joinpath(cur_path, 'locales') - return locales_dir - - @classmethod - def _defined_languages(cls) -> List[str]: - locales_dir = cls.get_locales_dir() - filenames = os.listdir(locales_dir) - return list(filter(lambda x: len(x) == 2, filenames)) - - @classmethod - def _get_translation_lang(cls) -> List[Tuple[str, str]]: - def_languages = cls._defined_languages() - languages = LanguageDefinitions() - return [(languages.get_language(lang), lang) for lang in def_languages] - - @classmethod - def get_available_lang(cls) -> List[str]: - def_languages = cls._defined_languages() - languages = LanguageDefinitions() - return [languages.get_language(lang) for lang in def_languages] diff --git a/archinstall/lib/translationhandler.py b/archinstall/lib/translationhandler.py new file mode 100644 index 00000000..12c8da4a --- /dev/null +++ b/archinstall/lib/translationhandler.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +import json +import logging +import os +import gettext +from dataclasses import dataclass + +from pathlib import Path +from typing import List, Dict, Any, TYPE_CHECKING, Optional +from .exceptions import TranslationError + +if TYPE_CHECKING: + _: Any + + +@dataclass +class Language: + abbr: str + lang: str + translation: gettext.NullTranslations + translation_percent: int + translated_lang: Optional[str] + + @property + def display_name(self) -> str: + if self.translated_lang: + name = self.translated_lang + else: + name = self.lang + return f'{name} ({self.translation_percent}%)' + + def is_match(self, lang_or_translated_lang: str) -> bool: + if self.lang == lang_or_translated_lang: + return True + elif self.translated_lang == lang_or_translated_lang: + return True + return False + + +class TranslationHandler: + _base_pot = 'base.pot' + _languages = 'languages.json' + + def __init__(self): + # to display cyrillic languages correctly + self._set_font('UniCyr_8x16') + + self._total_messages = self._get_total_messages() + self._translated_languages = self._get_translations() + + @property + def translated_languages(self) -> List[Language]: + return self._translated_languages + + def _get_translations(self) -> List[Language]: + mappings = self._load_language_mappings() + defined_languages = self._defined_languages() + + languages = [] + + for short_form in defined_languages: + mapping_entry: Dict[str, Any] = next(filter(lambda x: x['abbr'] == short_form, mappings)) + abbr = mapping_entry['abbr'] + lang = mapping_entry['lang'] + translated_lang = mapping_entry.get('translated_lang', None) + + try: + translation = gettext.translation('base', localedir=self._get_locales_dir(), languages=(abbr, lang)) + + if abbr == 'en': + percent = 100 + else: + num_translations = self._get_catalog_size(translation) + percent = int((num_translations / self._total_messages) * 100) + + language = Language(abbr, lang, translation, percent, translated_lang) + languages.append(language) + except FileNotFoundError as error: + raise TranslationError(f"Could not locate language file for '{lang}': {error}") + + return languages + + def _set_font(self, font: str): + from archinstall import SysCommand, log + try: + log(f'Setting font: {font}', level=logging.DEBUG) + SysCommand(f'setfont {font}') + except Exception: + log(f'Unable to set font {font}', level=logging.ERROR) + + def _load_language_mappings(self) -> List[Dict[str, Any]]: + locales_dir = self._get_locales_dir() + languages = Path.joinpath(locales_dir, self._languages) + + with open(languages, 'r') as fp: + return json.load(fp) + + def _get_catalog_size(self, translation: gettext.NullTranslations) -> int: + # this is a ery naughty way of retrieving the data but + # there's no alternative method exposed unfortunately + catalog = translation._catalog # type: ignore + messages = {k: v for k, v in catalog.items() if k and v} + return len(messages) + + def _get_total_messages(self) -> int: + locales = self._get_locales_dir() + with open(f'{locales}/{self._base_pot}', 'r') as fp: + lines = fp.readlines() + msgid_lines = [line for line in lines if 'msgid' in line] + return len(msgid_lines) - 1 # don't count the first line which contains the metadata + + def get_language(self, abbr: str) -> Language: + try: + return next(filter(lambda x: x.abbr == abbr, self._translated_languages)) + except Exception: + raise ValueError(f'No language with abbreviation "{abbr}" found') + + def activate(self, language: Language): + language.translation.install() + + def _get_locales_dir(self) -> Path: + cur_path = Path(__file__).parent.parent + locales_dir = Path.joinpath(cur_path, 'locales') + return locales_dir + + def _defined_languages(self) -> List[str]: + locales_dir = self._get_locales_dir() + filenames = os.listdir(locales_dir) + return list(filter(lambda x: len(x) == 2 or x == 'pt_BR', filenames)) + + +class DeferredTranslation: + def __init__(self, message: str): + self.message = message + + def __len__(self) -> int: + return len(self.message) + + def __str__(self) -> str: + translate = _ + if translate is DeferredTranslation: + return self.message + return translate(self.message) + + def __lt__(self, other) -> bool: + return self.message < other + + def __gt__(self, other) -> bool: + return self.message > other + + def __add__(self, other) -> DeferredTranslation: + if isinstance(other, str): + other = DeferredTranslation(other) + + concat = self.message + other.message + return DeferredTranslation(concat) + + def format(self, *args) -> str: + return self.message.format(*args) + + @classmethod + def install(cls): + import builtins + builtins._ = cls diff --git a/archinstall/lib/user_interaction/general_conf.py b/archinstall/lib/user_interaction/general_conf.py index 15c42b86..bdc602b3 100644 --- a/archinstall/lib/user_interaction/general_conf.py +++ b/archinstall/lib/user_interaction/general_conf.py @@ -12,7 +12,7 @@ from ..output import log from ..profiles import Profile, list_profiles from ..mirrors import list_mirrors -from ..translation import Translation +from ..translationhandler import Language from ..packages.packages import validate_package_list from ..storage import storage @@ -118,13 +118,22 @@ def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: case _: return {selected: mirrors[selected] for selected in selected_mirror.value} -def select_archinstall_language(preset_values: str): - languages = Translation.get_available_lang() - choice = Menu(_('Archinstall language'), languages, default_option=preset_values).run() +def select_archinstall_language(languages: List[Language], preset_value: 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} + + choice = Menu( + _('Archinstall language'), + list(options.keys()), + default_option=preset_value.display_name + ).run() match choice.type_: - case MenuSelectionType.Esc: return preset_values - case MenuSelectionType.Selection: return choice.value + case MenuSelectionType.Esc: return preset_value + case MenuSelectionType.Selection: + return options[choice.value] def select_profile(preset) -> Optional[Profile]: diff --git a/archinstall/lib/user_interaction/manage_users_conf.py b/archinstall/lib/user_interaction/manage_users_conf.py index a97328c2..84ce3556 100644 --- a/archinstall/lib/user_interaction/manage_users_conf.py +++ b/archinstall/lib/user_interaction/manage_users_conf.py @@ -57,10 +57,10 @@ class UserList(ListManager): 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), 1) + user = next(filter(lambda x: x == entry, data)) user.password = new_password elif action == self._actions[2]: # promote/demote - user = next(filter(lambda x: x == entry, data), 1) + user = next(filter(lambda x: x == entry, data)) user.sudo = False if user.sudo else True elif action == self._actions[3]: # delete data = [d for d in data if d != entry] diff --git a/archinstall/locales/README.md b/archinstall/locales/README.md index 70822c05..51662702 100644 --- a/archinstall/locales/README.md +++ b/archinstall/locales/README.md @@ -5,7 +5,7 @@ Archinstall supports multiple languages, which depend on translations coming fro New languages can be added simply by creating a new folder with the proper language abbrevation (see list `languages.json` if unsure). Run the following command to create a new template for a language ``` - mkdir -p /LC_MESSAGES/ && touch /LC_MESSAGES/base.po +mkdir -p /LC_MESSAGES/ && touch /LC_MESSAGES/base.po ``` After that run the script `./locales_generator.sh` it will automatically populate the new `base.po` file with the strings that @@ -31,3 +31,10 @@ msgstr "Wollen sie wirklich abbrechen?" After the translations have been written, run the script once more `./locales_generator.sh` and it will auto-generate the `base.mo` file with the included translations. After that you're all ready to go and enjoy Archinstall in the new language :) + +To display the language inside Archinstall in your own tongue, please edit the file `languages.json` and +add a `translated_lang` entry to the respective language, e.g. + +``` + {"abbr": "pl", "lang": "Polish", "translated_lang": "Polskie"} +``` diff --git a/archinstall/locales/cyrillic.json b/archinstall/locales/cyrillic.json deleted file mode 100644 index 13f11ad0..00000000 --- a/archinstall/locales/cyrillic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "languages": [ - "Abkhazian", - "Azerbaijani", - "Bashkir", - "Belarusian", - "Bulgarian", - "Chuvash", - "Komi", - "Macedonian", - "Mongolian", - "Russian", - "Serbo-Croatian", - "Tajik", - "Tatar", - "Ukrainian", - "Uzbek" - ] -} diff --git a/archinstall/locales/languages.json b/archinstall/locales/languages.json index c3c9d2c2..883042e8 100644 --- a/archinstall/locales/languages.json +++ b/archinstall/locales/languages.json @@ -20,7 +20,7 @@ {"abbr": "br", "lang": "Breton"}, {"abbr": "bg", "lang": "Bulgarian"}, {"abbr": "ca", "lang": "Catalan"}, - {"abbr": "cs", "lang": "Czech"}, + {"abbr": "cs", "lang": "Czech", "translated_lang": "čeština"}, {"abbr": "ch", "lang": "Chamorro"}, {"abbr": "ce", "lang": "Chechen"}, {"abbr": "cu", "lang": "Church Slavic"}, @@ -29,8 +29,8 @@ {"abbr": "co", "lang": "Corsican"}, {"abbr": "cr", "lang": "Cree"}, {"abbr": "cy", "lang": "Welsh"}, - {"abbr": "da", "lang": "Danish"}, - {"abbr": "de", "lang": "German"}, + {"abbr": "da", "lang": "Danish", "translated_lang": "Dansk"}, + {"abbr": "de", "lang": "German", "translated_lang": "Deutsch"}, {"abbr": "dv", "lang": "Dhivehi"}, {"abbr": "dz", "lang": "Dzongkha"}, {"abbr": "el", "lang": "Modern Greek (1453-)"}, @@ -43,7 +43,7 @@ {"abbr": "fa", "lang": "Persian"}, {"abbr": "fj", "lang": "Fijian"}, {"abbr": "fi", "lang": "Finnish"}, - {"abbr": "fr", "lang": "French"}, + {"abbr": "fr", "lang": "French", "translated_lang": "Française"}, {"abbr": "fy", "lang": "Western Frisian"}, {"abbr": "ff", "lang": "Fulah"}, {"abbr": "gd", "lang": "Scottish Gaelic"}, @@ -71,7 +71,7 @@ {"abbr": "id", "lang": "Indonesian"}, {"abbr": "ik", "lang": "Inupiaq"}, {"abbr": "is", "lang": "Icelandic"}, - {"abbr": "it", "lang": "Italian"}, + {"abbr": "it", "lang": "Italian", "translated_lang": "Italiano"}, {"abbr": "jv", "lang": "Javanese"}, {"abbr": "ja", "lang": "Japanese"}, {"abbr": "kl", "lang": "Kalaallisut"}, @@ -114,7 +114,7 @@ {"abbr": "nd", "lang": "North Ndebele"}, {"abbr": "ng", "lang": "Ndonga"}, {"abbr": "ne", "lang": "Nepali (macrolanguage)"}, - {"abbr": "nl", "lang": "Dutch"}, + {"abbr": "nl", "lang": "Dutch", "translated_lang": "Nederlands"}, {"abbr": "nn", "lang": "Norwegian Nynorsk"}, {"abbr": "nb", "lang": "Norwegian Bokmål"}, {"abbr": "no", "lang": "Norwegian"}, @@ -126,15 +126,15 @@ {"abbr": "os", "lang": "Ossetian"}, {"abbr": "pa", "lang": "Panjabi"}, {"abbr": "pi", "lang": "Pali"}, - {"abbr": "pl", "lang": "Polish"}, - {"abbr": "pt", "lang": "Portuguese"}, - {"abbr": "pt_BR", "lang": "Brazilian Portuguese"}, + {"abbr": "pl", "lang": "Polish", "translated_lang": "Polskie"}, + {"abbr": "pt", "lang": "Portuguese", "translated_lang": "Português"}, + {"abbr": "pt_BR", "lang": "Brazilian Portuguese", "translated_lang": "Portugues do Brasil"}, {"abbr": "ps", "lang": "Pushto"}, {"abbr": "qu", "lang": "Quechua"}, {"abbr": "rm", "lang": "Romansh"}, {"abbr": "ro", "lang": "Romanian"}, {"abbr": "rn", "lang": "Rundi"}, - {"abbr": "ru", "lang": "Russian"}, + {"abbr": "ru", "lang": "Russian", "translated_lang": "русский"}, {"abbr": "sg", "lang": "Sango"}, {"abbr": "sa", "lang": "Sanskrit"}, {"abbr": "si", "lang": "Sinhala"}, @@ -146,14 +146,14 @@ {"abbr": "sd", "lang": "Sindhi"}, {"abbr": "so", "lang": "Somali"}, {"abbr": "st", "lang": "Southern Sotho"}, - {"abbr": "es", "lang": "Spanish"}, + {"abbr": "es", "lang": "Spanish", "translated_lang": "Española"}, {"abbr": "sq", "lang": "Albanian"}, {"abbr": "sc", "lang": "Sardinian"}, {"abbr": "sr", "lang": "Serbian"}, {"abbr": "ss", "lang": "Swati"}, {"abbr": "su", "lang": "Sundanese"}, {"abbr": "sw", "lang": "Swahili (macrolanguage)"}, - {"abbr": "sv", "lang": "Swedish"}, + {"abbr": "sv", "lang": "Swedish", "translated_lang": "Svenska"}, {"abbr": "ty", "lang": "Tahitian"}, {"abbr": "ta", "lang": "Tamil"}, {"abbr": "tt", "lang": "Tatar"}, @@ -166,11 +166,11 @@ {"abbr": "tn", "lang": "Tswana"}, {"abbr": "ts", "lang": "Tsonga"}, {"abbr": "tk", "lang": "Turkmen"}, - {"abbr": "tr", "lang": "Turkish"}, + {"abbr": "tr", "lang": "Turkish", "translated_lang" : "Türk"}, {"abbr": "tw", "lang": "Twi"}, {"abbr": "ug", "lang": "Uighur"}, {"abbr": "uk", "lang": "Ukrainian"}, - {"abbr": "ur", "lang": "Urdu"}, + {"abbr": "ur", "lang": "Urdu", "translated_lang": "اردو"}, {"abbr": "uz", "lang": "Uzbek"}, {"abbr": "ve", "lang": "Venda"}, {"abbr": "vi", "lang": "Vietnamese"}, diff --git a/examples/swiss.py b/examples/swiss.py index 83b79c09..5d40dc68 100644 --- a/examples/swiss.py +++ b/examples/swiss.py @@ -158,24 +158,36 @@ class SetupMenu(archinstall.GeneralMenu): super().__init__(data_store=storage_area) def _setup_selection_menu_options(self): - self.set_option('archinstall-language', + self.set_option( + 'archinstall-language', archinstall.Selector( _('Archinstall language'), lambda x: self._select_archinstall_language(x), - default='English', - enabled=True)) - self.set_option('ntp', - archinstall.Selector( - 'Activate NTP', - lambda x: select_activate_NTP(), - default='Y', - enabled=True)) - self.set_option('mode', + display_func=lambda x: x.display_name, + default=self.translation_handler.get_language('en'), + enabled=True + ) + ) + + self.set_option( + 'ntp', + archinstall.Selector( + 'Activate NTP', + lambda x: select_activate_NTP(), + default='Y', + enabled=True + ) + ) + + self.set_option( + 'mode', archinstall.Selector( 'Excution mode', lambda x : select_mode(), default='full', - enabled=True)) + enabled=True) + ) + for item in ['LC_ALL','LC_CTYPE','LC_NUMERIC','LC_TIME','LC_MESSAGES','LC_COLLATE']: self.set_option(item, archinstall.Selector( -- cgit v1.2.3-70-g09d2 From 3e10fc106b27c70593147b1c8aef1be221106d75 Mon Sep 17 00:00:00 2001 From: Metin Yazici Date: Tue, 9 Aug 2022 20:14:28 +0200 Subject: Add command line option to get the program version (#1406) --- archinstall/__init__.py | 1 + 1 file changed, 1 insertion(+) (limited to 'archinstall/__init__.py') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index f607a922..3e9f8391 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -66,6 +66,7 @@ def define_arguments(): Remember that the property/entry name python assigns to the parameters is the first string defined as argument and dashes inside it '-' are changed to '_' """ + parser.add_argument("-v", "--version", action="version", version="%(prog)s " + __version__) parser.add_argument("--config", nargs="?", help="JSON configuration file or URL") parser.add_argument("--creds", nargs="?", help="JSON credentials configuration file") parser.add_argument("--disk_layouts","--disk_layout","--disk-layouts","--disk-layout",nargs="?", -- cgit v1.2.3-70-g09d2 From 1086fd686d28317851c7f09eb00898648ecaaad7 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Thu, 11 Aug 2022 18:06:02 +0200 Subject: Cleaned up argument loading slighly. (#1419) * Cleaned up argument loading slighly. Also flipped some --silent logic to avoid double negatives. --plugin and --conf {'plugin': ...} should now both work. * Tweaked xorg profile to use list instead of strings. Because strings causes some issues through add_additional_packages() as it ends up as [(xorg, xorg-xinit), nano] instead of a flat list of packages or string. * Tweaked xorg profile to use list instead of strings. Because strings causes some issues through add_additional_packages() as it ends up as [(xorg, xorg-xinit), nano] instead of a flat list of packages or string. --- archinstall/__init__.py | 30 +++++++++++++++++++++++++----- archinstall/lib/plugins.py | 2 ++ profiles/xorg.py | 10 +++++----- 3 files changed, 32 insertions(+), 10 deletions(-) (limited to 'archinstall/__init__.py') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 3e9f8391..090ad7de 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -1,5 +1,6 @@ """Arch Linux installer - guided, templates etc.""" -from argparse import ArgumentParser +import typing +from argparse import ArgumentParser, Namespace from .lib.disk import * from .lib.exceptions import * @@ -132,6 +133,24 @@ def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, erro print(f" We ignore the entry {element} as it isn't related to any argument") return config +def cleanup_empty_args(args :typing.Union[Namespace, dict]) -> dict: + """ + Takes arguments (dictionary or argparse Namespace) and removes any + None values. This ensures clean mergers during dict.update(args) + """ + if type(args) == Namespace: + args = vars(args) + + clean_args = {} + for key, val in args.items(): + if type(val) == dict: + val = cleanup_empty_args(val) + + if val is not None: + clean_args[key] = val + + return clean_args + def get_arguments() -> Dict[str, Any]: """ The handling of parameters from the command line Is done on following steps: @@ -159,14 +178,15 @@ def get_arguments() -> Dict[str, Any]: exit(1) # load the parameters. first the known, then the unknowns - config.update(vars(args)) + args = cleanup_empty_args(args) + config.update(args) config.update(parse_unspecified_argument_list(unknowns)) # amend the parameters (check internal consistency) # Installation can't be silent if config is not passed - if args.config is not None : - config["silent"] = args.silent - else: + if args.get('config') is None: config["silent"] = False + else: + config["silent"] = args.get('silent') # avoiding a compatibility issue if 'dry-run' in config: diff --git a/archinstall/lib/plugins.py b/archinstall/lib/plugins.py index f771aacb..0ff63610 100644 --- a/archinstall/lib/plugins.py +++ b/archinstall/lib/plugins.py @@ -73,6 +73,7 @@ def find_nth(haystack :List[str], needle :str, n :int) -> int: def load_plugin(path :str) -> ModuleType: parsed_url = urllib.parse.urlparse(path) + log(f"Loading plugin {parsed_url}.", fg="gray", level=logging.INFO) # The Profile was not a direct match on a remote URL if not parsed_url.scheme: @@ -96,6 +97,7 @@ def load_plugin(path :str) -> ModuleType: if hasattr(sys.modules[namespace], 'Plugin'): try: plugins[namespace] = sys.modules[namespace].Plugin() + log(f"Plugin {plugins[namespace]} has been loaded.", fg="gray", level=logging.INFO) except Exception as err: log(err, level=logging.ERROR) log(f"The above error was detected when initiating the plugin: {path}", fg="red", level=logging.ERROR) diff --git a/profiles/xorg.py b/profiles/xorg.py index 2ce8dcc2..de45acd3 100644 --- a/profiles/xorg.py +++ b/profiles/xorg.py @@ -47,9 +47,9 @@ if __name__ == 'xorg': for kernel in archinstall.storage['installation_session'].kernels: archinstall.storage['installation_session'].add_additional_packages(f"{kernel}-headers") # Fixes https://github.com/archlinux/archinstall/issues/585 archinstall.storage['installation_session'].add_additional_packages("dkms") # I've had kernel regen fail if it wasn't installed before nvidia-dkms - archinstall.storage['installation_session'].add_additional_packages("xorg-server xorg-xinit nvidia-dkms") + archinstall.storage['installation_session'].add_additional_packages("xorg-server", "xorg-xinit", "nvidia-dkms") else: - archinstall.storage['installation_session'].add_additional_packages(f"xorg-server xorg-xinit {' '.join(archinstall.storage.get('gfx_driver_packages', []))}") + archinstall.storage['installation_session'].add_additional_packages(f"xorg-server", "xorg-xinit", *archinstall.storage.get('gfx_driver_packages', [])) elif 'amdgpu' in archinstall.storage.get("gfx_driver_packages", []): # The order of these two are important if amdgpu is installed #808 if 'amdgpu' in archinstall.storage['installation_session'].MODULES: @@ -60,9 +60,9 @@ if __name__ == 'xorg': archinstall.storage['installation_session'].MODULES.remove('radeon') archinstall.storage['installation_session'].MODULES.append('radeon') - archinstall.storage['installation_session'].add_additional_packages(f"xorg-server xorg-xinit {' '.join(archinstall.storage.get('gfx_driver_packages', []))}") + archinstall.storage['installation_session'].add_additional_packages(f"xorg-server", "xorg-xinit", *archinstall.storage.get('gfx_driver_packages', [])) else: - archinstall.storage['installation_session'].add_additional_packages(f"xorg-server xorg-xinit {' '.join(archinstall.storage.get('gfx_driver_packages', []))}") + archinstall.storage['installation_session'].add_additional_packages(f"xorg-server", "xorg-xinit", *archinstall.storage.get('gfx_driver_packages', [])) except Exception as err: archinstall.log(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}", level=logging.WARNING, fg="yellow") - archinstall.storage['installation_session'].add_additional_packages("xorg-server xorg-xinit") # Prep didn't run, so there's no driver to install + archinstall.storage['installation_session'].add_additional_packages("xorg-server", "xorg-xinit") # Prep didn't run, so there's no driver to install -- cgit v1.2.3-70-g09d2 From ea407f5c9de2966ee741198e55fcfa8f29760295 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Tue, 30 Aug 2022 23:57:44 +0200 Subject: Version bump to v2.5.1rc1 in prep for release --- archinstall/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/__init__.py') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 090ad7de..184097b1 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -51,7 +51,7 @@ from .lib.hsm import ( ) parser = ArgumentParser() -__version__ = "2.5.0" +__version__ = "2.5.1rc1" storage['__version__'] = __version__ # add the custome _ as a builtin, it can now be used anywhere in the -- cgit v1.2.3-70-g09d2 From 4dcd5e684f9461145c5b8656b1a91f99ace26b27 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 6 Sep 2022 16:31:08 +1000 Subject: Move deserialization into init (#1456) Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 15 +- archinstall/lib/menu/global_menu.py | 57 +++----- archinstall/lib/translationhandler.py | 50 ++++++- archinstall/locales/ar/LC_MESSAGES/base.po | 192 ++++++++++++-------------- archinstall/locales/base.pot | 36 +++++ archinstall/locales/cs/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/de/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/el/LC_MESSAGES/base.po | 6 +- archinstall/locales/en/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/es/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/fr/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/it/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/nl/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/pl/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/pt/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/pt_BR/LC_MESSAGES/base.po | 27 +++- archinstall/locales/ru/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/sv/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/ta/LC_MESSAGES/base.po | 6 +- archinstall/locales/tr/LC_MESSAGES/base.po | 26 ++++ archinstall/locales/ur/LC_MESSAGES/base.po | 26 ++++ examples/swiss.py | 2 +- 22 files changed, 581 insertions(+), 148 deletions(-) (limited to 'archinstall/__init__.py') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 184097b1..4e1e6d6d 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -195,40 +195,53 @@ def get_arguments() -> Dict[str, Any]: return config def load_config(): - from .lib.models import NetworkConfiguration """ refine and set some arguments. Formerly at the scripts """ + from .lib.models import NetworkConfiguration + + if (archinstall_lang := arguments.get('archinstall-language', None)) is not None: + arguments['archinstall-language'] = TranslationHandler().get_language_by_name(archinstall_lang) + if arguments.get('harddrives', None) is not None: if type(arguments['harddrives']) is str: arguments['harddrives'] = arguments['harddrives'].split(',') arguments['harddrives'] = [BlockDevice(BlockDev) for BlockDev in arguments['harddrives']] # Temporarily disabling keep_partitions if config file is loaded # Temporary workaround to make Desktop Environments work + if arguments.get('profile', None) is not None: if type(arguments.get('profile', None)) is dict: arguments['profile'] = Profile(None, arguments.get('profile', None)['path']) else: arguments['profile'] = Profile(None, arguments.get('profile', None)) + storage['_desktop_profile'] = arguments.get('desktop-environment', None) + 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: list_mirrors()[selected_region]} + if arguments.get('sys-language', None) is not None: arguments['sys-language'] = arguments.get('sys-language', 'en_US') + if arguments.get('sys-encoding', None) is not None: arguments['sys-encoding'] = arguments.get('sys-encoding', 'utf-8') + if arguments.get('gfx_driver', None) is not None: storage['gfx_driver_packages'] = AVAILABLE_GFX_DRIVERS.get(arguments.get('gfx_driver', None), None) + if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) + if arguments.get('nic', None) is not None: handler = NetworkConfigurationHandler() handler.parse_arguments(arguments.get('nic')) arguments['nic'] = handler.configuration + if arguments.get('!users', None) is not None or arguments.get('!superusers', None) is not None: users = arguments.get('!users', None) superusers = arguments.get('!superusers', None) diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py index fc7c90bc..d1bec189 100644 --- a/archinstall/lib/menu/global_menu.py +++ b/archinstall/lib/menu/global_menu.py @@ -3,54 +3,41 @@ from __future__ import annotations from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING import archinstall - -from ..menu import Menu -from ..menu.selection_menu import Selector, GeneralMenu +from ..disk import encrypted_partitions from ..general import SysCommand, secret from ..hardware import has_uefi +from ..menu import Menu +from ..menu.selection_menu import Selector, GeneralMenu from ..models import NetworkConfiguration -from ..storage import storage +from ..models.users import User +from ..output import FormattedOutput from ..profiles import is_desktop_profile, Profile -from ..disk import encrypted_partitions - -from ..user_interaction import get_password, ask_for_a_timezone, save_config -from ..user_interaction import ask_ntp -from ..user_interaction import ask_for_swap +from ..storage import storage +from ..user_interaction import add_number_of_parrallel_downloads +from ..user_interaction import ask_additional_packages_to_install +from ..user_interaction import ask_for_additional_users +from ..user_interaction import ask_for_audio_selection from ..user_interaction import ask_for_bootloader +from ..user_interaction import ask_for_swap from ..user_interaction import ask_hostname -from ..user_interaction import ask_for_audio_selection -from ..user_interaction import ask_additional_packages_to_install +from ..user_interaction import ask_ntp from ..user_interaction import ask_to_configure_network -from ..user_interaction import ask_for_additional_users -from ..user_interaction import select_language -from ..user_interaction import select_mirror_regions -from ..user_interaction import select_locale_lang -from ..user_interaction import select_locale_enc +from ..user_interaction import get_password, ask_for_a_timezone, save_config +from ..user_interaction import select_additional_repositories from ..user_interaction import select_disk_layout -from ..user_interaction import select_kernel from ..user_interaction import select_encrypted_partitions from ..user_interaction import select_harddrives +from ..user_interaction import select_kernel +from ..user_interaction import select_language +from ..user_interaction import select_locale_enc +from ..user_interaction import select_locale_lang +from ..user_interaction import select_mirror_regions from ..user_interaction import select_profile -from ..user_interaction import select_additional_repositories -from ..user_interaction import add_number_of_parrallel_downloads -from ..models.users import User from ..user_interaction.partitioning_conf import current_partition_layout -from ..output import FormattedOutput -from ..translationhandler import Language if TYPE_CHECKING: _: Any -def display_language(global_menu, x): - if type(x) == Language: - return x - elif type(x) == str: - translation_handler = global_menu._translation_handler - for language in translation_handler._get_translations(): - if language.lang == x: - return language - else: - raise ValueError(f"Language entry needs to Language() object or string of full language like 'English'.") class GlobalMenu(GeneralMenu): def __init__(self,data_store): @@ -62,9 +49,9 @@ class GlobalMenu(GeneralMenu): self._menu_options['archinstall-language'] = \ Selector( _('Archinstall language'), - lambda x: self._select_archinstall_language(display_language(self, x)), - display_func=lambda x: display_language(self, x).display_name, - default=self.translation_handler.get_language('en')) + 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'] = \ Selector( _('Keyboard layout'), diff --git a/archinstall/lib/translationhandler.py b/archinstall/lib/translationhandler.py index 58b7ebd4..d6b3ccb6 100644 --- a/archinstall/lib/translationhandler.py +++ b/archinstall/lib/translationhandler.py @@ -40,6 +40,7 @@ class Language: def json(self) -> str: return self.lang + class TranslationHandler: _base_pot = 'base.pot' _languages = 'languages.json' @@ -48,7 +49,7 @@ class TranslationHandler: # to display cyrillic languages correctly self._set_font('UniCyr_8x16') - self._total_messages = self._get_total_messages() + self._total_messages = self._get_total_active_messages() self._translated_languages = self._get_translations() @property @@ -56,6 +57,9 @@ class TranslationHandler: return self._translated_languages def _get_translations(self) -> List[Language]: + """ + Load all translated languages and return a list of such + """ mappings = self._load_language_mappings() defined_languages = self._defined_languages() @@ -68,13 +72,17 @@ class TranslationHandler: translated_lang = mapping_entry.get('translated_lang', None) try: + # get a translation for a specific language translation = gettext.translation('base', localedir=self._get_locales_dir(), languages=(abbr, lang)) + # calculate the percentage of total translated text to total number of messages if abbr == 'en': percent = 100 else: num_translations = self._get_catalog_size(translation) percent = int((num_translations / self._total_messages) * 100) + # prevent cases where the .pot file is out of date and the percentage is above 100 + percent = min(100, percent) language = Language(abbr, lang, translation, percent, translated_lang) languages.append(language) @@ -84,6 +92,9 @@ class TranslationHandler: return languages def _set_font(self, font: str): + """ + Set the provided font as the new terminal font + """ from archinstall import SysCommand, log try: log(f'Setting font: {font}', level=logging.DEBUG) @@ -92,6 +103,9 @@ class TranslationHandler: log(f'Unable to set font {font}', level=logging.ERROR) def _load_language_mappings(self) -> List[Dict[str, Any]]: + """ + Load the mapping table of all known languages + """ locales_dir = self._get_locales_dir() languages = Path.joinpath(locales_dir, self._languages) @@ -99,34 +113,62 @@ class TranslationHandler: return json.load(fp) def _get_catalog_size(self, translation: gettext.NullTranslations) -> int: - # this is a ery naughty way of retrieving the data but + """ + Get the number of translated messages for a translations + """ + # this is a very naughty way of retrieving the data but # there's no alternative method exposed unfortunately catalog = translation._catalog # type: ignore messages = {k: v for k, v in catalog.items() if k and v} return len(messages) - def _get_total_messages(self) -> int: + def _get_total_active_messages(self) -> int: + """ + Get total messages that could be translated + """ locales = self._get_locales_dir() with open(f'{locales}/{self._base_pot}', 'r') as fp: lines = fp.readlines() msgid_lines = [line for line in lines if 'msgid' in line] + return len(msgid_lines) - 1 # don't count the first line which contains the metadata - def get_language(self, abbr: str) -> Language: + def get_language_by_name(self, name: str) -> Language: + """ + Get a language object by it's name, e.g. English + """ + try: + return next(filter(lambda x: x.lang == name, self._translated_languages)) + except Exception: + raise ValueError(f'No language with name found: {name}') + + def get_language_by_abbr(self, abbr: str) -> Language: + """ + Get a language object by its abbrevation, e.g. en + """ try: return next(filter(lambda x: x.abbr == abbr, self._translated_languages)) except Exception: raise ValueError(f'No language with abbreviation "{abbr}" found') def activate(self, language: Language): + """ + Set the provided language as the current translation + """ language.translation.install() def _get_locales_dir(self) -> Path: + """ + Get the locales directory path + """ cur_path = Path(__file__).parent.parent locales_dir = Path.joinpath(cur_path, 'locales') return locales_dir def _defined_languages(self) -> List[str]: + """ + Get a list of all known languages + """ locales_dir = self._get_locales_dir() filenames = os.listdir(locales_dir) return list(filter(lambda x: len(x) == 2 or x == 'pt_BR', filenames)) diff --git a/archinstall/locales/ar/LC_MESSAGES/base.po b/archinstall/locales/ar/LC_MESSAGES/base.po index aad0f928..ac14f102 100644 --- a/archinstall/locales/ar/LC_MESSAGES/base.po +++ b/archinstall/locales/ar/LC_MESSAGES/base.po @@ -3,26 +3,22 @@ # zer0-x, 2022. msgid "" msgstr "" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Last-Translator: zer0-x\n" -"PO-Revision-Date: 2022-06-16 03:35+0300\n" "Project-Id-Version: \n" +"PO-Revision-Date: 2022-06-16 03:35+0300\n" +"Last-Translator: zer0-x\n" "Language-Team: Arabic\n" "Language: ar\n" "MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" "X-Generator: Lokalize 22.04.2\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" msgid "[!] A log file has been created here: {} {}" msgstr "[!] مِلَف سِجِل أُنشِأ هُنا: {} {}" -msgid "" -" Please submit this issue (and file) to" -" https://github.com/archlinux/archinstall/issues" -msgstr "" -" يُرجى تسليم تقرير عن هذا الخلل (مع المِلَف) إلى" -" https://github.com/archlinux/archinstall/issues" +msgid " Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues" +msgstr " يُرجى تسليم تقرير عن هذا الخلل (مع المِلَف) إلى https://github.com/archlinux/archinstall/issues" msgid "Do you really want to abort?" msgstr "هل تُريدُ حقًا إجهاضَ العَملِيَّة؟" @@ -57,41 +53,26 @@ msgstr "اختر مُحمّل الإقلاع" msgid "Choose an audio server" msgstr "اختر خادِم صوتيات" -msgid "" -"Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and" -" optional profile packages are installed." -msgstr "" -"فقط الحزم مثل base وbase-devel وlinux وlinux-firmware وefibootmgr و" -" حِزم مِلف اختيارية سوف تُثَبَّت." +msgid "Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed." +msgstr "فقط الحزم مثل base وbase-devel وlinux وlinux-firmware وefibootmgr و حِزم مِلف اختيارية سوف تُثَبَّت." -msgid "" -"If you desire a web browser, such as firefox or chromium, you may specify it" -" in the following prompt." -msgstr "" -"إذا كنت ترغب في متصفح الويب ، مثل Firefox أو chromium، فيمكنك تحديده" -" في موضِع الكتابة التالي." +msgid "If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt." +msgstr "إذا كنت ترغب في متصفح الويب ، مثل Firefox أو chromium، فيمكنك تحديده في موضِع الكتابة التالي." -msgid "" -"Write additional packages to install (space separated, leave blank to skip): " +msgid "Write additional packages to install (space separated, leave blank to skip): " msgstr "اكتب حزمًا إضافية لتثبيتها (تُفصَل بالمسافات، اتركها فارغة للتخطي):" msgid "Copy ISO network configuration to installation" msgstr "انسخ إعداد شبكة الـISO للتثبيت" -msgid "" -"Use NetworkManager (necessary to configure internet graphically in GNOME and" -" KDE)" -msgstr "" -"استخدم مُدير الشبكة (ضروري لإعداد الإنترنت باستخدام واجهة رسومية في جنوم و" -" كيدي)" +msgid "Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)" +msgstr "استخدم مُدير الشبكة (ضروري لإعداد الإنترنت باستخدام واجهة رسومية في جنوم و كيدي)" msgid "Select one network interface to configure" msgstr "حدِّد واجهة شبكة واحدة للإعداد" -msgid "" -"Select which mode to configure for \"{}\" or skip to use default mode \"{}\"" -msgstr "" -"حدد الوضع المراد تهيئته لـ\"{}\" أو تخطى لاستخدام الوضع الافتراضي \"{}\"" +msgid "Select which mode to configure for \"{}\" or skip to use default mode \"{}\"" +msgstr "حدد الوضع المراد تهيئته لـ\"{}\" أو تخطى لاستخدام الوضع الافتراضي \"{}\"" msgid "Enter the IP and subnet for {} (example: 192.168.0.5/24): " msgstr "أدخِل الIP مع تجزئة الشبكة لـ{} (على سبيل المثال: 192.168.0.5/24): " @@ -119,8 +100,7 @@ msgstr "" msgid "Enter the start sector (percentage or block number, default: {}): " msgstr "" -msgid "" -"Enter the end sector of the partition (percentage or block number, ex: {}): " +msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " msgstr "" msgid "{} contains queued partitions, this will remove those, are you sure?" @@ -138,9 +118,7 @@ msgid "" "Select by index which partition to mount where" msgstr "" -msgid "" -" * Partition mount-points are relative to inside the installation, the boot" -" would be /boot as an example." +msgid " * Partition mount-points are relative to inside the installation, the boot would be /boot as an example." msgstr "" msgid "Select where to mount partition (leave blank to remove mountpoint): " @@ -179,16 +157,13 @@ msgstr "" msgid "Wipe all selected drives and use a best-effort default partition layout" msgstr "" -msgid "" -"Select what to do with each individual drive (followed by partition usage)" +msgid "Select what to do with each individual drive (followed by partition usage)" msgstr "" msgid "Select what you wish to do with the selected block devices" msgstr "" -msgid "" -"This is a list of pre-programmed profiles, they might make it easier to" -" install things like desktop environments" +msgid "This is a list of pre-programmed profiles, they might make it easier to install things like desktop environments" msgstr "" msgid "Select keyboard layout" @@ -200,19 +175,13 @@ msgstr "" msgid "Select one or more hard drives to use and configure" msgstr "" -msgid "" -"For the best compatibility with your AMD hardware, you may want to use either" -" the all open-source or AMD / ATI options." +msgid "For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options." msgstr "" -msgid "" -"For the best compatibility with your Intel hardware, you may want to use" -" either the all open-source or Intel options.\n" +msgid "For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n" msgstr "" -msgid "" -"For the best compatibility with your Nvidia hardware, you may want to use the" -" Nvidia proprietary driver.\n" +msgid "For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n" msgstr "" msgid "" @@ -242,9 +211,7 @@ msgstr "" msgid "Adding partition...." msgstr "" -msgid "" -"You need to enter a valid fs-type in order to continue. See `man parted` for" -" valid fs-type's." +msgid "You need to enter a valid fs-type in order to continue. See `man parted` for valid fs-type's." msgstr "" msgid "Error: Listing profiles on URL \"{}\" resulted in:" @@ -388,18 +355,14 @@ msgstr "" msgid "Password for user \"{}\": " msgstr "" -msgid "" -"Verifying that additional packages exist (this might take a few seconds)" +msgid "Verifying that additional packages exist (this might take a few seconds)" msgstr "" -msgid "" -"Would you like to use automatic time synchronization (NTP) with the default" -" time servers?\n" +msgid "Would you like to use automatic time synchronization (NTP) with the default time servers?\n" msgstr "" msgid "" -"Hardware time and other post-configuration steps might be required in order" -" for NTP to work.\n" +"Hardware time and other post-configuration steps might be required in order for NTP to work.\n" "For more information, please check the Arch wiki" msgstr "" @@ -411,8 +374,7 @@ msgstr "" msgid "" "\n" -" Choose an object from the list, and select one of the available actions for" -" it to execute" +" Choose an object from the list, and select one of the available actions for it to execute" msgstr "" msgid "Cancel" @@ -433,8 +395,9 @@ msgstr "" msgid "Delete" msgstr "" -msgid "Select an action for < {} >" -msgstr "" +#, fuzzy +msgid "Select an action for '{}'" +msgstr "حدِّد منطقة زمنية" msgid "Copy to new key:" msgstr "" @@ -447,13 +410,10 @@ msgid "" "This is your chosen configuration:" msgstr "" -msgid "" -"Pacman is already running, waiting maximum 10 minutes for it to terminate." +msgid "Pacman is already running, waiting maximum 10 minutes for it to terminate." msgstr "" -msgid "" -"Pre-existing pacman lock never exited. Please clean up any existing pacman" -" sessions before using archinstall." +msgid "Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall." msgstr "" msgid "Choose which optional additional repositories to enable" @@ -598,9 +558,7 @@ msgstr "" msgid "Would you like to create a separate partition for /home?" msgstr "" -msgid "" -"The selected drives do not have the minimum capacity required for an" -" automatic suggestion\n" +msgid "The selected drives do not have the minimum capacity required for an automatic suggestion\n" msgstr "" msgid "Minimum capacity for /home partition: {}GB\n" @@ -648,31 +606,22 @@ msgstr "" msgid "Mark/Unmark a partition as compressed (btrfs only)" msgstr "" -msgid "" -"The password you are using seems to be weak, are you sure you want to use it?" +msgid "The password you are using seems to be weak, are you sure you want to use it?" msgstr "" -msgid "" -"Provides a selection of desktop environments and tiling window managers, e.g." -" gnome, kde, sway" +msgid "Provides a selection of desktop environments and tiling window managers, e.g. gnome, kde, sway" msgstr "" msgid "Select your desired desktop environment" msgstr "" -msgid "" -"A very basic installation that allows you to customize Arch Linux as you see" -" fit." +msgid "A very basic installation that allows you to customize Arch Linux as you see fit." msgstr "" -msgid "" -"Provides a selection of various server packages to install and enable, e.g." -" httpd, nginx, mariadb" +msgid "Provides a selection of various server packages to install and enable, e.g. httpd, nginx, mariadb" msgstr "" -msgid "" -"Choose which servers to install, if none then a minimal installation wil be" -" done" +msgid "Choose which servers to install, if none then a minimal installation will be done" msgstr "" msgid "Installs a minimal system as well as xorg and graphics drivers." @@ -681,9 +630,7 @@ msgstr "" msgid "Press Enter to continue." msgstr "" -msgid "" -"Would you like to chroot into the newly created installation and perform" -" post-installation configuration?" +msgid "Would you like to chroot into the newly created installation and perform post-installation configuration?" msgstr "" msgid "Are you sure you want to reset this setting?" @@ -695,9 +642,7 @@ msgstr "" msgid "Any modifications to the existing setting will reset the disk layout!" msgstr "" -msgid "" -"If you reset the harddrive selection this will also reset the current disk" -" layout. Are you sure?" +msgid "If you reset the harddrive selection this will also reset the current disk layout. Are you sure?" msgstr "" msgid "Save and exit" @@ -740,9 +685,7 @@ msgstr "" msgid "Value: " msgstr "" -msgid "" -"You can skip selecting a drive and partitioning and use whatever drive-setup" -" is mounted at /mnt (experimental)" +msgid "You can skip selecting a drive and partitioning and use whatever drive-setup is mounted at /mnt (experimental)" msgstr "" msgid "Select one of the disks or skip and use /mnt as default" @@ -766,8 +709,7 @@ msgstr "" msgid "Bus-type" msgstr "" -msgid "" -"Either root-password or at least 1 user with sudo privileges must be specified" +msgid "Either root-password or at least 1 user with sudo privileges must be specified" msgstr "" msgid "Enter username (leave blank to skip): " @@ -781,3 +723,53 @@ msgstr "" msgid "Select which partitions to encrypt:" msgstr "" + +msgid "very weak" +msgstr "" + +msgid "weak" +msgstr "" + +msgid "moderate" +msgstr "" + +msgid "strong" +msgstr "" + +msgid "Add subvolume" +msgstr "" + +msgid "Edit subvolume" +msgstr "" + +msgid "Delete subvolume" +msgstr "" + +msgid "Configured {} interfaces" +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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot index 7a259336..b2be65f8 100644 --- a/archinstall/locales/base.pot +++ b/archinstall/locales/base.pot @@ -787,3 +787,39 @@ msgstr "" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid "" +" - Maximum value : {max_downloads} ( Allows {max_downloads} parallel " +"downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid "" +" - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a " +"time )" +msgstr "" + +msgid "" +" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 " +"download at a time )" +msgstr "" + +#, python-brace-format +msgid "" +"Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to " +"disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" diff --git a/archinstall/locales/cs/LC_MESSAGES/base.po b/archinstall/locales/cs/LC_MESSAGES/base.po index 7a2a197f..194bbf78 100644 --- a/archinstall/locales/cs/LC_MESSAGES/base.po +++ b/archinstall/locales/cs/LC_MESSAGES/base.po @@ -792,3 +792,29 @@ msgstr "Smazat uživatele" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" diff --git a/archinstall/locales/de/LC_MESSAGES/base.po b/archinstall/locales/de/LC_MESSAGES/base.po index a601326e..11fc821d 100644 --- a/archinstall/locales/de/LC_MESSAGES/base.po +++ b/archinstall/locales/de/LC_MESSAGES/base.po @@ -808,6 +808,32 @@ msgstr "Benutzerkonto löschen" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" + #~ msgid "Select disk layout" #~ msgstr "Laufwerke-layout auswählen" diff --git a/archinstall/locales/el/LC_MESSAGES/base.po b/archinstall/locales/el/LC_MESSAGES/base.po index 2561c9f2..9b342ac9 100644 --- a/archinstall/locales/el/LC_MESSAGES/base.po +++ b/archinstall/locales/el/LC_MESSAGES/base.po @@ -787,6 +787,9 @@ msgstr "Επεξεργασία υποόγκου" msgid "Delete subvolume" msgstr "Διαγραφή υποόγκου" +msgid "Configured {} interfaces" +msgstr "Διαμορφωμένες {} διεπαφές" + msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "Αυτή η επιλογή θέτει τον αριθμό των παράλληλων λήψεων που μπορούν να συμβούν κατά την εγκατάσταση" @@ -815,6 +818,3 @@ msgstr "Μη έγκυρη είσοδος! Προσπαθήστε ξανά με msgid "Parallel Downloads" msgstr "Παράλληλες Λήψεις" - -msgid "Configured {} interfaces" -msgstr "Διαμορφωμένες {} διεπαφές" diff --git a/archinstall/locales/en/LC_MESSAGES/base.po b/archinstall/locales/en/LC_MESSAGES/base.po index 1825d501..01d4414f 100644 --- a/archinstall/locales/en/LC_MESSAGES/base.po +++ b/archinstall/locales/en/LC_MESSAGES/base.po @@ -743,3 +743,29 @@ msgstr "" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" diff --git a/archinstall/locales/es/LC_MESSAGES/base.po b/archinstall/locales/es/LC_MESSAGES/base.po index 47a64b0a..6186b6d8 100644 --- a/archinstall/locales/es/LC_MESSAGES/base.po +++ b/archinstall/locales/es/LC_MESSAGES/base.po @@ -791,6 +791,32 @@ msgstr "Eliminar usuario" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" + #~ msgid "Select disk layout" #~ msgstr "Seleccione el diseño del disco" diff --git a/archinstall/locales/fr/LC_MESSAGES/base.po b/archinstall/locales/fr/LC_MESSAGES/base.po index 877b793f..2d7de615 100644 --- a/archinstall/locales/fr/LC_MESSAGES/base.po +++ b/archinstall/locales/fr/LC_MESSAGES/base.po @@ -797,6 +797,32 @@ msgstr "Supprimer l'utilisateur" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" + #~ msgid "Select disk layout" #~ msgstr "Sélectionner la disposition du disque" diff --git a/archinstall/locales/it/LC_MESSAGES/base.po b/archinstall/locales/it/LC_MESSAGES/base.po index baf55a28..893fe292 100644 --- a/archinstall/locales/it/LC_MESSAGES/base.po +++ b/archinstall/locales/it/LC_MESSAGES/base.po @@ -799,3 +799,29 @@ msgstr "Elimina utente" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" diff --git a/archinstall/locales/nl/LC_MESSAGES/base.po b/archinstall/locales/nl/LC_MESSAGES/base.po index 8553595e..aa7754d2 100644 --- a/archinstall/locales/nl/LC_MESSAGES/base.po +++ b/archinstall/locales/nl/LC_MESSAGES/base.po @@ -823,6 +823,32 @@ msgstr "Gebruiker verwijderen" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" + #~ msgid "Select disk layout" #~ msgstr "Kies een schijfindeling" diff --git a/archinstall/locales/pl/LC_MESSAGES/base.po b/archinstall/locales/pl/LC_MESSAGES/base.po index 1bd4b47e..c65126ea 100644 --- a/archinstall/locales/pl/LC_MESSAGES/base.po +++ b/archinstall/locales/pl/LC_MESSAGES/base.po @@ -805,6 +805,32 @@ msgstr "Usuń użytkownika" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" + #~ msgid "Select disk layout" #~ msgstr "Wybierz układ dysku" diff --git a/archinstall/locales/pt/LC_MESSAGES/base.po b/archinstall/locales/pt/LC_MESSAGES/base.po index 2e9b461c..98d261a9 100644 --- a/archinstall/locales/pt/LC_MESSAGES/base.po +++ b/archinstall/locales/pt/LC_MESSAGES/base.po @@ -843,6 +843,32 @@ msgstr "Eliminar Utilizador" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" + #~ msgid "Select disk layout" #~ msgstr "Seleciona o esquema de disco" diff --git a/archinstall/locales/pt_BR/LC_MESSAGES/base.po b/archinstall/locales/pt_BR/LC_MESSAGES/base.po index 69f8c902..3acbdb95 100644 --- a/archinstall/locales/pt_BR/LC_MESSAGES/base.po +++ b/archinstall/locales/pt_BR/LC_MESSAGES/base.po @@ -1,7 +1,6 @@ # Translators: # @Cain-dev (cain-dev.github.io) # Rafael Fontenelle - msgid "" msgstr "" "Last-Translator: Rafael Fontenelle \n" @@ -788,3 +787,29 @@ msgstr "Deletar subvolume" msgid "Configured {} interfaces" msgstr "{} interfaces configuradas" + +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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" diff --git a/archinstall/locales/ru/LC_MESSAGES/base.po b/archinstall/locales/ru/LC_MESSAGES/base.po index a88c58a1..8aeb57ff 100644 --- a/archinstall/locales/ru/LC_MESSAGES/base.po +++ b/archinstall/locales/ru/LC_MESSAGES/base.po @@ -791,6 +791,32 @@ msgstr "Удалить подтом" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" + #, 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 b3712972..590929c6 100644 --- a/archinstall/locales/sv/LC_MESSAGES/base.po +++ b/archinstall/locales/sv/LC_MESSAGES/base.po @@ -804,5 +804,31 @@ msgstr "Ta bort användare" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" + #~ msgid "Select disk layout" #~ msgstr "Välj hårddisk-layout" diff --git a/archinstall/locales/ta/LC_MESSAGES/base.po b/archinstall/locales/ta/LC_MESSAGES/base.po index 9cf261f2..3c1cd848 100644 --- a/archinstall/locales/ta/LC_MESSAGES/base.po +++ b/archinstall/locales/ta/LC_MESSAGES/base.po @@ -790,9 +790,6 @@ msgstr "துணைத்தொகுதியை நீக்கவும்" msgid "Configured {} interfaces" msgstr "கட்டமைக்கப்பட்ட {} இடைமுகங்கள்" -msgid "Parallel Downloads" -msgstr "இணையான பதிவிறக்கங்கள்" - msgid "This option enables the number of parallel downloads that can occur during installation" msgstr "இந்த விருப்பம் நிறுவலின் போது நிகழக்கூடிய இணையான பதிவிறக்கங்களின் எண்ணிக்கையை செயல்படுத்துகிறது" @@ -818,3 +815,6 @@ msgstr " - முடக்கு/இயல்புநிலை: 0 (இணை #, 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 வரை] மீண்டும் முயற்சிக்கவும்" + +msgid "Parallel Downloads" +msgstr "இணையான பதிவிறக்கங்கள்" diff --git a/archinstall/locales/tr/LC_MESSAGES/base.po b/archinstall/locales/tr/LC_MESSAGES/base.po index 4d978ee0..63f9dee0 100644 --- a/archinstall/locales/tr/LC_MESSAGES/base.po +++ b/archinstall/locales/tr/LC_MESSAGES/base.po @@ -803,3 +803,29 @@ msgstr "Kullanıcı Sil" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" diff --git a/archinstall/locales/ur/LC_MESSAGES/base.po b/archinstall/locales/ur/LC_MESSAGES/base.po index 2927c3ea..6494ae28 100644 --- a/archinstall/locales/ur/LC_MESSAGES/base.po +++ b/archinstall/locales/ur/LC_MESSAGES/base.po @@ -825,6 +825,32 @@ msgstr "صارف کو حذف کریں" msgid "Configured {} interfaces" 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" +"Note:" +msgstr "" + +msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" +msgstr "" + +msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" +msgstr "" + +msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" +msgstr "" + +#, python-brace-format +msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" +msgstr "" + +msgid "Parallel Downloads" +msgstr "" + #~ msgid "Select disk layout" #~ msgstr "ڈسک لے آؤٹ کو منتخب کریں" diff --git a/examples/swiss.py b/examples/swiss.py index 5d40dc68..da45cd18 100644 --- a/examples/swiss.py +++ b/examples/swiss.py @@ -164,7 +164,7 @@ class SetupMenu(archinstall.GeneralMenu): _('Archinstall language'), lambda x: self._select_archinstall_language(x), display_func=lambda x: x.display_name, - default=self.translation_handler.get_language('en'), + default=self.translation_handler.get_language_by_abbr('en'), enabled=True ) ) -- cgit v1.2.3-70-g09d2 From 53a2797af6ac0832bf7dd00dfe96b8ea1867db2e Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sun, 25 Sep 2022 12:05:25 +0200 Subject: Updating version to v2.5.1 --- archinstall/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/__init__.py') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 4e1e6d6d..44852d4c 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -51,7 +51,7 @@ from .lib.hsm import ( ) parser = ArgumentParser() -__version__ = "2.5.1rc1" +__version__ = "2.5.1" storage['__version__'] = __version__ # add the custome _ as a builtin, it can now be used anywhere in the -- cgit v1.2.3-70-g09d2