diff --git a/archinstall/__init__.py b/archinstall/__init__.py index c8a55e7f..6f5d3825 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -1,4 +1,5 @@ """Arch Linux installer - guided, templates etc.""" + import curses import importlib import os @@ -9,29 +10,13 @@ from argparse import ArgumentParser, Namespace from pathlib import Path from typing import TYPE_CHECKING, Any -from . import default_profiles -from .lib import disk, exceptions, interactions, locale, luks, mirrors, models, networking, packages, profile -from .lib.boot import Boot -from .lib.configuration import ConfigurationOutput -from .lib.general import ( - JSON, - UNSAFE_JSON, - SysCommand, - SysCommandWorker, - clear_vt100_escape_codes, - generate_password, - json_stream_to_structure, - locate_binary, - run_custom_user_commands, - secret, -) -from .lib.global_menu import GlobalMenu -from .lib.hardware import GfxDriver, SysInfo -from .lib.installer import Installer, accessibility_tools_in_use +from archinstall.lib.args import arch_config_handler +from archinstall.lib.disk.utils import disk_layouts + +from .lib.hardware import SysInfo from .lib.output import FormattedOutput, debug, error, info, log, warn from .lib.pacman import Pacman from .lib.plugins import load_plugin, plugins -from .lib.storage import storage from .lib.translationhandler import DeferredTranslation, Language, translation_handler from .tui import Tui @@ -41,9 +26,6 @@ if TYPE_CHECKING: _: Callable[[str], DeferredTranslation] -__version__ = "3.0.2" -storage['__version__'] = __version__ - # add the custom _ as a builtin, it can now be used anywhere in the # project to mark strings as translatable with _('translate me') DeferredTranslation.install() @@ -56,236 +38,18 @@ debug(f"Virtualization detected: {SysInfo.virtualization()}; is VM: {SysInfo.is_ debug(f"Graphics devices detected: {SysInfo._graphics_devices().keys()}") # For support reasons, we'll log the disk layout pre installation to match against post-installation layout -debug(f"Disk states before installing:\n{disk.disk_layouts()}") - -parser = ArgumentParser() - - -def define_arguments() -> None: - """ - Define which explicit arguments do we allow. - Refer to https://docs.python.org/3/library/argparse.html for documentation and - https://docs.python.org/3/howto/argparse.html for a tutorial - 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("--silent", action="store_true", - help="WARNING: Disables all prompts for input and confirmation. If no configuration is provided, this is ignored") - parser.add_argument("--dry-run", "--dry_run", action="store_true", - help="Generates a configuration file and then exits instead of performing an installation") - parser.add_argument("--script", default="guided", nargs="?", help="Script to run for installation", type=str) - parser.add_argument("--mount-point", "--mount_point", default=Path("/mnt/archinstall"), nargs="?", type=Path, - help="Define an alternate mount point for installation") - parser.add_argument("--skip-ntp", action="store_true", help="Disables NTP checks during installation", default=False) - parser.add_argument("--debug", action="store_true", default=False, help="Adds debug info into the log") - parser.add_argument("--offline", action="store_true", default=False, - help="Disabled online upstream services such as package search and key-ring auto update.") - 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) - parser.add_argument("--skip-version-check", action="store_true", - help="Skip the version check when running archinstall") +debug(f"Disk states before installing:\n{disk_layouts()}") if 'sphinx' not in sys.modules and 'pylint' not in sys.modules: if '--help' in sys.argv or '-h' in sys.argv: - define_arguments() - parser.print_help() + arch_config_handler.print_help() exit(0) if os.getuid() != 0: print(_("Archinstall requires root privileges to run. See --help for more.")) exit(1) -def parse_unspecified_argument_list(unknowns: list, multiple: bool = False, err: bool = False) -> dict: # type: ignore[type-arg] - """We accept arguments not defined to the parser. (arguments "ad hoc"). - Internally argparse return to us a list of words so we have to parse its contents, manually. - We accept following individual syntax for each argument - --argument value - --argument=value - --argument = value - --argument (boolean as default) - 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. - - To a certain extent, multiple and error are incompatible. In fact, the only error this routine can catch, as of now, - is the event argument value value ... - which isn't am error if multiple is specified - """ - tmp_list = [arg for arg in unknowns if arg != "="] # wastes a few bytes, but avoids any collateral effect of the destructive nature of the pop method() - config = {} - key = None - last_key = None - while tmp_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)] - config[key] = value - last_key = key # for multiple handling - key = None # we have the kwy value pair we need - else: - key = element[2:] - config[key] = True # every argument starts its lifecycle as boolean - elif key: - config[key] = element - last_key = key # multiple - key = None - elif multiple and last_key: - if isinstance(config[last_key], str): - config[last_key] = [config[last_key], element] - else: - config[last_key].append(element) - elif err: - raise ValueError(f"Entry {element} is not related to any argument") - else: - print(f" We ignore the entry {element} as it isn't related to any argument") - return config - - -def cleanup_empty_args(args: Namespace | dict) -> dict: # type: ignore[type-arg] - """ - Takes arguments (dictionary or argparse Namespace) and removes any - None values. This ensures clean mergers during dict.update(args) - """ - if type(args) is Namespace: - args = vars(args) - - clean_args = {} - for key, val in args.items(): - if isinstance(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: - 0) we create a dict to store the arguments and their values - 1) preprocess. - We take those arguments which use JSON files, and read them into the argument dict. So each first level entry - becomes an argument on its own right - 2) Load. - We convert the predefined argument list directly into the dict via the vars() function. Non specified arguments - are loaded with value None or false if they are booleans (action="store_true"). The name is chosen according to - argparse conventions. See above (the first text is used as argument name, but underscore substitutes dash). We - then load all the undefined arguments. In this case the names are taken as written. - Important. This way explicit command line arguments take precedence over configuration files. - 3) Amend - Change whatever is needed on the configuration dictionary (it could be done in post_process_arguments but this - ougth to be left to changes anywhere else in the code, not in the arguments dictionary - """ - config: dict[str, Any] = {} - args, unknowns = parser.parse_known_args() - # preprocess the JSON files. - # TODO Expand the url access to the other JSON file arguments ? - if args.config is not None: - if not json_stream_to_structure('--config', args.config, config): - exit(1) - - if args.creds is not None: - if not json_stream_to_structure('--creds', args.creds, config): - exit(1) - - # load the parameters. first the known, then the unknowns - clean_args = cleanup_empty_args(args) - config.update(clean_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 clean_args.get('config') is None: - config["silent"] = False - else: - config["silent"] = clean_args.get('silent') - - # avoiding a compatibility issue - if 'dry-run' in config: - del config['dry-run'] - - return config - - -def load_config() -> None: - """ - refine and set some arguments. Formerly at the scripts - """ - from .lib.models import NetworkConfiguration - - arguments['locale_config'] = locale.LocaleConfiguration.parse_arg(arguments) - - if (archinstall_lang := arguments.get('archinstall-language', None)) is not None: - arguments['archinstall-language'] = translation_handler.get_language_by_name(archinstall_lang) - - if disk_config := arguments.get('disk_config', {}): - arguments['disk_config'] = disk.DiskLayoutConfiguration.parse_arg(disk_config) - - if profile_config := arguments.get('profile_config', None): - arguments['profile_config'] = profile.ProfileConfiguration.parse_arg(profile_config) - - if mirror_config := arguments.get('mirror_config', None): - arguments['mirror_config'] = mirrors.MirrorConfiguration.parse_args(mirror_config) - - if arguments.get('servers', None) is not None: - storage['_selected_servers'] = arguments.get('servers', None) - - if (net_config := arguments.get('network_config', None)) is not None: - config = NetworkConfiguration.parse_arg(net_config) - arguments['network_config'] = config - - if arguments.get('!users', None) is not None or arguments.get('!superusers', None) is not None: - users = arguments.get('!users', None) - superusers = arguments.get('!superusers', None) - arguments['!users'] = models.User.parse_arguments(users, superusers) - - if arguments.get('bootloader', None) is not None: - arguments['bootloader'] = models.Bootloader.from_arg(arguments['bootloader']) - - if arguments.get('uki') and not arguments['bootloader'].has_uki_support(): - arguments['uki'] = False - - if arguments.get('audio_config', None) is not None: - arguments['audio_config'] = models.AudioConfiguration.parse_arg(arguments['audio_config']) - - if arguments.get('disk_encryption', None) is not None and disk_config is not None: - arguments['disk_encryption'] = disk.DiskEncryption.parse_arg( - arguments['disk_config'], - arguments['disk_encryption'], - arguments.get('encryption_password', '') - ) - - -def post_process_arguments(args: dict[str, Any]) -> None: - storage['arguments'] = args - - if args.get('debug'): - warn(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!") - - if args.get('plugin'): - path = args['plugin'] - load_plugin(path) - - try: - load_config() - except ValueError as err: - warn(str(err)) - exit(1) - - -define_arguments() -arguments: dict[str, Any] = get_arguments() -post_process_arguments(arguments) - - # @archinstall.plugin decorator hook to programmatically add # plugins in runtime. Useful in profiles_bck and other things. def plugin(f, *args, **kwargs) -> None: # type: ignore[no-untyped-def] @@ -321,13 +85,10 @@ def main() -> None: OR straight as a module: python -m archinstall In any case we will be attempting to load the provided script to be run from the scripts/ folder """ - if not arguments.get('skip_version_check'): + if not arch_config_handler.args.skip_version_check: _check_new_version() - script = arguments.get('script', None) - - if script is None: - print('No script to run provided') + script = arch_config_handler.args.script mod_name = f'archinstall.scripts.{script}' # by loading the module we'll automatically run the script diff --git a/archinstall/default_profiles/applications/pipewire.py b/archinstall/default_profiles/applications/pipewire.py index 72b54862..6f02995b 100644 --- a/archinstall/default_profiles/applications/pipewire.py +++ b/archinstall/default_profiles/applications/pipewire.py @@ -1,6 +1,5 @@ from typing import TYPE_CHECKING, override -import archinstall from archinstall.default_profiles.profile import Profile, ProfileType if TYPE_CHECKING: @@ -26,14 +25,12 @@ class PipewireProfile(Profile): ] def _enable_pipewire_for_all(self, install_session: 'Installer') -> None: - users: User | list[User] | None = archinstall.arguments.get('!users', None) + from archinstall.lib.args import arch_config_handler + users: list[User] | None = arch_config_handler.config.users if users is None: return - if not isinstance(users, list): - users = [users] - for user in users: # Create the full path for enabling the pipewire systemd items service_dir = install_session.target / "home" / user.username / ".config" / "systemd" / "user" / "default.target.wants" diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index c9a492f9..897a1b5d 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -4,8 +4,6 @@ import sys from enum import Enum, auto from typing import TYPE_CHECKING -from ..lib.storage import storage - if TYPE_CHECKING: from collections.abc import Callable @@ -110,7 +108,8 @@ class Profile: Used to control if the Profile() should be visible or not in different contexts. Returns True if --advanced is given on a Profile(advanced=True) instance. """ - return self.advanced is False or storage['arguments'].get('advanced', False) is True + from archinstall.lib.args import arch_config_handler + return self.advanced is False or arch_config_handler.args.advanced is True def install(self, install_session: 'Installer') -> None: """ diff --git a/archinstall/default_profiles/servers/docker.py b/archinstall/default_profiles/servers/docker.py index e4937277..82486380 100644 --- a/archinstall/default_profiles/servers/docker.py +++ b/archinstall/default_profiles/servers/docker.py @@ -1,8 +1,7 @@ from typing import TYPE_CHECKING, override -import archinstall from archinstall.default_profiles.profile import Profile, ProfileType -from archinstall.lib.models import User +from archinstall.lib.args import arch_config_handler if TYPE_CHECKING: from archinstall.lib.installer import Installer @@ -27,9 +26,5 @@ class DockerProfile(Profile): @override def post_install(self, install_session: 'Installer') -> None: - users: User | list[User] = archinstall.arguments.get('!users', []) - if not isinstance(users, list): - users = [users] - - for user in users: + for user in arch_config_handler.config.users: install_session.arch_chroot(f'usermod -a -G docker {user.username}') diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index 0bfb3208..1c53e2ea 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -4,21 +4,22 @@ import urllib.error import urllib.parse from argparse import ArgumentParser, Namespace from dataclasses import dataclass, field +from importlib.metadata import version from pathlib import Path from typing import Any from urllib.request import Request, urlopen from pydantic.dataclasses import dataclass as p_dataclass -from .disk import DiskEncryption, DiskLayoutConfiguration -from .locale import LocaleConfiguration -from .mirrors import MirrorConfiguration -from .models import AudioConfiguration, Bootloader, NetworkConfiguration, User -from .output import error, warn -from .plugins import load_plugin -from .profile import ProfileConfiguration -from .storage import storage -from .translationhandler import Language, translation_handler +from archinstall.lib.mirrors import MirrorConfiguration +from archinstall.lib.models import AudioConfiguration, Bootloader, NetworkConfiguration, User +from archinstall.lib.models.device_model import DiskEncryption, DiskLayoutConfiguration +from archinstall.lib.models.locale import LocaleConfiguration +from archinstall.lib.models.profile_model import ProfileConfiguration +from archinstall.lib.output import error, warn +from archinstall.lib.plugins import load_plugin +from archinstall.lib.storage import storage +from archinstall.lib.translationhandler import Language, translation_handler @p_dataclass @@ -30,7 +31,7 @@ class Arguments: silent: bool = False dry_run: bool = False script: str = 'guided' - mount_point: Path | None = Path('/mnt') + mountpoint: Path = Path('/mnt') skip_ntp: bool = False debug: bool = False offline: bool = False @@ -38,11 +39,12 @@ class Arguments: plugin: str | None = None skip_version_check: bool = False advanced: bool = False + verbose: bool = False @dataclass class ArchConfig: - version: str = field(default_factory=lambda: storage['__version__']) + version: str = field(default_factory=lambda: version('archinstall')) locale_config: LocaleConfiguration | None = None archinstall_language: Language = field(default_factory=lambda: translation_handler.get_language_by_abbr('en')) disk_config: DiskLayoutConfiguration | None = None @@ -60,10 +62,62 @@ class ArchConfig: swap: bool = True timezone: str = 'UTC' additional_repositories: list[str] = field(default_factory=list) + services: list[str] = field(default_factory=list) + custom_commands: list[str] = field(default_factory=list) # Special fields that should be handle with care due to security implications - _users: list[User] = field(default_factory=list) - _disk_encryption: DiskEncryption | None = None + users: list[User] = field(default_factory=list) + disk_encryption: DiskEncryption | None = None + root_password: str | None = None + + def unsafe_json(self) -> dict[str, Any]: + config = { + '!users': [user.json() for user in self.users], + '!root-password': self.root_password, + } + + if self.disk_encryption: + config['encryption_password'] = self.disk_encryption.encryption_password + + return config + + def safe_json(self) -> dict[str, Any]: + config = { + 'version': self.version, + 'archinstall-language': self.archinstall_language.json(), + 'hostname': self.hostname, + 'kernels': self.kernels, + 'ntp': self.ntp, + 'packages': self.packages, + 'parallel_downloads': self.parallel_downloads, + 'swap': self.swap, + 'timezone': self.timezone, + 'additional-repositories': self.additional_repositories, + 'services': self.services, + 'custom_commands': self.custom_commands, + 'bootloader': self.bootloader.json(), + 'audio_config': self.audio_config.json() if self.audio_config else None, + } + + if self.locale_config: + config['locale_config'] = self.locale_config.json() + + if self.disk_config: + config['disk_config'] = self.disk_config.json() + + if self.disk_encryption: + config['disk_encryption'] = self.disk_encryption.json() + + if self.profile_config: + config['profile_config'] = self.profile_config.json() + + if self.mirror_config: + config['mirror_config'] = self.mirror_config.json() + + if self.network_config: + config['network_config'] = self.network_config.json() + + return config @classmethod def from_config(cls, args_config: dict[str, Any]) -> 'ArchConfig': @@ -89,7 +143,7 @@ class ArchConfig: users = args_config.get('!users', None) superusers = args_config.get('!superusers', None) if users is not None or superusers is not None: - arch_config._users = User.parse_arguments(users, superusers) + arch_config.users = User.parse_arguments(users, superusers) if bootloader_config := args_config.get('bootloader', None): arch_config.bootloader = Bootloader.from_arg(bootloader_config) @@ -101,7 +155,7 @@ class ArchConfig: arch_config.audio_config = AudioConfiguration.parse_arg(audio_config) if args_config.get('disk_encryption', None) is not None and arch_config.disk_config is not None: - arch_config._disk_encryption = DiskEncryption.parse_arg( + arch_config.disk_encryption = DiskEncryption.parse_arg( arch_config.disk_config, args_config['disk_encryption'], args_config.get('encryption_password', '') @@ -130,6 +184,15 @@ class ArchConfig: if additional_repositories := args_config.get('additional-repositories', []): arch_config.additional_repositories = additional_repositories + if services := args_config.get('services', []): + arch_config.services = services + + if root_password := args_config.get('!root-password', None): + arch_config.root_password = root_password + + if custom_commands := args_config.get('custom_commands', []): + arch_config.custom_commands = custom_commands + return arch_config @@ -139,11 +202,11 @@ class ArchConfigHandler: self._args: Arguments = self._parse_args() config = self._parse_config() - self._arch_config = ArchConfig.from_config(config) + self._config = ArchConfig.from_config(config) @property - def arch_config(self) -> ArchConfig: - return self._arch_config + def config(self) -> ArchConfig: + return self._config @property def args(self) -> Arguments: @@ -159,7 +222,7 @@ class ArchConfigHandler: "--version", action="version", default=False, - version="%(prog)s " + storage['__version__'] + version="%(prog)s " + version('archinstall') ) parser.add_argument( "--config", @@ -260,6 +323,12 @@ class ArchConfigHandler: default=False, help="Enabled advanced options" ) + parser.add_argument( + "--verbose", + action="store_true", + default=False, + help="Enabled verbose options" + ) return parser @@ -336,3 +405,6 @@ class ArchConfigHandler: clean_args[key] = val return clean_args + + +arch_config_handler: ArchConfigHandler = ArchConfigHandler() diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index e1aab325..3ea90c00 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -2,10 +2,11 @@ import json import readline import stat from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, Orientation, PreviewStyle, ResultType, SelectMenu, Tui +from .args import ArchConfig from .general import JSON, UNSAFE_JSON from .output import debug, warn from .storage import storage @@ -20,58 +21,36 @@ if TYPE_CHECKING: class ConfigurationOutput: - def __init__(self, config: dict[str, Any]): + def __init__(self, config: ArchConfig): """ - Configuration output handler to parse the existing configuration data structure and prepare for output on the + Configuration output handler to parse the existing + configuration data structure and prepare for output on the console and for saving it to configuration files - :param config: A dictionary containing configurations (basically archinstall.arguments) - :type config: dict + :param config: Archinstall configuration object + :type config: ArchConfig """ + self._config = config - self._user_credentials: dict[str, Any] = {} - self._user_config: dict[str, Any] = {} self._default_save_path = storage.get('LOG_PATH', Path('.')) - self._user_config_file = 'user_configuration.json' - self._user_creds_file = "user_credentials.json" - - self._sensitive = ['!users', '!root-password'] - self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run'] - - self._process_config() + self._user_config_file = Path('user_configuration.json') + self._user_creds_file = Path('user_credentials.json') @property - def user_credentials_file(self) -> str: - return self._user_creds_file - - @property - def user_configuration_file(self) -> str: + def user_configuration_file(self) -> Path: return self._user_config_file - def _process_config(self) -> None: - for key, value in self._config.items(): - if key in self._sensitive: - self._user_credentials[key] = value - elif key in self._ignore: - pass - else: - self._user_config[key] = value - - # special handling for encryption password - if key == 'disk_encryption' and value: - self._user_credentials['encryption_password'] = value.encryption_password + @property + def user_credentials_file(self) -> Path: + return self._user_creds_file def user_config_to_json(self) -> str: - return json.dumps({ - 'config_version': storage['__version__'], # Tells us what version was used to generate the config - **self._user_config, # __version__ will be overwritten by old version definition found in config - 'version': storage['__version__'] - }, indent=4, sort_keys=True, cls=JSON) + out = self._config.safe_json() + return json.dumps(out, indent=4, sort_keys=True, cls=JSON) - def user_credentials_to_json(self) -> str | None: - if self._user_credentials: - return json.dumps(self._user_credentials, indent=4, sort_keys=True, cls=UNSAFE_JSON) - return None + def user_credentials_to_json(self) -> str: + out = self._config.unsafe_json() + return json.dumps(out, indent=4, sort_keys=True, cls=UNSAFE_JSON) def write_debug(self) -> None: debug(" -- Chosen configuration --") @@ -120,10 +99,9 @@ class ConfigurationOutput: def save_user_creds(self, dest_path: Path) -> None: if self._is_valid_path(dest_path): - if user_creds := self.user_credentials_to_json(): - target = dest_path / self._user_creds_file - target.write_text(user_creds) - target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) + target = dest_path / self._user_creds_file + target.write_text(self.user_credentials_to_json()) + target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) def save(self, dest_path: Path | None = None) -> None: save_path = dest_path or self._default_save_path @@ -133,7 +111,7 @@ class ConfigurationOutput: self.save_user_creds(save_path) -def save_config(config: dict[str, Any]) -> None: +def save_config(config: ArchConfig) -> None: def preview(item: MenuItem) -> str | None: match item.value: case "user_config": @@ -144,9 +122,9 @@ def save_config(config: dict[str, Any]) -> None: return f"{config_output.user_credentials_file}\n{maybe_serial}" return str(_("No configuration")) case "all": - output = [config_output.user_configuration_file] - if config_output.user_credentials_to_json(): - output.append(config_output.user_credentials_file) + output = [str(config_output.user_configuration_file)] + config_output.user_credentials_to_json() + output.append(str(config_output.user_credentials_file)) return '\n'.join(output) return None diff --git a/archinstall/lib/disk/__init__.py b/archinstall/lib/disk/__init__.py index 9e079756..e69de29b 100644 --- a/archinstall/lib/disk/__init__.py +++ b/archinstall/lib/disk/__init__.py @@ -1,43 +0,0 @@ -from .device_handler import device_handler, disk_layouts -from .device_model import ( - BDevice, - DeviceGeometry, - DeviceModification, - DiskEncryption, - DiskLayoutConfiguration, - DiskLayoutType, - EncryptionType, - Fido2Device, - FilesystemType, - LsblkInfo, - LvmConfiguration, - LvmLayoutType, - LvmVolume, - LvmVolumeGroup, - LvmVolumeStatus, - ModificationStatus, - PartitionFlag, - PartitionModification, - PartitionTable, - PartitionType, - SectorSize, - Size, - SubvolumeModification, - Unit, - _DeviceInfo, - get_all_lsblk_info, - get_lsblk_by_mountpoint, - get_lsblk_info, -) -from .disk_menu import DiskLayoutConfigurationMenu -from .encryption_menu import ( - DiskEncryptionMenu, - select_encrypted_password, - select_encryption_type, - select_hsm, - select_partitions_to_encrypt, -) -from .fido import Fido2 -from .filesystem import FilesystemHandler -from .partitioning_menu import PartitioningList, manual_partitioning -from .subvolume_menu import SubvolumeMenu diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index f45c27b7..c099859b 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -13,9 +13,7 @@ from parted import Device, Disk, DiskException, FileSystem, Geometry, IOExceptio from ..exceptions import DiskError, UnknownFilesystemFormat from ..general import SysCallError, SysCommand, SysCommandWorker from ..luks import Luks2 -from ..output import debug, error, info, log, warn -from ..utils.util import is_subpath -from .device_model import ( +from ..models.device_model import ( BDevice, BtrfsMountOption, DeviceModification, @@ -39,10 +37,13 @@ from .device_model import ( _BtrfsSubvolumeInfo, _DeviceInfo, _PartitionInfo, +) +from ..output import debug, error, info, log +from ..utils.util import is_subpath +from .utils import ( find_lsblk_info, get_all_lsblk_info, get_lsblk_info, - get_lsblk_output, ) @@ -866,13 +867,3 @@ class DeviceHandler: device_handler = DeviceHandler() - - -def disk_layouts() -> str: - try: - lsblk_output = get_lsblk_output() - except SysCallError as err: - warn(f"Could not return disk layouts: {err}") - return '' - - return lsblk_output.model_dump_json(indent=4) diff --git a/archinstall/lib/disk/disk_menu.py b/archinstall/lib/disk/disk_menu.py index 9e91e3ce..fa227ef1 100644 --- a/archinstall/lib/disk/disk_menu.py +++ b/archinstall/lib/disk/disk_menu.py @@ -1,13 +1,13 @@ -from typing import TYPE_CHECKING, Any, override +from dataclasses import dataclass +from typing import TYPE_CHECKING, override +from archinstall.lib.models.device_model import DiskLayoutConfiguration, DiskLayoutType, LvmConfiguration from archinstall.tui import MenuItem, MenuItemGroup from ..interactions import select_disk_config from ..interactions.disk_conf import select_lvm_config from ..menu import AbstractSubMenu from ..output import FormattedOutput -from . import DiskLayoutConfiguration, DiskLayoutType -from .device_model import LvmConfiguration if TYPE_CHECKING: from collections.abc import Callable @@ -17,34 +17,44 @@ if TYPE_CHECKING: _: Callable[[str], DeferredTranslation] +@dataclass +class DiskMenuConfig: + disk_config: DiskLayoutConfiguration | None + lvm_config: LvmConfiguration | None + + class DiskLayoutConfigurationMenu(AbstractSubMenu): - def __init__( - self, - disk_layout_config: DiskLayoutConfiguration | None, - advanced: bool = False - ): - self._disk_layout_config = disk_layout_config - self._advanced = advanced - self._data_store: dict[str, Any] = {} + def __init__(self, disk_layout_config: DiskLayoutConfiguration | None): + if not disk_layout_config: + self._disk_menu_config = DiskMenuConfig(disk_config=None, lvm_config=None) + else: + self._disk_menu_config = DiskMenuConfig( + disk_config=disk_layout_config, + lvm_config=disk_layout_config.lvm_config + ) menu_optioons = self._define_menu_options() self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True) - super().__init__(self._item_group, data_store=self._data_store, allow_reset=True) + super().__init__( + self._item_group, + self._disk_menu_config, + allow_reset=True + ) def _define_menu_options(self) -> list[MenuItem]: return [ MenuItem( text=str(_('Partitioning')), action=self._select_disk_layout_config, - value=self._disk_layout_config, + value=self._disk_menu_config.disk_config, preview_action=self._prev_disk_layouts, key='disk_config' ), MenuItem( text='LVM (BETA)', action=self._select_lvm_config, - value=self._disk_layout_config.lvm_config if self._disk_layout_config else None, + value=self._disk_menu_config.lvm_config, preview_action=self._prev_lvm_config, dependencies=[self._check_dep_lvm], key='lvm_config' @@ -55,12 +65,11 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu): def run(self) -> DiskLayoutConfiguration | None: super().run() - disk_layout_config: DiskLayoutConfiguration | None = self._data_store.get('disk_config', None) + if self._disk_menu_config.disk_config: + self._disk_menu_config.disk_config.lvm_config = self._disk_menu_config.lvm_config + return self._disk_menu_config.disk_config - if disk_layout_config: - disk_layout_config.lvm_config = self._data_store.get('lvm_config', None) - - return disk_layout_config + return None def _check_dep_lvm(self) -> bool: disk_layout_conf: DiskLayoutConfiguration | None = self._menu_item_group.find_by_key('disk_config').value @@ -74,7 +83,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu): self, preset: DiskLayoutConfiguration | None ) -> DiskLayoutConfiguration | None: - disk_config = select_disk_config(preset, advanced_option=self._advanced) + disk_config = select_disk_config(preset) if disk_config != preset: self._menu_item_group.find_by_key('lvm_config').value = None diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 0f602795..cac0650f 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -1,14 +1,21 @@ from pathlib import Path -from typing import TYPE_CHECKING, Any, override +from typing import TYPE_CHECKING, override from archinstall.lib.menu.menu_helper import MenuHelper +from archinstall.lib.models.device_model import ( + DeviceModification, + DiskEncryption, + DiskLayoutConfiguration, + EncryptionType, + LvmConfiguration, + LvmVolume, + PartitionModification, +) from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, ResultType, SelectMenu -from ..disk import DeviceModification, DiskEncryption, DiskLayoutConfiguration, EncryptionType, PartitionModification from ..menu import AbstractSubMenu from ..output import FormattedOutput from ..utils.util import get_password -from . import LvmConfiguration, LvmVolume from .fido import Fido2, Fido2Device if TYPE_CHECKING: @@ -26,31 +33,34 @@ class DiskEncryptionMenu(AbstractSubMenu): preset: DiskEncryption | None = None ): if preset: - self._preset = preset + self._enc_config = preset else: - self._preset = DiskEncryption() + self._enc_config = DiskEncryption() - self._data_store: dict[str, Any] = {} self._disk_config = disk_config menu_optioons = self._define_menu_options() self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True) - super().__init__(self._item_group, data_store=self._data_store, allow_reset=True) + super().__init__( + self._item_group, + self._enc_config, + allow_reset=True + ) def _define_menu_options(self) -> list[MenuItem]: return [ MenuItem( text=str(_('Encryption type')), action=lambda x: select_encryption_type(self._disk_config, x), - value=self._preset.encryption_type, + value=self._enc_config.encryption_type, preview_action=self._preview, key='encryption_type' ), MenuItem( text=str(_('Encryption password')), action=lambda x: select_encrypted_password(), - value=self._preset.encryption_password, + value=self._enc_config.encryption_password, dependencies=[self._check_dep_enc_type], preview_action=self._preview, key='encryption_password' @@ -58,7 +68,7 @@ class DiskEncryptionMenu(AbstractSubMenu): MenuItem( text=str(_('Partitions')), action=lambda x: select_partitions_to_encrypt(self._disk_config.device_modifications, x), - value=self._preset.partitions, + value=self._enc_config.partitions, dependencies=[self._check_dep_partitions], preview_action=self._preview, key='partitions' @@ -66,18 +76,18 @@ class DiskEncryptionMenu(AbstractSubMenu): MenuItem( text=str(_('LVM volumes')), action=self._select_lvm_vols, - value=self._preset.lvm_volumes, + value=self._enc_config.lvm_volumes, dependencies=[self._check_dep_lvm_vols], preview_action=self._preview, - key='lvm_vols' + key='lvm_volumes' ), MenuItem( text=str(_('HSM')), action=select_hsm, - value=self._preset.hsm_device, + value=self._enc_config.hsm_device, dependencies=[self._check_dep_enc_type], preview_action=self._preview, - key='HSM' + key='hsm_device' ), ] @@ -111,7 +121,7 @@ class DiskEncryptionMenu(AbstractSubMenu): enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value enc_password: str | None = self._item_group.find_by_key('encryption_password').value enc_partitions = self._item_group.find_by_key('partitions').value - enc_lvm_vols = self._item_group.find_by_key('lvm_vols').value + enc_lvm_vols = self._item_group.find_by_key('lvm_volumes').value assert enc_type is not None assert enc_partitions is not None @@ -129,7 +139,7 @@ class DiskEncryptionMenu(AbstractSubMenu): encryption_type=enc_type, partitions=enc_partitions, lvm_volumes=enc_lvm_vols, - hsm_device=self._data_store.get('HSM', None) + hsm_device=self._enc_config.hsm_device ) return None @@ -186,7 +196,7 @@ class DiskEncryptionMenu(AbstractSubMenu): return None def _prev_lvm_vols(self) -> str | None: - volumes: list[PartitionModification] | None = self._item_group.find_by_key('lvm_vols').value + volumes: list[PartitionModification] | None = self._item_group.find_by_key('lvm_volumes').value if volumes: output = str(_('LVM volumes to be encrypted')) + '\n' @@ -196,7 +206,7 @@ class DiskEncryptionMenu(AbstractSubMenu): return None def _prev_hsm(self) -> str | None: - fido_device: Fido2Device | None = self._item_group.find_by_key('HSM').value + fido_device: Fido2Device | None = self._item_group.find_by_key('hsm_device').value if not fido_device: return None diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 53d59597..b5faefef 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -4,10 +4,11 @@ import getpass from pathlib import Path from typing import ClassVar +from archinstall.lib.models.device_model import Fido2Device + from ..exceptions import SysCallError from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes from ..output import error, info -from .device_model import Fido2Device class Fido2: diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 21de2915..980c0378 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -9,9 +9,7 @@ from archinstall.tui import Tui from ..hardware import SysInfo from ..interactions.general_conf import ask_abort from ..luks import Luks2 -from ..output import debug, info -from .device_handler import device_handler -from .device_model import ( +from ..models.device_model import ( DiskEncryption, DiskLayoutConfiguration, DiskLayoutType, @@ -26,6 +24,8 @@ from .device_model import ( Size, Unit, ) +from ..output import debug, info +from .device_handler import device_handler if TYPE_CHECKING: from collections.abc import Callable diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 08ece3e8..59eb4913 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -4,13 +4,7 @@ import re from pathlib import Path from typing import TYPE_CHECKING, override -from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, Orientation, ResultType, SelectMenu - -from ..hardware import SysInfo -from ..menu import ListManager -from ..output import FormattedOutput -from ..utils.util import prompt_dir -from .device_model import ( +from archinstall.lib.models.device_model import ( BDevice, BtrfsMountOption, FilesystemType, @@ -22,6 +16,12 @@ from .device_model import ( Size, Unit, ) +from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, Orientation, ResultType, SelectMenu + +from ..hardware import SysInfo +from ..menu import ListManager +from ..output import FormattedOutput +from ..utils.util import prompt_dir from .subvolume_menu import SubvolumeMenu if TYPE_CHECKING: diff --git a/archinstall/lib/disk/subvolume_menu.py b/archinstall/lib/disk/subvolume_menu.py index 4978ac91..f21a04a6 100644 --- a/archinstall/lib/disk/subvolume_menu.py +++ b/archinstall/lib/disk/subvolume_menu.py @@ -1,11 +1,11 @@ from pathlib import Path from typing import TYPE_CHECKING, override +from archinstall.lib.models.device_model import SubvolumeModification from archinstall.tui import Alignment, EditMenu, ResultType from ..menu import ListManager from ..utils.util import prompt_dir -from .device_model import SubvolumeModification if TYPE_CHECKING: from collections.abc import Callable diff --git a/archinstall/lib/disk/utils.py b/archinstall/lib/disk/utils.py new file mode 100644 index 00000000..5d25c2f8 --- /dev/null +++ b/archinstall/lib/disk/utils.py @@ -0,0 +1,111 @@ +from pathlib import Path + +from pydantic import BaseModel + +from archinstall.lib.exceptions import DiskError, SysCallError +from archinstall.lib.general import SysCommand +from archinstall.lib.models.device_model import LsblkInfo +from archinstall.lib.output import debug, warn + + +class LsblkOutput(BaseModel): + blockdevices: list[LsblkInfo] + + +def _fetch_lsblk_info( + dev_path: Path | str | None = None, + reverse: bool = False, + full_dev_path: bool = False +) -> LsblkOutput: + cmd = ['lsblk', '--json', '--bytes', '--output', ','.join(LsblkInfo.fields())] + + if reverse: + cmd.append('--inverse') + + if full_dev_path: + cmd.append('--paths') + + if dev_path: + cmd.append(str(dev_path)) + + try: + worker = SysCommand(cmd) + except SysCallError as err: + # Get the output minus the message/info from lsblk if it returns a non-zero exit code. + if err.worker: + err_str = err.worker.decode() + debug(f'Error calling lsblk: {err_str}') + + if dev_path: + raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') + + raise err + + output = worker.output(remove_cr=False) + return LsblkOutput.model_validate_json(output) + + +def get_lsblk_info( + dev_path: Path | str, + reverse: bool = False, + full_dev_path: bool = False +) -> LsblkInfo: + infos = _fetch_lsblk_info(dev_path, reverse=reverse, full_dev_path=full_dev_path) + + if infos.blockdevices: + return infos.blockdevices[0] + + raise DiskError(f'lsblk failed to retrieve information for "{dev_path}"') + + +def get_all_lsblk_info() -> list[LsblkInfo]: + return _fetch_lsblk_info().blockdevices + + +def get_lsblk_output() -> LsblkOutput: + return _fetch_lsblk_info() + + +def find_lsblk_info( + dev_path: Path | str, + info: list[LsblkInfo] +) -> LsblkInfo | None: + if isinstance(dev_path, str): + dev_path = Path(dev_path) + + for lsblk_info in info: + if lsblk_info.path == dev_path: + return lsblk_info + + return None + + +def get_lsblk_by_mountpoint(mountpoint: Path, as_prefix: bool = False) -> list[LsblkInfo]: + def _check(infos: list[LsblkInfo]) -> list[LsblkInfo]: + devices = [] + for entry in infos: + if as_prefix: + matches = [m for m in entry.mountpoints if str(m).startswith(str(mountpoint))] + if matches: + devices += [entry] + elif mountpoint in entry.mountpoints: + devices += [entry] + + if len(entry.children) > 0: + if len(match := _check(entry.children)) > 0: + devices += match + + return devices + + all_info = get_all_lsblk_info() + return _check(all_info) + + +def disk_layouts() -> str: + try: + lsblk_output = get_lsblk_output() + except SysCallError as err: + warn(f"Could not return disk layouts: {err}") + return '' + + return lsblk_output.model_dump_json(indent=4) diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 7015192d..5715bfec 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -319,8 +319,7 @@ class SysCommandWorker: # If history_logfile does not exist, ignore the error pass - if storage.get('arguments', {}).get('debug'): - debug(f"Executing: {self.cmd}") + debug(f"Executing: {" ".join(self.cmd)}") try: os.execve(self.cmd[0], list(self.cmd), {**os.environ, **self.environment_vars}) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 0f4a6a4f..84d1ccb1 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -1,10 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING +from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu +from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu +from archinstall.lib.models.device_model import DiskEncryption, DiskLayoutConfiguration, DiskLayoutType, EncryptionType, FilesystemType, PartitionModification from archinstall.tui import MenuItem, MenuItemGroup -from . import disk +from .args import ArchConfig from .configuration import save_config from .general import secret from .hardware import SysInfo @@ -23,12 +26,13 @@ from .interactions import ( select_additional_repositories, select_kernel, ) -from .locale.locale_menu import LocaleConfiguration, LocaleMenu +from .locale.locale_menu import LocaleMenu from .menu import AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu from .models import NetworkConfiguration, NicType from .models.audio_configuration import AudioConfiguration from .models.bootloader import Bootloader +from .models.locale import LocaleConfiguration from .models.users import User from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration @@ -44,20 +48,17 @@ if TYPE_CHECKING: class GlobalMenu(AbstractMenu): - def __init__(self, data_store: dict[str, Any]): - self._data_store = data_store - - if 'archinstall-language' not in data_store: - data_store['archinstall-language'] = translation_handler.get_language_by_abbr('en') - + def __init__(self, arch_config: ArchConfig) -> None: + self._arch_config = arch_config menu_optioons = self._get_menu_options() + self._item_group = MenuItemGroup( menu_optioons, sort_items=False, checkmarks=True ) - super().__init__(self._item_group, data_store) + super().__init__(self._item_group, config=arch_config) def _get_menu_options(self) -> list[MenuItem]: return [ @@ -65,7 +66,7 @@ class GlobalMenu(AbstractMenu): text=str(_('Archinstall language')), action=self._select_archinstall_language, display_action=lambda x: x.display_name if x else '', - key='archinstall-language' + key='archinstall_language' ), MenuItem( text=str(_('Locales')), @@ -90,8 +91,8 @@ class GlobalMenu(AbstractMenu): text=str(_('Disk encryption')), action=self._disk_encryption, preview_action=self._prev_disk_encryption, - key='disk_encryption', - dependencies=['disk_config'] + dependencies=['disk_config'], + key='disk_encryption' ), MenuItem( text=str(_('Swap')), @@ -126,13 +127,13 @@ class GlobalMenu(AbstractMenu): text=str(_('Root password')), action=self._set_root_password, preview_action=self._prev_root_pwd, - key='!root-password', + key='root_password', ), MenuItem( text=str(_('User account')), action=self._create_user_account, preview_action=self._prev_users, - key='!users' + key='users' ), MenuItem( text=str(_('Profile')), @@ -166,7 +167,7 @@ class GlobalMenu(AbstractMenu): action=add_number_of_parallel_downloads, value=0, preview_action=self._prev_parallel_dw, - key='parallel downloads' + key='parallel_downloads' ), MenuItem( text=str(_('Additional packages')), @@ -180,7 +181,7 @@ class GlobalMenu(AbstractMenu): action=select_additional_repositories, value=[], preview_action=self._prev_additional_repos, - key='additional-repositories' + key='additional_repositories' ), MenuItem( text=str(_('Timezone')), @@ -202,27 +203,28 @@ class GlobalMenu(AbstractMenu): MenuItem( text=str(_('Save configuration')), action=lambda x: self._safe_config(), - key='save_config' + key='__config__' ), MenuItem( text=str(_('Install')), preview_action=self._prev_install_invalid_config, - key='install' + key='__config__' ), MenuItem( text=str(_('Abort')), action=lambda x: exit(1), - key='abort' + key='__config__' ) ] def _safe_config(self) -> None: - data: dict[str, Any] = {} - for item in self._item_group.items: - if item.key is not None: - data[item.key] = item.value + # data: dict[str, Any] = {} + # for item in self._item_group.items: + # if item.key is not None: + # data[item.key] = item.value - save_config(data) + self.sync_all_to_config() + save_config(self._arch_config) def _missing_configs(self) -> list[str]: def check(s) -> bool: @@ -280,17 +282,17 @@ class GlobalMenu(AbstractMenu): if o.key is not None: self._item_group.find_by_key(o.key).text = o.text - def _disk_encryption(self, preset: disk.DiskEncryption | None) -> disk.DiskEncryption | None: - disk_config: disk.DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value + def _disk_encryption(self, preset: DiskEncryption | None) -> DiskEncryption | None: + disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value if not disk_config: # this should not happen as the encryption menu has the disk_config as dependency raise ValueError('No disk layout specified') - if not disk.DiskEncryption.validate_enc(disk_config): + if not DiskEncryption.validate_enc(disk_config): return None - disk_encryption = disk.DiskEncryptionMenu(disk_config, preset=preset).run() + disk_encryption = DiskEncryptionMenu(disk_config, preset=preset).run() return disk_encryption def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration: @@ -339,12 +341,12 @@ class GlobalMenu(AbstractMenu): return None def _prev_disk_config(self, item: MenuItem) -> str | None: - disk_layout_conf: disk.DiskLayoutConfiguration | None = item.value + disk_layout_conf: DiskLayoutConfiguration | None = item.value if disk_layout_conf: output = str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg()) + '\n' - if disk_layout_conf.config_type == disk.DiskLayoutType.Pre_mount: + if disk_layout_conf.config_type == DiskLayoutType.Pre_mount: output += str(_('Mountpoint')) + ': ' + str(disk_layout_conf.mountpoint) if disk_layout_conf.lvm_config: @@ -401,14 +403,14 @@ class GlobalMenu(AbstractMenu): return None def _prev_disk_encryption(self, item: MenuItem) -> str | None: - disk_config: disk.DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value - enc_config: disk.DiskEncryption | None = item.value + disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value + enc_config: DiskEncryption | None = item.value - if disk_config and not disk.DiskEncryption.validate_enc(disk_config): + if disk_config and not DiskEncryption.validate_enc(disk_config): return str(_('LVM disk encryption with more than 2 partitions is currently not supported')) if enc_config: - enc_type = disk.EncryptionType.type_to_text(enc_config.encryption_type) + enc_type = EncryptionType.type_to_text(enc_config.encryption_type) output = str(_('Encryption type')) + f': {enc_type}\n' output += str(_('Password')) + f': {secret(enc_config.encryption_password)}\n' @@ -436,7 +438,7 @@ class GlobalMenu(AbstractMenu): shim if necessary. """ bootloader = self._item_group.find_by_key('bootloader').value - boot_partition: disk.PartitionModification | None = None + boot_partition: PartitionModification | None = None if disk_config := self._item_group.find_by_key('disk_config').value: for layout in disk_config.device_modifications: @@ -449,7 +451,7 @@ class GlobalMenu(AbstractMenu): return "Boot partition not found" if bootloader == Bootloader.Limine: - if boot_partition.fs_type != disk.FilesystemType.Fat32: + if boot_partition.fs_type != FilesystemType.Fat32: return "Limine does not support booting from filesystems other than FAT32" return None @@ -499,9 +501,9 @@ class GlobalMenu(AbstractMenu): def _select_disk_config( self, - preset: disk.DiskLayoutConfiguration | None = None - ) -> disk.DiskLayoutConfiguration | None: - disk_config = disk.DiskLayoutConfigurationMenu(preset).run() + preset: DiskLayoutConfiguration | None = None + ) -> DiskLayoutConfiguration | None: + disk_config = DiskLayoutConfigurationMenu(preset).run() if disk_config != preset: self._menu_item_group.find_by_key('disk_encryption').value = None diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index f2f27e82..10bb8513 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -10,16 +10,33 @@ from pathlib import Path from types import TracebackType from typing import TYPE_CHECKING, Any +from archinstall.lib.disk.device_handler import device_handler +from archinstall.lib.disk.fido import Fido2 +from archinstall.lib.disk.utils import get_lsblk_by_mountpoint, get_lsblk_info +from archinstall.lib.models.device_model import ( + DiskEncryption, + DiskLayoutConfiguration, + EncryptionType, + FilesystemType, + LvmVolume, + PartitionModification, + SectorSize, + Size, + SubvolumeModification, + Unit, +) from archinstall.tui.curses_menu import Tui -from . import disk, pacman +from . import pacman +from .args import arch_config_handler from .exceptions import DiskError, HardwareIncompatibilityError, RequirementError, ServiceException, SysCallError from .general import SysCommand from .hardware import SysInfo -from .locale import LocaleConfiguration, verify_keyboard_layout, verify_x11_keyboard_layout +from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .luks import Luks2 from .mirrors import MirrorConfiguration from .models.bootloader import Bootloader +from .models.locale import LocaleConfiguration from .models.network_configuration import Nic from .models.users import User from .output import debug, error, info, log, warn @@ -45,8 +62,8 @@ class Installer: def __init__( self, target: Path, - disk_config: disk.DiskLayoutConfiguration, - disk_encryption: disk.DiskEncryption | None = None, + disk_config: DiskLayoutConfiguration, + disk_encryption: DiskEncryption | None = None, base_packages: list[str] = [], kernels: list[str] | None = None ): @@ -58,7 +75,7 @@ class Installer: self.kernels = kernels or ['linux'] self._disk_config = disk_config - self._disk_encryption = disk_encryption or disk.DiskEncryption(disk.EncryptionType.NoEncryption) + self._disk_encryption = disk_encryption or DiskEncryption(EncryptionType.NoEncryption) self.target: Path = target self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S') @@ -94,7 +111,7 @@ class Installer: self._zram_enabled = False self._disable_fstrim = False - self.pacman = Pacman(self.target, storage['arguments'].get('silent', False)) + self.pacman = Pacman(self.target, arch_config_handler.args.silent) def __enter__(self) -> 'Installer': return self @@ -144,7 +161,7 @@ class Installer: We need to wait for it before we continue since we opted in to use a custom mirror/region. """ - if not storage['arguments'].get('skip_ntp', False): + if not arch_config_handler.args.skip_ntp: info(_('Waiting for time sync (timedatectl show) to complete.')) started_wait = time.time() @@ -189,10 +206,10 @@ class Installer: NOTE: this function should be run AFTER running the mount_ordered_layout function """ boot_mount = self.target / 'boot' - lsblk_info = disk.get_lsblk_by_mountpoint(boot_mount) + lsblk_info = get_lsblk_by_mountpoint(boot_mount) if len(lsblk_info) > 0: - if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB, disk.SectorSize.default()): + if lsblk_info[0].size < Size(200, Unit.MiB, SectorSize.default()): raise DiskError( f'The boot partition mounted at {boot_mount} is not large enough to install a boot loader. ' f'Please resize it to at least 200MiB and re-run the installation.' @@ -208,15 +225,15 @@ class Installer: luks_handlers: dict[Any, Luks2] = {} match self._disk_encryption.encryption_type: - case disk.EncryptionType.NoEncryption: + case EncryptionType.NoEncryption: self._mount_lvm_layout() - case disk.EncryptionType.Luks: + case EncryptionType.Luks: luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions) - case disk.EncryptionType.LvmOnLuks: + case EncryptionType.LvmOnLuks: luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions) self._import_lvm() self._mount_lvm_layout(luks_handlers) - case disk.EncryptionType.LuksOnLvm: + case EncryptionType.LuksOnLvm: self._import_lvm() luks_handlers = self._prepare_luks_lvm(self._disk_encryption.lvm_volumes) self._mount_lvm_layout(luks_handlers) @@ -275,10 +292,10 @@ class Installer: def _prepare_luks_partitions( self, - partitions: list[disk.PartitionModification] - ) -> dict[disk.PartitionModification, Luks2]: + partitions: list[PartitionModification] + ) -> dict[PartitionModification, Luks2]: return { - part_mod: disk.device_handler.unlock_luks2_dev( + part_mod: device_handler.unlock_luks2_dev( part_mod.dev_path, part_mod.mapper_name, self._disk_encryption.encryption_password @@ -295,17 +312,17 @@ class Installer: return for vg in lvm_config.vol_groups: - disk.device_handler.lvm_import_vg(vg) + device_handler.lvm_import_vg(vg) for vol in vg.volumes: - disk.device_handler.lvm_vol_change(vol, True) + device_handler.lvm_vol_change(vol, True) def _prepare_luks_lvm( self, - lvm_volumes: list[disk.LvmVolume] - ) -> dict[disk.LvmVolume, Luks2]: + lvm_volumes: list[LvmVolume] + ) -> dict[LvmVolume, Luks2]: return { - vol: disk.device_handler.unlock_luks2_dev( + vol: device_handler.unlock_luks2_dev( vol.dev_path, vol.mapper_name, self._disk_encryption.encryption_password @@ -314,69 +331,69 @@ class Installer: if vol.mapper_name and vol.dev_path } - def _mount_partition(self, part_mod: disk.PartitionModification) -> None: + def _mount_partition(self, part_mod: PartitionModification) -> None: if not part_mod.dev_path: return # it would be none if it's btrfs as the subvolumes will have the mountpoints defined if part_mod.mountpoint: target = self.target / part_mod.relative_mountpoint - disk.device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options) - elif part_mod.fs_type == disk.FilesystemType.Btrfs: + device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options) + elif part_mod.fs_type == FilesystemType.Btrfs: self._mount_btrfs_subvol( part_mod.dev_path, part_mod.btrfs_subvols, part_mod.mount_options ) elif part_mod.is_swap(): - disk.device_handler.swapon(part_mod.dev_path) + device_handler.swapon(part_mod.dev_path) - def _mount_lvm_vol(self, volume: disk.LvmVolume) -> None: - if volume.fs_type != disk.FilesystemType.Btrfs: + def _mount_lvm_vol(self, volume: LvmVolume) -> None: + if volume.fs_type != FilesystemType.Btrfs: if volume.mountpoint and volume.dev_path: target = self.target / volume.relative_mountpoint - disk.device_handler.mount(volume.dev_path, target, options=volume.mount_options) + device_handler.mount(volume.dev_path, target, options=volume.mount_options) - if volume.fs_type == disk.FilesystemType.Btrfs and volume.dev_path: + if volume.fs_type == FilesystemType.Btrfs and volume.dev_path: self._mount_btrfs_subvol(volume.dev_path, volume.btrfs_subvols, volume.mount_options) - def _mount_luks_partition(self, part_mod: disk.PartitionModification, luks_handler: Luks2) -> None: + def _mount_luks_partition(self, part_mod: PartitionModification, luks_handler: Luks2) -> None: if not luks_handler.mapper_dev: return None - if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.btrfs_subvols: + if part_mod.fs_type == FilesystemType.Btrfs and part_mod.btrfs_subvols: self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols, part_mod.mount_options) elif part_mod.mountpoint: target = self.target / part_mod.relative_mountpoint - disk.device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options) + device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options) - def _mount_luks_volume(self, volume: disk.LvmVolume, luks_handler: Luks2) -> None: - if volume.fs_type != disk.FilesystemType.Btrfs: + def _mount_luks_volume(self, volume: LvmVolume, luks_handler: Luks2) -> None: + if volume.fs_type != FilesystemType.Btrfs: if volume.mountpoint and luks_handler.mapper_dev: target = self.target / volume.relative_mountpoint - disk.device_handler.mount(luks_handler.mapper_dev, target, options=volume.mount_options) + device_handler.mount(luks_handler.mapper_dev, target, options=volume.mount_options) - if volume.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev: + if volume.fs_type == FilesystemType.Btrfs and luks_handler.mapper_dev: self._mount_btrfs_subvol(luks_handler.mapper_dev, volume.btrfs_subvols, volume.mount_options) def _mount_btrfs_subvol( self, dev_path: Path, - subvolumes: list[disk.SubvolumeModification], + subvolumes: list[SubvolumeModification], mount_options: list[str] = [] ) -> None: for subvol in sorted(subvolumes, key=lambda x: x.relative_mountpoint): mountpoint = self.target / subvol.relative_mountpoint options = mount_options + [f'subvol={subvol.name}'] - disk.device_handler.mount(dev_path, mountpoint, options=options) + device_handler.mount(dev_path, mountpoint, options=options) def generate_key_files(self) -> None: match self._disk_encryption.encryption_type: - case disk.EncryptionType.Luks: + case EncryptionType.Luks: self._generate_key_files_partitions() - case disk.EncryptionType.LuksOnLvm: + case EncryptionType.LuksOnLvm: self._generate_key_file_lvm_volumes() - case disk.EncryptionType.LvmOnLuks: + case EncryptionType.LvmOnLuks: # currently LvmOnLuks only supports a single # partitioning layout (boot + partition) # so we won't need any keyfile generation atm @@ -398,7 +415,7 @@ class Installer: if part_mod.is_root() and not gen_enc_file: if self._disk_encryption.hsm_device: - disk.Fido2.fido2_enroll( + Fido2.fido2_enroll( self._disk_encryption.hsm_device, part_mod.safe_dev_path, self._disk_encryption.encryption_password @@ -420,7 +437,7 @@ class Installer: if vol.is_root() and not gen_enc_file: if self._disk_encryption.hsm_device: - disk.Fido2.fido2_enroll( + Fido2.fido2_enroll( self._disk_encryption.hsm_device, vol.safe_dev_path, self._disk_encryption.encryption_password @@ -742,7 +759,7 @@ class Installer: def _prepare_fs_type( self, - fs_type: disk.FilesystemType, + fs_type: FilesystemType, mountpoint: Path | None ) -> None: if (pkg := fs_type.installation_pkg) is not None: @@ -778,7 +795,7 @@ class Installer: multilib: bool = False, mkinitcpio: bool = True, hostname: str | None = None, - locale_config: LocaleConfiguration = LocaleConfiguration.default() + locale_config: LocaleConfiguration | None = LocaleConfiguration.default() ): if self._disk_config.lvm_config: lvm = 'lvm2' @@ -790,7 +807,7 @@ class Installer: if vol.fs_type is not None: self._prepare_fs_type(vol.fs_type, vol.mountpoint) - types = (disk.EncryptionType.LvmOnLuks, disk.EncryptionType.LuksOnLvm) + types = (EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm) if self._disk_encryption.encryption_type in types: self._prepare_encrypt(lvm) else: @@ -852,8 +869,9 @@ class Installer: if hostname: self.set_hostname(hostname) - self.set_locale(locale_config) - self.set_keyboard_language(locale_config.kb_layout) + if locale_config: + self.set_locale(locale_config) + self.set_keyboard_language(locale_config.kb_layout) # TODO: Use python functions for this SysCommand(f'arch-chroot {self.target} chmod 700 /root') @@ -889,19 +907,19 @@ class Installer: else: raise ValueError("Archinstall currently only supports setting up swap on zram") - def _get_efi_partition(self) -> disk.PartitionModification | None: + def _get_efi_partition(self) -> PartitionModification | None: for layout in self._disk_config.device_modifications: if partition := layout.get_efi_partition(): return partition return None - def _get_boot_partition(self) -> disk.PartitionModification | None: + def _get_boot_partition(self) -> PartitionModification | None: for layout in self._disk_config.device_modifications: if boot := layout.get_boot_partition(): return boot return None - def _get_root(self) -> disk.PartitionModification | disk.LvmVolume | None: + def _get_root(self) -> PartitionModification | LvmVolume | None: if self._disk_config.lvm_config: return self._disk_config.lvm_config.get_root_volume() else: @@ -911,7 +929,7 @@ class Installer: return None def _get_luks_uuid_from_mapper_dev(self, mapper_dev_path: Path) -> str: - lsblk_info = disk.get_lsblk_info(mapper_dev_path, reverse=True, full_dev_path=True) + lsblk_info = get_lsblk_info(mapper_dev_path, reverse=True, full_dev_path=True) if not lsblk_info.children or not lsblk_info.children[0].uuid: raise ValueError('Unable to determine UUID of luks superblock') @@ -920,7 +938,7 @@ class Installer: def _get_kernel_params_partition( self, - root_partition: disk.PartitionModification, + root_partition: PartitionModification, id_root: bool = True, partuuid: bool = True ) -> list[str]: @@ -958,16 +976,16 @@ class Installer: def _get_kernel_params_lvm( self, - lvm: disk.LvmVolume + lvm: LvmVolume ) -> list[str]: kernel_parameters = [] match self._disk_encryption.encryption_type: - case disk.EncryptionType.LvmOnLuks: + case EncryptionType.LvmOnLuks: if not lvm.vg_name: raise ValueError(f'Unable to determine VG name for {lvm.name}') - pv_seg_info = disk.device_handler.lvm_pvseg_info(lvm.vg_name, lvm.name) + pv_seg_info = device_handler.lvm_pvseg_info(lvm.vg_name, lvm.name) if not pv_seg_info: raise ValueError(f'Unable to determine PV segment info for {lvm.vg_name}/{lvm.name}') @@ -980,7 +998,7 @@ class Installer: else: debug(f'LvmOnLuks, encrypted root partition, identifying by UUID: {uuid}') kernel_parameters.append(f'cryptdevice=UUID={uuid}:cryptlvm root={lvm.safe_dev_path}') - case disk.EncryptionType.LuksOnLvm: + case EncryptionType.LuksOnLvm: uuid = self._get_luks_uuid_from_mapper_dev(lvm.mapper_path) if self._disk_encryption.hsm_device: @@ -989,7 +1007,7 @@ class Installer: else: debug(f'LuksOnLvm, encrypted root partition, identifying by UUID: {uuid}') kernel_parameters.append(f'cryptdevice=UUID={uuid}:root root=/dev/mapper/root') - case disk.EncryptionType.NoEncryption: + case EncryptionType.NoEncryption: debug(f'Identifying root lvm by mapper device: {lvm.dev_path}') kernel_parameters.append(f'root={lvm.safe_dev_path}') @@ -997,13 +1015,13 @@ class Installer: def _get_kernel_params( self, - root: disk.PartitionModification | disk.LvmVolume, + root: PartitionModification | LvmVolume, id_root: bool = True, partuuid: bool = True ) -> list[str]: kernel_parameters = [] - if isinstance(root, disk.LvmVolume): + if isinstance(root, LvmVolume): kernel_parameters = self._get_kernel_params_lvm(root) else: kernel_parameters = self._get_kernel_params_partition(root, id_root, partuuid) @@ -1030,9 +1048,9 @@ class Installer: def _add_systemd_bootloader( self, - boot_partition: disk.PartitionModification, - root: disk.PartitionModification | disk.LvmVolume, - efi_partition: disk.PartitionModification | None, + boot_partition: PartitionModification, + root: PartitionModification | LvmVolume, + efi_partition: PartitionModification | None, uki_enabled: bool = False ) -> None: debug('Installing systemd bootloader') @@ -1129,9 +1147,9 @@ class Installer: def _add_grub_bootloader( self, - boot_partition: disk.PartitionModification, - root: disk.PartitionModification | disk.LvmVolume, - efi_partition: disk.PartitionModification | None + boot_partition: PartitionModification, + root: PartitionModification | LvmVolume, + efi_partition: PartitionModification | None ) -> None: debug('Installing grub bootloader') @@ -1189,7 +1207,7 @@ class Installer: else: info(f"GRUB boot partition: {boot_partition.dev_path}") - parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path) + parent_dev_path = device_handler.get_parent_device_path(boot_partition.safe_dev_path) add_options = [ '--target=i386-pc', @@ -1214,9 +1232,9 @@ class Installer: def _add_limine_bootloader( self, - boot_partition: disk.PartitionModification, - efi_partition: disk.PartitionModification | None, - root: disk.PartitionModification | disk.LvmVolume + boot_partition: PartitionModification, + efi_partition: PartitionModification | None, + root: PartitionModification | LvmVolume ) -> None: debug('Installing limine bootloader') @@ -1249,16 +1267,16 @@ class Installer: f' && /usr/bin/cp /usr/share/limine/BOOTX64.EFI {efi_partition.mountpoint}/EFI/BOOT/' ) else: - parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path) + parent_dev_path = device_handler.get_parent_device_path(boot_partition.safe_dev_path) - if unique_path := disk.device_handler.get_unique_path_for_device(parent_dev_path): + if unique_path := device_handler.get_unique_path_for_device(parent_dev_path): parent_dev_path = unique_path try: # The `limine-bios.sys` file contains stage 3 code. shutil.copy(limine_path / 'limine-bios.sys', self.target / 'boot') - # `limine bios-install` deploys the stage 1 and 2 to the disk. + # `limine bios-install` deploys the stage 1 and 2 to the SysCommand(f'arch-chroot {self.target} limine bios-install {parent_dev_path}', peek_output=True) except Exception as err: raise DiskError(f'Failed to install Limine on {parent_dev_path}: {err}') @@ -1308,8 +1326,8 @@ Exec = /bin/sh -c "{hook_command}" def _add_efistub_bootloader( self, - boot_partition: disk.PartitionModification, - root: disk.PartitionModification | disk.LvmVolume, + boot_partition: PartitionModification, + root: PartitionModification | LvmVolume, uki_enabled: bool = False ) -> None: debug('Installing efistub bootloader') @@ -1336,7 +1354,7 @@ Exec = /bin/sh -c "{hook_command}" loader = '/EFI/Linux/arch-{kernel}.efi' cmdline = [] - parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path) + parent_dev_path = device_handler.get_parent_device_path(boot_partition.safe_dev_path) cmd_template = ( 'efibootmgr', @@ -1358,8 +1376,8 @@ Exec = /bin/sh -c "{hook_command}" def _config_uki( self, - root: disk.PartitionModification | disk.LvmVolume, - efi_partition: disk.PartitionModification | None + root: PartitionModification | LvmVolume, + efi_partition: PartitionModification | None ) -> None: if not efi_partition or not efi_partition.mountpoint: raise ValueError(f'Could not detect ESP at mountpoint {self.target}') diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 949b447a..37e7819c 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -1,16 +1,37 @@ -from __future__ import annotations - from pathlib import Path from typing import TYPE_CHECKING +from archinstall.lib.args import arch_config_handler +from archinstall.lib.disk.device_handler import device_handler +from archinstall.lib.disk.partitioning_menu import manual_partitioning from archinstall.lib.menu.menu_helper import MenuHelper +from archinstall.lib.models.device_model import ( + BDevice, + BtrfsMountOption, + DeviceModification, + DiskLayoutConfiguration, + DiskLayoutType, + FilesystemType, + LvmConfiguration, + LvmLayoutType, + LvmVolume, + LvmVolumeGroup, + LvmVolumeStatus, + ModificationStatus, + PartitionFlag, + PartitionModification, + PartitionType, + SectorSize, + Size, + SubvolumeModification, + Unit, + _DeviceInfo, +) +from archinstall.lib.output import debug from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, Orientation, PreviewStyle, ResultType, SelectMenu -from .. import disk -from ..disk.device_model import BtrfsMountOption from ..hardware import SysInfo -from ..output import FormattedOutput, debug -from ..storage import storage +from ..output import FormattedOutput from ..utils.util import prompt_dir if TYPE_CHECKING: @@ -21,10 +42,10 @@ if TYPE_CHECKING: _: Callable[[str], DeferredTranslation] -def select_devices(preset: list[disk.BDevice] | None = []) -> list[disk.BDevice]: +def select_devices(preset: list[BDevice] | None = []) -> list[BDevice]: def _preview_device_selection(item: MenuItem) -> str | None: - device: disk._DeviceInfo = item.get_value() - dev = disk.device_handler.get_device(device.path) + device: _DeviceInfo = item.get_value() + dev = device_handler.get_device(device.path) if dev and dev.partition_infos: return FormattedOutput.as_table(dev.partition_infos) @@ -33,7 +54,7 @@ def select_devices(preset: list[disk.BDevice] | None = []) -> list[disk.BDevice] if preset is None: preset = [] - devices = disk.device_handler.devices + devices = device_handler.devices options = [d.device_info for d in devices] presets = [p.device_info for p in preset] @@ -59,7 +80,7 @@ def select_devices(preset: list[disk.BDevice] | None = []) -> list[disk.BDevice] case ResultType.Skip: return preset case ResultType.Selection: - selected_device_info: list[disk._DeviceInfo] = result.get_values() + selected_device_info: list[_DeviceInfo] = result.get_values() selected_devices = [] for device in devices: @@ -70,49 +91,43 @@ def select_devices(preset: list[disk.BDevice] | None = []) -> list[disk.BDevice] def get_default_partition_layout( - devices: list[disk.BDevice], - filesystem_type: disk.FilesystemType | None = None, - advanced_option: bool = False -) -> list[disk.DeviceModification]: + devices: list[BDevice], + filesystem_type: FilesystemType | None = None +) -> list[DeviceModification]: if len(devices) == 1: device_modification = suggest_single_disk_layout( devices[0], - filesystem_type=filesystem_type, - advanced_options=advanced_option + filesystem_type=filesystem_type ) return [device_modification] else: return suggest_multi_disk_layout( devices, - filesystem_type=filesystem_type, - advanced_options=advanced_option + filesystem_type=filesystem_type ) def _manual_partitioning( - preset: list[disk.DeviceModification], - devices: list[disk.BDevice] -) -> list[disk.DeviceModification]: + preset: list[DeviceModification], + devices: list[BDevice] +) -> list[DeviceModification]: modifications = [] for device in devices: mod = next(filter(lambda x: x.device == device, preset), None) if not mod: - mod = disk.DeviceModification(device, wipe=False) + mod = DeviceModification(device, wipe=False) - if partitions := disk.manual_partitioning(device, preset=mod.partitions): + if partitions := manual_partitioning(device, preset=mod.partitions): mod.partitions = partitions modifications.append(mod) return modifications -def select_disk_config( - preset: disk.DiskLayoutConfiguration | None = None, - advanced_option: bool = False -) -> disk.DiskLayoutConfiguration | None: - default_layout = disk.DiskLayoutType.Default.display_msg() - manual_mode = disk.DiskLayoutType.Manual.display_msg() - pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg() +def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLayoutConfiguration | None: + default_layout = DiskLayoutType.Default.display_msg() + manual_mode = DiskLayoutType.Manual.display_msg() + pre_mount_mode = DiskLayoutType.Pre_mount.display_msg() items = [ MenuItem(default_layout, value=default_layout), @@ -149,12 +164,10 @@ def select_disk_config( if path is None: return None - mods = disk.device_handler.detect_pre_mounted_mods(path) + mods = device_handler.detect_pre_mounted_mods(path) - storage['arguments']['mount_point'] = path - - return disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Pre_mount, + return DiskLayoutConfiguration( + config_type=DiskLayoutType.Pre_mount, device_modifications=mods, mountpoint=path ) @@ -166,10 +179,10 @@ def select_disk_config( return None if result.get_value() == default_layout: - modifications = get_default_partition_layout(devices, advanced_option=advanced_option) + modifications = get_default_partition_layout(devices) if modifications: - return disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Default, + return DiskLayoutConfiguration( + config_type=DiskLayoutType.Default, device_modifications=modifications ) elif result.get_value() == manual_mode: @@ -177,8 +190,8 @@ def select_disk_config( modifications = _manual_partitioning(preset_mods, devices) if modifications: - return disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Manual, + return DiskLayoutConfiguration( + config_type=DiskLayoutType.Manual, device_modifications=modifications ) @@ -186,11 +199,11 @@ def select_disk_config( def select_lvm_config( - disk_config: disk.DiskLayoutConfiguration, - preset: disk.LvmConfiguration | None = None, -) -> disk.LvmConfiguration | None: + disk_config: DiskLayoutConfiguration, + preset: LvmConfiguration | None = None, +) -> LvmConfiguration | None: preset_value = preset.config_type.display_msg() if preset else None - default_mode = disk.LvmLayoutType.Default.display_msg() + default_mode = LvmLayoutType.Default.display_msg() items = [MenuItem(default_mode, value=default_mode)] group = MenuItemGroup(items) @@ -216,37 +229,37 @@ def select_lvm_config( return None -def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.PartitionModification: - flags = [disk.PartitionFlag.BOOT] - size = disk.Size(1, disk.Unit.GiB, sector_size) +def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModification: + flags = [PartitionFlag.BOOT] + size = Size(1, Unit.GiB, sector_size) if using_gpt: - start = disk.Size(1, disk.Unit.MiB, sector_size) - flags.append(disk.PartitionFlag.ESP) + start = Size(1, Unit.MiB, sector_size) + flags.append(PartitionFlag.ESP) else: - start = disk.Size(3, disk.Unit.MiB, sector_size) + start = Size(3, Unit.MiB, sector_size) # boot partition - return disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, + return PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, start=start, length=size, mountpoint=Path('/boot'), - fs_type=disk.FilesystemType.Fat32, + fs_type=FilesystemType.Fat32, flags=flags ) -def select_main_filesystem_format(advanced_options: bool = False) -> disk.FilesystemType: +def select_main_filesystem_format() -> FilesystemType: items = [ - MenuItem('btrfs', value=disk.FilesystemType.Btrfs), - MenuItem('ext4', value=disk.FilesystemType.Ext4), - MenuItem('xfs', value=disk.FilesystemType.Xfs), - MenuItem('f2fs', value=disk.FilesystemType.F2fs) + MenuItem('btrfs', value=FilesystemType.Btrfs), + MenuItem('ext4', value=FilesystemType.Ext4), + MenuItem('xfs', value=FilesystemType.Xfs), + MenuItem('f2fs', value=FilesystemType.F2fs) ] - if advanced_options: - items.append(MenuItem('ntfs', value=disk.FilesystemType.Ntfs)) + if arch_config_handler.args.advanced: + items.append(MenuItem('ntfs', value=FilesystemType.Ntfs)) group = MenuItemGroup(items, sort_items=False) result = SelectMenu( @@ -292,36 +305,35 @@ def select_mount_options() -> list[str]: raise ValueError('Unhandled result type') -def process_root_partition_size(total_size: disk.Size, sector_size: disk.SectorSize) -> disk.Size: +def process_root_partition_size(total_size: Size, sector_size: SectorSize) -> Size: # root partition size processing - total_device_size = total_size.convert(disk.Unit.GiB) + total_device_size = total_size.convert(Unit.GiB) if total_device_size.value > 500: # maximum size - return disk.Size(value=50, unit=disk.Unit.GiB, sector_size=sector_size) + return Size(value=50, unit=Unit.GiB, sector_size=sector_size) elif total_device_size.value < 320: # minimum size - return disk.Size(value=32, unit=disk.Unit.GiB, sector_size=sector_size) + return Size(value=32, unit=Unit.GiB, sector_size=sector_size) else: # 10% of total size length = total_device_size.value // 10 - return disk.Size(value=length, unit=disk.Unit.GiB, sector_size=sector_size) + return Size(value=length, unit=Unit.GiB, sector_size=sector_size) def suggest_single_disk_layout( - device: disk.BDevice, - filesystem_type: disk.FilesystemType | None = None, - advanced_options: bool = False, + device: BDevice, + filesystem_type: FilesystemType | None = None, separate_home: bool | None = None -) -> disk.DeviceModification: +) -> DeviceModification: if not filesystem_type: - filesystem_type = select_main_filesystem_format(advanced_options) + filesystem_type = select_main_filesystem_format() sector_size = device.device_info.sector_size total_size = device.device_info.total_size available_space = total_size - min_size_to_allow_home_part = disk.Size(64, disk.Unit.GiB, sector_size) + min_size_to_allow_home_part = Size(64, Unit.GiB, sector_size) - if filesystem_type == disk.FilesystemType.Btrfs: + if filesystem_type == FilesystemType.Btrfs: prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + '\n' group = MenuItemGroup.yes_no() group.set_focus_by_value(MenuItem.yes().value) @@ -340,7 +352,7 @@ def suggest_single_disk_layout( using_subvolumes = False mount_options = [] - device_modification = disk.DeviceModification(device, wipe=True) + device_modification = DeviceModification(device, wipe=True) using_gpt = SysInfo.has_uefi() @@ -395,9 +407,9 @@ def suggest_single_disk_layout( else: root_length = available_space - root_start - root_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, + root_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, start=root_start, length=root_length, mountpoint=Path('/') if not using_subvolumes else None, @@ -412,11 +424,11 @@ def suggest_single_disk_layout( # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh subvolumes = [ - disk.SubvolumeModification(Path('@'), Path('/')), - disk.SubvolumeModification(Path('@home'), Path('/home')), - disk.SubvolumeModification(Path('@log'), Path('/var/log')), - disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), - disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')) + SubvolumeModification(Path('@'), Path('/')), + SubvolumeModification(Path('@home'), Path('/home')), + SubvolumeModification(Path('@log'), Path('/var/log')), + SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), + SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')) ] root_partition.btrfs_subvols = subvolumes elif using_home_partition: @@ -428,11 +440,11 @@ def suggest_single_disk_layout( flags = [] if using_gpt: - flags.append(disk.PartitionFlag.LINUX_HOME) + flags.append(PartitionFlag.LINUX_HOME) - home_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, + home_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, start=home_start, length=home_length, mountpoint=Path('/home'), @@ -446,23 +458,22 @@ def suggest_single_disk_layout( def suggest_multi_disk_layout( - devices: list[disk.BDevice], - filesystem_type: disk.FilesystemType | None = None, - advanced_options: bool = False -) -> list[disk.DeviceModification]: + devices: list[BDevice], + filesystem_type: FilesystemType | None = None +) -> list[DeviceModification]: if not devices: return [] # Not really a rock solid foundation of information to stand on, but it's a start: # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ - min_home_partition_size = disk.Size(40, disk.Unit.GiB, disk.SectorSize.default()) + min_home_partition_size = Size(40, Unit.GiB, SectorSize.default()) # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? - desired_root_partition_size = disk.Size(32, disk.Unit.GiB, disk.SectorSize.default()) + desired_root_partition_size = Size(32, Unit.GiB, SectorSize.default()) mount_options = [] if not filesystem_type: - filesystem_type = select_main_filesystem_format(advanced_options) + filesystem_type = select_main_filesystem_format() # find proper disk for /home possible_devices = [d for d in devices if d.device_info.total_size >= min_home_partition_size] @@ -475,13 +486,13 @@ def suggest_multi_disk_layout( delta = device.device_info.total_size - desired_root_partition_size devices_delta[device] = delta - sorted_delta: list[tuple[disk.BDevice, disk.Size]] = sorted(devices_delta.items(), key=lambda x: x[1]) - root_device: disk.BDevice | None = sorted_delta[0][0] + sorted_delta: list[tuple[BDevice, Size]] = sorted(devices_delta.items(), key=lambda x: x[1]) + root_device: BDevice | None = sorted_delta[0][0] if home_device is None or root_device is None: text = str(_('The selected drives do not have the minimum capacity required for an automatic suggestion\n')) - text += str(_('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB))) - text += str(_('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB))) + text += str(_('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(Unit.GiB))) + text += str(_('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(Unit.GiB))) items = [MenuItem(str(_('Continue')))] group = MenuItemGroup(items) @@ -489,7 +500,7 @@ def suggest_multi_disk_layout( return [] - if filesystem_type == disk.FilesystemType.Btrfs: + if filesystem_type == FilesystemType.Btrfs: mount_options = select_mount_options() device_paths = ', '.join([str(d.device_info.path) for d in devices]) @@ -498,8 +509,8 @@ def suggest_multi_disk_layout( debug(f'/root: {root_device.device_info.path}') debug(f'/home: {home_device.device_info.path}') - root_device_modification = disk.DeviceModification(root_device, wipe=True) - home_device_modification = disk.DeviceModification(home_device, wipe=True) + root_device_modification = DeviceModification(root_device, wipe=True) + home_device_modification = DeviceModification(home_device, wipe=True) root_device_sector_size = root_device_modification.device.device_info.sector_size home_device_sector_size = home_device_modification.device.device_info.sector_size @@ -519,9 +530,9 @@ def suggest_multi_disk_layout( root_length = root_length.align() # add root partition to the root device - root_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, + root_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, start=root_start, length=root_length, mountpoint=Path('/'), @@ -530,20 +541,20 @@ def suggest_multi_disk_layout( ) root_device_modification.add_partition(root_partition) - home_start = disk.Size(1, disk.Unit.MiB, home_device_sector_size) + home_start = Size(1, Unit.MiB, home_device_sector_size) home_length = home_device.device_info.total_size - home_start flags = [] if using_gpt: home_length = home_length.gpt_end() - flags.append(disk.PartitionFlag.LINUX_HOME) + flags.append(PartitionFlag.LINUX_HOME) home_length = home_length.align() # add home partition to home device - home_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, + home_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, start=home_start, length=home_length, mountpoint=Path('/home'), @@ -557,11 +568,11 @@ def suggest_multi_disk_layout( def suggest_lvm_layout( - disk_config: disk.DiskLayoutConfiguration, - filesystem_type: disk.FilesystemType | None = None, + disk_config: DiskLayoutConfiguration, + filesystem_type: FilesystemType | None = None, vg_grp_name: str = 'ArchinstallVg', -) -> disk.LvmConfiguration: - if disk_config.config_type != disk.DiskLayoutType.Default: +) -> LvmConfiguration: + if disk_config.config_type != DiskLayoutType.Default: raise ValueError('LVM suggested volumes are only available for default partitioning') using_subvolumes = False @@ -572,7 +583,7 @@ def suggest_lvm_layout( if not filesystem_type: filesystem_type = select_main_filesystem_format() - if filesystem_type == disk.FilesystemType.Btrfs: + if filesystem_type == FilesystemType.Btrfs: prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + '\n' group = MenuItemGroup.yes_no() group.set_focus_by_value(MenuItem.yes().value) @@ -592,17 +603,17 @@ def suggest_lvm_layout( if using_subvolumes: btrfs_subvols = [ - disk.SubvolumeModification(Path('@'), Path('/')), - disk.SubvolumeModification(Path('@home'), Path('/home')), - disk.SubvolumeModification(Path('@log'), Path('/var/log')), - disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), - disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')), + SubvolumeModification(Path('@'), Path('/')), + SubvolumeModification(Path('@home'), Path('/home')), + SubvolumeModification(Path('@log'), Path('/var/log')), + SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), + SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')), ] home_volume = False - boot_part: disk.PartitionModification | None = None - other_part: list[disk.PartitionModification] = [] + boot_part: PartitionModification | None = None + other_part: list[PartitionModification] = [] for mod in disk_config.device_modifications: for part in mod.partitions: @@ -616,15 +627,15 @@ def suggest_lvm_layout( total_vol_available = sum( [p.length for p in other_part], - disk.Size(0, disk.Unit.B, disk.SectorSize.default()), + Size(0, Unit.B, SectorSize.default()), ) - root_vol_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default()) + root_vol_size = Size(20, Unit.GiB, SectorSize.default()) home_vol_size = total_vol_available - root_vol_size - lvm_vol_group = disk.LvmVolumeGroup(vg_grp_name, pvs=other_part) + lvm_vol_group = LvmVolumeGroup(vg_grp_name, pvs=other_part) - root_vol = disk.LvmVolume( - status=disk.LvmVolumeStatus.Create, + root_vol = LvmVolume( + status=LvmVolumeStatus.Create, name='root', fs_type=filesystem_type, length=root_vol_size, @@ -636,8 +647,8 @@ def suggest_lvm_layout( lvm_vol_group.volumes.append(root_vol) if home_volume: - home_vol = disk.LvmVolume( - status=disk.LvmVolumeStatus.Create, + home_vol = LvmVolume( + status=LvmVolumeStatus.Create, name='home', fs_type=filesystem_type, length=home_vol_size, @@ -646,4 +657,4 @@ def suggest_lvm_layout( lvm_vol_group.volumes.append(home_vol) - return disk.LvmConfiguration(disk.LvmLayoutType.Default, [lvm_vol_group]) + return LvmConfiguration(LvmLayoutType.Default, [lvm_vol_group]) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 17bdb152..713ecd66 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -5,11 +5,11 @@ from typing import TYPE_CHECKING from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, Orientation, ResultType, SelectMenu, Tui +from ..args import arch_config_handler from ..locale import list_timezones from ..models.audio_configuration import Audio, AudioConfiguration from ..output import warn from ..packages.packages import validate_package_list -from ..storage import storage from ..translationhandler import Language if TYPE_CHECKING: @@ -175,7 +175,7 @@ def ask_additional_packages_to_install(preset: list[str] = []) -> list[str]: if len(packages) == 0: return None - if storage['arguments']['offline'] or storage['arguments']['no_pkg_lookups']: + if arch_config_handler.args.offline or arch_config_handler.args.no_pkg_lookups: return None # Verify packages that were given diff --git a/archinstall/lib/locale/__init__.py b/archinstall/lib/locale/__init__.py index 47e3d22f..84b1d4c0 100644 --- a/archinstall/lib/locale/__init__.py +++ b/archinstall/lib/locale/__init__.py @@ -1,4 +1,3 @@ -from .locale_menu import LocaleConfiguration from .utils import ( list_keyboard_languages, list_locales, diff --git a/archinstall/lib/locale/locale_menu.py b/archinstall/lib/locale/locale_menu.py index 0e589ca1..175d88bd 100644 --- a/archinstall/lib/locale/locale_menu.py +++ b/archinstall/lib/locale/locale_menu.py @@ -1,10 +1,10 @@ -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, override +from typing import TYPE_CHECKING, override from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, ResultType, SelectMenu from ..menu import AbstractSubMenu -from .utils import get_kb_layout, list_keyboard_languages, list_locales, set_kb_layout +from ..models.locale import LocaleConfiguration +from .utils import list_keyboard_languages, list_locales, set_kb_layout if TYPE_CHECKING: from collections.abc import Callable @@ -14,66 +14,20 @@ if TYPE_CHECKING: _: Callable[[str], DeferredTranslation] -@dataclass -class LocaleConfiguration: - kb_layout: str - sys_lang: str - sys_enc: str - - @staticmethod - def default() -> 'LocaleConfiguration': - layout = get_kb_layout() - if layout == "": - return LocaleConfiguration('us', 'en_US', 'UTF-8') - return LocaleConfiguration(layout, 'en_US', 'UTF-8') - - def json(self) -> dict[str, str]: - return { - 'kb_layout': self.kb_layout, - 'sys_lang': self.sys_lang, - 'sys_enc': self.sys_enc - } - - def preview(self) -> str: - output = '{}: {}\n'.format(str(_('Keyboard layout')), self.kb_layout) - output += '{}: {}\n'.format(str(_('Locale language')), self.sys_lang) - output += '{}: {}'.format(str(_('Locale encoding')), self.sys_enc) - return output - - @classmethod - def _load_config(cls, config: 'LocaleConfiguration', args: dict[str, Any]) -> 'LocaleConfiguration': - if 'sys_lang' in args: - config.sys_lang = args['sys_lang'] - if 'sys_enc' in args: - config.sys_enc = args['sys_enc'] - if 'kb_layout' in args: - config.kb_layout = args['kb_layout'] - - return config - - @classmethod - def parse_arg(cls, args: dict[str, Any]) -> 'LocaleConfiguration': - default = cls.default() - - if 'locale_config' in args: - default = cls._load_config(default, args['locale_config']) - else: - default = cls._load_config(default, args) - - return default - - class LocaleMenu(AbstractSubMenu): def __init__( self, locale_conf: LocaleConfiguration ): self._locale_conf = locale_conf - self._data_store: dict[str, str] = {} menu_optioons = self._define_menu_options() self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True) - super().__init__(self._item_group, data_store=self._data_store, allow_reset=True) + super().__init__( + self._item_group, + config=self._locale_conf, + allow_reset=True + ) def _define_menu_options(self) -> list[MenuItem]: return [ @@ -82,44 +36,36 @@ class LocaleMenu(AbstractSubMenu): action=self._select_kb_layout, value=self._locale_conf.kb_layout, preview_action=self._prev_locale, - key='keyboard-layout' + key='kb_layout' ), MenuItem( text=str(_('Locale language')), action=select_locale_lang, value=self._locale_conf.sys_lang, preview_action=self._prev_locale, - key='sys-language' + key='sys_lang' ), MenuItem( text=str(_('Locale encoding')), action=select_locale_enc, value=self._locale_conf.sys_enc, preview_action=self._prev_locale, - key='sys-encoding' + key='sys_enc' ) ] def _prev_locale(self, item: MenuItem) -> str | None: temp_locale = LocaleConfiguration( - self._menu_item_group.find_by_key('keyboard-layout').get_value(), - self._menu_item_group.find_by_key('sys-language').get_value(), - self._menu_item_group.find_by_key('sys-encoding').get_value(), + self._menu_item_group.find_by_key('kb_layout').get_value(), + self._menu_item_group.find_by_key('sys_lang').get_value(), + self._menu_item_group.find_by_key('sys_enc').get_value(), ) return temp_locale.preview() @override def run(self) -> LocaleConfiguration: super().run() - - if not self._data_store: - return LocaleConfiguration.default() - - return LocaleConfiguration( - self._data_store['keyboard-layout'], - self._data_store['sys-language'], - self._data_store['sys-encoding'] - ) + return self._locale_conf def _select_kb_layout(self, preset: str | None) -> str | None: kb_lang = select_kb_layout(preset) diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index 45990452..8673280b 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -5,7 +5,8 @@ import time from dataclasses import dataclass from pathlib import Path -from . import disk +from archinstall.lib.disk.utils import get_lsblk_info + from .exceptions import DiskError, SysCallError from .general import SysCommand, SysCommandWorker, generate_password from .output import debug, info @@ -152,18 +153,19 @@ class Luks2: raise DiskError(f'Failed to open luks2 device: {self.luks_dev_path}') def lock(self) -> None: - disk.device_handler.umount(self.luks_dev_path) + from archinstall.lib.disk.device_handler import device_handler + device_handler.umount(self.luks_dev_path) # Get crypt-information about the device by doing a reverse lookup starting with the partition path # For instance: /dev/sda - lsblk_info = disk.get_lsblk_info(self.luks_dev_path) + lsblk_info = get_lsblk_info(self.luks_dev_path) # For each child (sub-partition/sub-device) for child in lsblk_info.children: # Unmount the child location for mountpoint in child.mountpoints: debug(f'Unmounting {mountpoint}') - disk.device_handler.umount(mountpoint, recursive=True) + device_handler.umount(mountpoint, recursive=True) # And close it if possible. debug(f"Closing crypt device {child.name}") diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 47c2f696..e3ae00e1 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -13,24 +13,27 @@ if TYPE_CHECKING: _: Callable[[str], DeferredTranslation] +CONFIG_KEY = '__config__' + + class AbstractMenu: def __init__( self, item_group: MenuItemGroup, - data_store: dict[str, Any], + config: Any, auto_cursor: bool = True, allow_reset: bool = False, reset_warning: str | None = None ): self._menu_item_group = item_group - self._data_store = data_store + self._config = config self.auto_cursor = auto_cursor self._allow_reset = allow_reset self._reset_warning = reset_warning self.is_context_mgr = False - self._sync_all_from_ds() + self._sync_from_config() def __enter__(self, *args: Any, **kwargs: Any) -> AbstractMenu: self.is_context_mgr = True @@ -44,43 +47,48 @@ class AbstractMenu: Tui.print("Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues") raise args[1] - self._sync_all_to_ds() + self.sync_all_to_config() - def _sync_all_from_ds(self) -> None: + def _sync_from_config(self) -> None: for item in self._menu_item_group.menu_items: - if item.key is not None: - if (store_value := self._data_store.get(item.key, None)) is not None: - item.value = store_value + if item.key is not None and item.key != CONFIG_KEY: + config_value = getattr(self._config, item.key) + if config_value is not None: + item.value = config_value - def _sync_all_to_ds(self) -> None: + def sync_all_to_config(self) -> None: for item in self._menu_item_group.menu_items: if item.key: - self._data_store[item.key] = item.value + setattr(self._config, item.key, item.value) def _sync(self, item: MenuItem) -> None: - if not item.key: + if not item.key or item.key == CONFIG_KEY: return - store_value = self._data_store.get(item.key, None) + config_value = getattr(self._config, item.key) - if store_value is not None: - item.value = store_value + if config_value is not None: + item.value = config_value elif item.value is not None: - self._data_store[item.key] = item.value + setattr(self._config, item.key, item.value) def set_enabled(self, key: str, enabled: bool) -> None: - if (item := self._menu_item_group.find_by_key(key)) is not None: - item.enabled = enabled - return None + # the __config__ is associated with multiple items + found = False + for item in self._menu_item_group.items: + if item.key == key: + item.enabled = enabled + found = True - raise ValueError(f'No selector found: {key}') + if not found: + raise ValueError(f'No selector found: {key}') def disable_all(self) -> None: for item in self._menu_item_group.items: item.enabled = False def run(self) -> Any | None: - self._sync_all_from_ds() + self._sync_from_config() while True: result = SelectMenu( @@ -100,10 +108,9 @@ class AbstractMenu: if item.action is None: break case ResultType.Reset: - self._data_store = {} return None - self._sync_all_to_ds() + self.sync_all_to_config() return None @@ -111,7 +118,7 @@ class AbstractSubMenu(AbstractMenu): def __init__( self, item_group: MenuItemGroup, - data_store: dict[str, Any], + config: Any, auto_cursor: bool = True, allow_reset: bool = False ): @@ -120,7 +127,7 @@ class AbstractSubMenu(AbstractMenu): super().__init__( item_group, - data_store=data_store, + config=config, auto_cursor=auto_cursor, allow_reset=allow_reset ) diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index b92bc985..0a40fc46 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -1,11 +1,9 @@ -from dataclasses import dataclass, field -from enum import Enum -from typing import TYPE_CHECKING, Any, override +from typing import TYPE_CHECKING, override from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, ResultType, SelectMenu from .menu import AbstractSubMenu, ListManager -from .models.mirrors import MirrorRegion, mirror_list_handler +from .models.mirrors import CustomMirror, MirrorConfiguration, MirrorRegion, SignCheck, SignOption, mirror_list_handler from .output import FormattedOutput if TYPE_CHECKING: @@ -16,118 +14,6 @@ if TYPE_CHECKING: _: Callable[[str], DeferredTranslation] -class SignCheck(Enum): - Never = 'Never' - Optional = 'Optional' - Required = 'Required' - - -class SignOption(Enum): - TrustedOnly = 'TrustedOnly' - TrustAll = 'TrustAll' - - -@dataclass -class CustomMirror: - name: str - url: str - sign_check: SignCheck - sign_option: SignOption - - def table_data(self) -> dict[str, str]: - return { - 'Name': self.name, - 'Url': self.url, - 'Sign check': self.sign_check.value, - 'Sign options': self.sign_option.value - } - - def json(self) -> dict[str, str]: - return { - 'name': self.name, - 'url': self.url, - 'sign_check': self.sign_check.value, - 'sign_option': self.sign_option.value - } - - @classmethod - def parse_args(cls, args: list[dict[str, str]]) -> list['CustomMirror']: - configs = [] - for arg in args: - configs.append( - CustomMirror( - arg['name'], - arg['url'], - SignCheck(arg['sign_check']), - SignOption(arg['sign_option']) - ) - ) - - return configs - - -@dataclass -class MirrorConfiguration: - mirror_regions: list[MirrorRegion] = field(default_factory=list) - custom_mirrors: list[CustomMirror] = field(default_factory=list) - - @property - def regions(self) -> str: - return ', '.join([m.name for m in self.mirror_regions]) - - def json(self) -> dict[str, Any]: - regions = {} - for m in self.mirror_regions: - regions.update(m.json()) - - return { - 'mirror_regions': regions, - 'custom_mirrors': [c.json() for c in self.custom_mirrors] - } - - def mirrorlist_config(self, speed_sort: bool = True) -> str: - config = '' - - for mirror_region in self.mirror_regions: - sorted_stati = mirror_list_handler.get_status_by_region( - mirror_region.name, - speed_sort=speed_sort - ) - - config += f'\n\n## {mirror_region.name}\n' - - for status in sorted_stati: - config += f'Server = {status.server_url}\n' - - for cm in self.custom_mirrors: - config += f'\n\n## {cm.name}\nServer = {cm.url}\n' - - return config - - def pacman_config(self) -> str: - config = '' - - for mirror in self.custom_mirrors: - config += f'\n\n[{mirror.name}]\n' - config += f'SigLevel = {mirror.sign_check.value} {mirror.sign_option.value}\n' - config += f'Server = {mirror.url}\n' - - return config - - @classmethod - def parse_args(cls, args: dict[str, Any]) -> 'MirrorConfiguration': - config = MirrorConfiguration() - - if 'mirror_regions' in args: - for region, urls in args['mirror_regions'].items(): - config.mirror_regions.append(MirrorRegion(region, urls)) - - if 'custom_mirrors' in args: - config.custom_mirrors = CustomMirror.parse_args(args['custom_mirrors']) - - return config - - class CustomMirrorList(ListManager): def __init__(self, custom_mirrors: list[CustomMirror]): self._actions = [ @@ -260,12 +146,14 @@ class MirrorMenu(AbstractSubMenu): else: self._mirror_config = MirrorConfiguration() - self._data_store: dict[str, Any] = {} - menu_optioons = self._define_menu_options() self._item_group = MenuItemGroup(menu_optioons, checkmarks=True) - super().__init__(self._item_group, data_store=self._data_store, allow_reset=True) + super().__init__( + self._item_group, + config=self._mirror_config, + allow_reset=True + ) def _define_menu_options(self) -> list[MenuItem]: return [ @@ -310,14 +198,7 @@ class MirrorMenu(AbstractSubMenu): @override def run(self) -> MirrorConfiguration: super().run() - - if not self._data_store: - return MirrorConfiguration() - - return MirrorConfiguration( - mirror_regions=self._data_store.get('mirror_regions', None), - custom_mirrors=self._data_store.get('custom_mirrors', None), - ) + return self._mirror_config def select_mirror_regions(preset: list[MirrorRegion]) -> list[MirrorRegion]: diff --git a/archinstall/lib/models/__init__.py b/archinstall/lib/models/__init__.py index 209a0d49..fd21d452 100644 --- a/archinstall/lib/models/__init__.py +++ b/archinstall/lib/models/__init__.py @@ -1,5 +1,35 @@ from .audio_configuration import Audio, AudioConfiguration from .bootloader import Bootloader +from .device_model import ( + BDevice, + DeviceGeometry, + DeviceModification, + DiskEncryption, + DiskLayoutConfiguration, + DiskLayoutType, + EncryptionType, + Fido2Device, + FilesystemType, + LsblkInfo, + LvmConfiguration, + LvmLayoutType, + LvmVolume, + LvmVolumeGroup, + LvmVolumeStatus, + ModificationStatus, + PartitionFlag, + PartitionModification, + PartitionTable, + PartitionType, + SectorSize, + Size, + SubvolumeModification, + Unit, + _DeviceInfo, +) from .gen import LocalPackage, PackageSearch, PackageSearchResult +from .locale import LocaleConfiguration +from .mirrors import CustomMirror, MirrorConfiguration, MirrorRegion from .network_configuration import NetworkConfiguration, Nic, NicType +from .profile_model import ProfileConfiguration from .users import PasswordStrength, User diff --git a/archinstall/lib/models/audio_configuration.py b/archinstall/lib/models/audio_configuration.py index 1d0c1900..f8f37553 100644 --- a/archinstall/lib/models/audio_configuration.py +++ b/archinstall/lib/models/audio_configuration.py @@ -1,11 +1,13 @@ from dataclasses import dataclass from enum import StrEnum, auto +from typing import TYPE_CHECKING -from ...default_profiles.applications.pipewire import PipewireProfile from ..hardware import SysInfo -from ..installer import Installer from ..output import info +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + class Audio(StrEnum): NO_AUDIO = 'No audio server' @@ -30,10 +32,12 @@ class AudioConfiguration: def install_audio_config( self, - installation: Installer + installation: 'Installer' ) -> None: info(f'Installing audio server: {self.audio.name}') + from ...default_profiles.applications.pipewire import PipewireProfile + match self.audio: case Audio.PIPEWIRE: PipewireProfile().install(installation) diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/models/device_model.py similarity index 94% rename from archinstall/lib/disk/device_model.py rename to archinstall/lib/models/device_model.py index 8f10058e..c3cc6df4 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/models/device_model.py @@ -11,11 +11,8 @@ import parted from parted import Disk, Geometry, Partition from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator -from ..exceptions import DiskError, SysCallError -from ..general import SysCommand from ..hardware import SysInfo from ..output import debug -from ..storage import storage if TYPE_CHECKING: from collections.abc import Callable @@ -78,7 +75,7 @@ class DiskLayoutConfiguration: @classmethod def parse_arg(cls, disk_config: _DiskLayoutConfigurationSerialization) -> DiskLayoutConfiguration | None: - from .device_handler import device_handler + from archinstall.lib.disk.device_handler import device_handler device_modifications: list[DeviceModification] = [] config_type = disk_config.get('config_type', None) @@ -100,8 +97,6 @@ class DiskLayoutConfiguration: mods = device_handler.detect_pre_mounted_mods(path) device_modifications.extend(mods) - storage['arguments']['mount_point'] = path - config.mountpoint = path return config @@ -1598,96 +1593,3 @@ class LsblkInfo(BaseModel): for name, field in cls.model_fields.items() if name != 'children' ] - - -class LsblkOutput(BaseModel): - blockdevices: list[LsblkInfo] - - -def _fetch_lsblk_info( - dev_path: Path | str | None = None, - reverse: bool = False, - full_dev_path: bool = False -) -> LsblkOutput: - cmd = ['lsblk', '--json', '--bytes', '--output', ','.join(LsblkInfo.fields())] - - if reverse: - cmd.append('--inverse') - - if full_dev_path: - cmd.append('--paths') - - if dev_path: - cmd.append(str(dev_path)) - - try: - worker = SysCommand(cmd) - except SysCallError as err: - # Get the output minus the message/info from lsblk if it returns a non-zero exit code. - if err.worker: - err_str = err.worker.decode() - debug(f'Error calling lsblk: {err_str}') - - if dev_path: - raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') - - raise err - - output = worker.output(remove_cr=False) - return LsblkOutput.model_validate_json(output) - - -def get_lsblk_info( - dev_path: Path | str, - reverse: bool = False, - full_dev_path: bool = False -) -> LsblkInfo: - infos = _fetch_lsblk_info(dev_path, reverse=reverse, full_dev_path=full_dev_path) - - if infos.blockdevices: - return infos.blockdevices[0] - - raise DiskError(f'lsblk failed to retrieve information for "{dev_path}"') - - -def get_all_lsblk_info() -> list[LsblkInfo]: - return _fetch_lsblk_info().blockdevices - - -def get_lsblk_output() -> LsblkOutput: - return _fetch_lsblk_info() - - -def find_lsblk_info( - dev_path: Path | str, - info: list[LsblkInfo] -) -> LsblkInfo | None: - if isinstance(dev_path, str): - dev_path = Path(dev_path) - - for lsblk_info in info: - if lsblk_info.path == dev_path: - return lsblk_info - - return None - - -def get_lsblk_by_mountpoint(mountpoint: Path, as_prefix: bool = False) -> list[LsblkInfo]: - def _check(infos: list[LsblkInfo]) -> list[LsblkInfo]: - devices = [] - for entry in infos: - if as_prefix: - matches = [m for m in entry.mountpoints if str(m).startswith(str(mountpoint))] - if matches: - devices += [entry] - elif mountpoint in entry.mountpoints: - devices += [entry] - - if len(entry.children) > 0: - if len(match := _check(entry.children)) > 0: - devices += match - - return devices - - all_info = get_all_lsblk_info() - return _check(all_info) diff --git a/archinstall/lib/models/locale.py b/archinstall/lib/models/locale.py new file mode 100644 index 00000000..006dc9bb --- /dev/null +++ b/archinstall/lib/models/locale.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from ..locale.utils import get_kb_layout + +if TYPE_CHECKING: + from collections.abc import Callable + + from archinstall.lib.translationhandler import DeferredTranslation + + _: Callable[[str], DeferredTranslation] + + +@dataclass +class LocaleConfiguration: + kb_layout: str + sys_lang: str + sys_enc: str + + @staticmethod + def default() -> 'LocaleConfiguration': + layout = get_kb_layout() + if layout == "": + return LocaleConfiguration('us', 'en_US', 'UTF-8') + return LocaleConfiguration(layout, 'en_US', 'UTF-8') + + def json(self) -> dict[str, str]: + return { + 'kb_layout': self.kb_layout, + 'sys_lang': self.sys_lang, + 'sys_enc': self.sys_enc + } + + def preview(self) -> str: + output = '{}: {}\n'.format(str(_('Keyboard layout')), self.kb_layout) + output += '{}: {}\n'.format(str(_('Locale language')), self.sys_lang) + output += '{}: {}'.format(str(_('Locale encoding')), self.sys_enc) + return output + + @classmethod + def _load_config(cls, config: 'LocaleConfiguration', args: dict[str, Any]) -> 'LocaleConfiguration': + if 'sys_lang' in args: + config.sys_lang = args['sys_lang'] + if 'sys_enc' in args: + config.sys_enc = args['sys_enc'] + if 'kb_layout' in args: + config.kb_layout = args['kb_layout'] + + return config + + @classmethod + def parse_arg(cls, args: dict[str, Any]) -> 'LocaleConfiguration': + default = cls.default() + + if 'locale_config' in args: + default = cls._load_config(default, args['locale_config']) + else: + default = cls._load_config(default, args) + + return default diff --git a/archinstall/lib/models/mirrors.py b/archinstall/lib/models/mirrors.py index fcaa42f5..6dccdfe9 100644 --- a/archinstall/lib/models/mirrors.py +++ b/archinstall/lib/models/mirrors.py @@ -4,14 +4,22 @@ import time import urllib.error import urllib.parse import urllib.request -from dataclasses import dataclass +from dataclasses import dataclass, field +from enum import Enum from pathlib import Path +from typing import TYPE_CHECKING, Any from pydantic import BaseModel, field_validator, model_validator from ..networking import DownloadTimer, fetch_data_from_url, ping from ..output import debug -from ..storage import storage + +if TYPE_CHECKING: + from collections.abc import Callable + + from archinstall.lib.translationhandler import DeferredTranslation + + _: Callable[[str], DeferredTranslation] class MirrorStatusEntryV3(BaseModel): @@ -171,7 +179,8 @@ class MirrorListHandler: return available_mirrors def load_mirrors(self) -> None: - if storage['arguments']['offline']: + from ..args import arch_config_handler + if arch_config_handler.args.offline: self.load_local_mirrors() else: if not self.load_remote_mirrors(): @@ -281,3 +290,116 @@ class MirrorListHandler: mirror_list_handler = MirrorListHandler() + + +class SignCheck(Enum): + Never = 'Never' + Optional = 'Optional' + Required = 'Required' + + +class SignOption(Enum): + TrustedOnly = 'TrustedOnly' + TrustAll = 'TrustAll' + + +@dataclass +class CustomMirror: + name: str + url: str + sign_check: SignCheck + sign_option: SignOption + + def table_data(self) -> dict[str, str]: + return { + 'Name': self.name, + 'Url': self.url, + 'Sign check': self.sign_check.value, + 'Sign options': self.sign_option.value + } + + def json(self) -> dict[str, str]: + return { + 'name': self.name, + 'url': self.url, + 'sign_check': self.sign_check.value, + 'sign_option': self.sign_option.value + } + + @classmethod + def parse_args(cls, args: list[dict[str, str]]) -> list['CustomMirror']: + configs = [] + for arg in args: + configs.append( + CustomMirror( + arg['name'], + arg['url'], + SignCheck(arg['sign_check']), + SignOption(arg['sign_option']) + ) + ) + + return configs + + +@dataclass +class MirrorConfiguration: + mirror_regions: list[MirrorRegion] = field(default_factory=list) + custom_mirrors: list[CustomMirror] = field(default_factory=list) + + @property + def regions(self) -> str: + return ', '.join([m.name for m in self.mirror_regions]) + + def json(self) -> dict[str, Any]: + regions = {} + for m in self.mirror_regions: + regions.update(m.json()) + + return { + 'mirror_regions': regions, + 'custom_mirrors': [c.json() for c in self.custom_mirrors] + } + + def mirrorlist_config(self, speed_sort: bool = True) -> str: + config = '' + + for mirror_region in self.mirror_regions: + sorted_stati = mirror_list_handler.get_status_by_region( + mirror_region.name, + speed_sort=speed_sort + ) + + config += f'\n\n## {mirror_region.name}\n' + + for status in sorted_stati: + config += f'Server = {status.server_url}\n' + + for cm in self.custom_mirrors: + config += f'\n\n## {cm.name}\nServer = {cm.url}\n' + + return config + + def pacman_config(self) -> str: + config = '' + + for mirror in self.custom_mirrors: + config += f'\n\n[{mirror.name}]\n' + config += f'SigLevel = {mirror.sign_check.value} {mirror.sign_option.value}\n' + config += f'Server = {mirror.url}\n' + + return config + + @classmethod + def parse_args(cls, args: dict[str, Any]) -> 'MirrorConfiguration': + config = MirrorConfiguration() + + mirror_regions = args.get('mirror_regions', []) + if mirror_regions: + for region, urls in mirror_regions.items(): + config.mirror_regions.append(MirrorRegion(region, urls)) + + if 'custom_mirrors' in args: + config.custom_mirrors = CustomMirror.parse_args(args['custom_mirrors']) + + return config diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index 2c4cc432..5a95ff13 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field from enum import Enum from typing import TYPE_CHECKING, Any -from ..profile import ProfileConfiguration +from ..models.profile_model import ProfileConfiguration if TYPE_CHECKING: from collections.abc import Callable diff --git a/archinstall/lib/profile/profile_model.py b/archinstall/lib/models/profile_model.py similarity index 89% rename from archinstall/lib/profile/profile_model.py rename to archinstall/lib/models/profile_model.py index 846446df..74361358 100644 --- a/archinstall/lib/profile/profile_model.py +++ b/archinstall/lib/models/profile_model.py @@ -15,7 +15,7 @@ class ProfileConfiguration: greeter: GreeterType | None = None def json(self) -> dict[str, Any]: - from .profiles_handler import profile_handler + from ..profile.profiles_handler import profile_handler return { 'profile': profile_handler.to_json(self.profile), 'gfx_driver': self.gfx_driver.value if self.gfx_driver else None, @@ -24,8 +24,7 @@ class ProfileConfiguration: @classmethod def parse_arg(cls, arg: dict[str, Any]) -> 'ProfileConfiguration': - from .profiles_handler import profile_handler - + from ..profile.profiles_handler import profile_handler profile = profile_handler.parse_profile_config(arg['profile']) greeter = arg.get('greeter', None) gfx_driver = arg.get('gfx_driver', None) diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index bed2959c..541d8fb5 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -322,7 +322,7 @@ def log( Journald.log(text, level=level) - if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False): + if level != logging.DEBUG: from archinstall.tui import Tui Tui.print(text) diff --git a/archinstall/lib/profile/__init__.py b/archinstall/lib/profile/__init__.py index 2558856e..7dc28c4a 100644 --- a/archinstall/lib/profile/__init__.py +++ b/archinstall/lib/profile/__init__.py @@ -1,3 +1,2 @@ from .profile_menu import ProfileMenu, select_greeter, select_profile -from .profile_model import ProfileConfiguration from .profiles_handler import profile_handler diff --git a/archinstall/lib/profile/profile_menu.py b/archinstall/lib/profile/profile_menu.py index afb19707..6898154d 100644 --- a/archinstall/lib/profile/profile_menu.py +++ b/archinstall/lib/profile/profile_menu.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, override +from typing import TYPE_CHECKING, override from archinstall.default_profiles.profile import GreeterType, Profile from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, Orientation, ResultType, SelectMenu @@ -8,7 +8,7 @@ from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, from ..hardware import GfxDriver from ..interactions.system_conf import select_driver from ..menu import AbstractSubMenu -from .profile_model import ProfileConfiguration +from ..models.profile_model import ProfileConfiguration if TYPE_CHECKING: from collections.abc import Callable @@ -24,40 +24,42 @@ class ProfileMenu(AbstractSubMenu): preset: ProfileConfiguration | None = None ): if preset: - self._preset = preset + self._profile_config = preset else: - self._preset = ProfileConfiguration() - - self._data_store: dict[str, Any] = {} + self._profile_config = ProfileConfiguration() menu_optioons = self._define_menu_options() self._item_group = MenuItemGroup(menu_optioons, checkmarks=True) - super().__init__(self._item_group, data_store=self._data_store, allow_reset=True) + super().__init__( + self._item_group, + self._profile_config, + allow_reset=True + ) def _define_menu_options(self) -> list[MenuItem]: return [ MenuItem( text=str(_('Type')), action=self._select_profile, - value=self._preset.profile, + value=self._profile_config.profile, preview_action=self._preview_profile, key='profile' ), MenuItem( text=str(_('Graphics driver')), action=self._select_gfx_driver, - value=self._preset.gfx_driver if self._preset.profile and self._preset.profile.is_graphic_driver_supported() else None, + value=self._profile_config.gfx_driver if self._profile_config.profile and self._profile_config.profile.is_graphic_driver_supported() else None, preview_action=self._prev_gfx, - enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False, + enabled=self._profile_config.profile.is_graphic_driver_supported() if self._profile_config.profile else False, dependencies=['profile'], key='gfx_driver', ), MenuItem( text=str(_('Greeter')), action=lambda x: select_greeter(preset=x), - value=self._preset.greeter if self._preset.profile and self._preset.profile.is_greeter_supported() else None, - enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False, + value=self._profile_config.greeter if self._profile_config.profile and self._profile_config.profile.is_greeter_supported() else None, + enabled=self._profile_config.profile.is_graphic_driver_supported() if self._profile_config.profile else False, preview_action=self._prev_greeter, dependencies=['profile'], key='greeter', @@ -67,15 +69,7 @@ class ProfileMenu(AbstractSubMenu): @override def run(self) -> ProfileConfiguration | None: super().run() - - if self._data_store.get('profile', None): - return ProfileConfiguration( - self._data_store.get('profile', None), - self._data_store.get('gfx_driver', None), - self._data_store.get('greeter', None), - ) - - return None + return self._profile_config def _select_profile(self, preset: Profile | None) -> Profile | None: profile = select_profile(preset) diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index f5bbde17..8a1a5773 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -12,9 +12,9 @@ from typing import TYPE_CHECKING, Any from ...default_profiles.profile import GreeterType, Profile from ..hardware import GfxDriver +from ..models.profile_model import ProfileConfiguration from ..networking import fetch_data_from_url, list_interfaces from ..output import debug, error, info -from .profile_model import ProfileConfiguration if TYPE_CHECKING: from collections.abc import Callable diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 2938336a..c2e0f61f 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -1,21 +1,25 @@ from pathlib import Path -import archinstall -from archinstall import SysInfo, debug, info -from archinstall.lib import disk, locale +from archinstall import SysInfo +from archinstall.lib.args import ArchConfig, arch_config_handler from archinstall.lib.configuration import ConfigurationOutput +from archinstall.lib.disk.filesystem import FilesystemHandler +from archinstall.lib.disk.utils import disk_layouts +from archinstall.lib.general import run_custom_user_commands from archinstall.lib.global_menu import GlobalMenu -from archinstall.lib.installer import Installer +from archinstall.lib.installer import Installer, accessibility_tools_in_use from archinstall.lib.interactions.general_conf import ask_chroot from archinstall.lib.models import AudioConfiguration, Bootloader +from archinstall.lib.models.device_model import ( + DiskLayoutConfiguration, + DiskLayoutType, + EncryptionType, +) from archinstall.lib.models.network_configuration import NetworkConfiguration +from archinstall.lib.output import debug, error, info from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.tui import Tui -if archinstall.arguments.get('help'): - print("See `man archinstall` for help.") - exit(0) - def ask_user_questions() -> None: """ @@ -25,10 +29,10 @@ def ask_user_questions() -> None: """ with Tui(): - global_menu = GlobalMenu(data_store=archinstall.arguments) + global_menu = GlobalMenu(arch_config_handler.config) - if not archinstall.arguments.get('advanced', False): - global_menu.set_enabled('parallel downloads', False) + if not arch_config_handler.args.advanced: + global_menu.set_enabled('parallel_downloads', False) global_menu.run() @@ -40,111 +44,114 @@ def perform_installation(mountpoint: Path) -> None: formatted and setup prior to entering this function. """ info('Starting installation...') - disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + + config: ArchConfig = arch_config_handler.config + + if not config.disk_config: + error("No disk configuration provided") + return # Retrieve list of additional repositories and set boolean values appropriately - enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) - enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) - run_mkinitcpio = not archinstall.arguments.get('uki') - locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config'] - disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + disk_config: DiskLayoutConfiguration = config.disk_config + enable_testing = 'testing' in config.additional_repositories + enable_multilib = 'multilib' in config.additional_repositories + run_mkinitcpio = not config.uki + locale_config = config.locale_config + disk_encryption = config.disk_encryption with Installer( mountpoint, disk_config, disk_encryption=disk_encryption, - kernels=archinstall.arguments.get('kernels', ['linux']) + kernels=config.kernels ) as installation: # Mount all the drives to the desired mountpoint - if disk_config.config_type != disk.DiskLayoutType.Pre_mount: + if disk_config.config_type != DiskLayoutType.Pre_mount: installation.mount_ordered_layout() installation.sanity_check() - if disk_config.config_type != disk.DiskLayoutType.Pre_mount: - if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption: + if disk_config.config_type != DiskLayoutType.Pre_mount: + if disk_encryption and disk_encryption.encryption_type != EncryptionType.NoEncryption: # generate encryption key files for the mounted luks devices installation.generate_key_files() - if mirror_config := archinstall.arguments.get('mirror_config', None): + if mirror_config := config.mirror_config: installation.set_mirrors(mirror_config, on_target=False) installation.minimal_installation( testing=enable_testing, multilib=enable_multilib, mkinitcpio=run_mkinitcpio, - hostname=archinstall.arguments.get('hostname'), + hostname=arch_config_handler.config.hostname, locale_config=locale_config ) - if mirror_config := archinstall.arguments.get('mirror_config', None): + if mirror_config := config.mirror_config: installation.set_mirrors(mirror_config, on_target=True) - if archinstall.arguments.get('swap'): + if config.swap: installation.setup_swap('zram') - if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi(): + if config.bootloader == Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages("grub") - installation.add_bootloader( - archinstall.arguments["bootloader"], - archinstall.arguments.get('uki', False) - ) + installation.add_bootloader(config.bootloader, config.uki) # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config: NetworkConfiguration | None = archinstall.arguments.get('network_config', None) + network_config: NetworkConfiguration | None = config.network_config if network_config: network_config.install_network_config( installation, - archinstall.arguments.get('profile_config', None) + config.profile_config ) - if users := archinstall.arguments.get('!users', None): + if users := config.users: installation.create_users(users) - audio_config: AudioConfiguration | None = archinstall.arguments.get('audio_config', None) + audio_config: AudioConfiguration | None = config.audio_config if audio_config: audio_config.install_audio_config(installation) else: info("No audio server will be installed") - if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', None)) + if config.packages and config.packages[0] != '': + installation.add_additional_packages(config.packages) - if profile_config := archinstall.arguments.get('profile_config', None): + if profile_config := config.profile_config: profile_handler.install_profile_config(installation, profile_config) - if timezone := archinstall.arguments.get('timezone', None): + if timezone := config.timezone: installation.set_timezone(timezone) - if archinstall.arguments.get('ntp', False): + if config.ntp: installation.activate_time_synchronization() - if archinstall.accessibility_tools_in_use(): + if accessibility_tools_in_use(): installation.enable_espeakup() - if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): + if (root_pw := config.root_password) and len(root_pw): installation.user_set_pw('root', root_pw) - if profile_config := archinstall.arguments.get('profile_config', None): + if (profile_config := config.profile_config) and profile_config.profile: profile_config.profile.post_install(installation) # If the user provided a list of services to be enabled, pass the list to the enable_service function. # Note that while it's called enable_service, it can actually take a list of services and iterate it. - if archinstall.arguments.get('services', None): - installation.enable_service(archinstall.arguments.get('services', [])) + if servies := config.services: + installation.enable_service(servies) # If the user provided custom commands to be run post-installation, execute them now. - if archinstall.arguments.get('custom-commands', None): - archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation) + if cc := config.custom_commands: + run_custom_user_commands(cc, installation) installation.genfstab() info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation") - if not archinstall.arguments.get('silent'): + if not arch_config_handler.args.silent: with Tui(): chroot = ask_chroot() @@ -154,33 +161,35 @@ def perform_installation(mountpoint: Path) -> None: except Exception: pass - debug(f"Disk states after installing:\n{disk.disk_layouts()}") + debug(f"Disk states after installing:\n{disk_layouts()}") def guided() -> None: - if not archinstall.arguments.get('silent'): + if not arch_config_handler.args.silent: ask_user_questions() - config = ConfigurationOutput(archinstall.arguments) + config = ConfigurationOutput(arch_config_handler.config) config.write_debug() config.save() - if archinstall.arguments.get('dry_run'): + if arch_config_handler.args.dry_run: exit(0) - if not archinstall.arguments.get('silent'): + if not arch_config_handler.args.silent: with Tui(): if not config.confirm_config(): debug('Installation aborted') guided() - fs_handler = disk.FilesystemHandler( - archinstall.arguments['disk_config'], - archinstall.arguments.get('disk_encryption', None) - ) + if arch_config_handler.config.disk_config: + fs_handler = FilesystemHandler( + arch_config_handler.config.disk_config, + arch_config_handler.config.disk_encryption + ) - fs_handler.perform_filesystem_operations() - perform_installation(archinstall.arguments.get('mount_point', Path('/mnt'))) + fs_handler.perform_filesystem_operations() + + perform_installation(arch_config_handler.args.mountpoint) guided() diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py index e287f780..2b3bd8e6 100644 --- a/archinstall/scripts/minimal.py +++ b/archinstall/scripts/minimal.py @@ -1,32 +1,38 @@ from pathlib import Path -import archinstall -from archinstall import ConfigurationOutput, Installer, debug, info from archinstall.default_profiles.minimal import MinimalProfile -from archinstall.lib import disk -from archinstall.lib.interactions import select_devices, suggest_single_disk_layout +from archinstall.lib.args import ArchConfig, arch_config_handler +from archinstall.lib.configuration import ConfigurationOutput +from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu +from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu +from archinstall.lib.disk.filesystem import FilesystemHandler +from archinstall.lib.installer import Installer from archinstall.lib.models import Bootloader, User -from archinstall.lib.profile import ProfileConfiguration, profile_handler +from archinstall.lib.models.device_model import ( + DiskLayoutConfiguration, +) +from archinstall.lib.models.network_configuration import NetworkConfiguration +from archinstall.lib.models.profile_model import ProfileConfiguration +from archinstall.lib.output import debug, error, info +from archinstall.lib.profile import profile_handler from archinstall.tui import Tui -info("Minimal only supports:") -info(" * Being installed to a single disk") - -if archinstall.arguments.get('help', None): - info(" - Optional disk encryption via --!encryption-password=") - info(" - Optional filesystem type via --filesystem=") - info(" - Optional systemd network via --network") - def perform_installation(mountpoint: Path) -> None: - disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] - disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + config: ArchConfig = arch_config_handler.config + + if not config.disk_config: + error("No disk configuration provided") + return + + disk_config: DiskLayoutConfiguration = config.disk_config + disk_encryption = config.disk_encryption with Installer( mountpoint, disk_config, disk_encryption=disk_encryption, - kernels=archinstall.arguments.get('kernels', ['linux']) + kernels=config.kernels ) as installation: # Strap in the base system, add a boot loader and configure # some other minor details as specified by this profile and user. @@ -34,9 +40,13 @@ def perform_installation(mountpoint: Path) -> None: installation.set_hostname('minimal-arch') installation.add_bootloader(Bootloader.Systemd) - # Optionally enable networking: - if archinstall.arguments.get('network', None): - installation.copy_iso_network_config(enable_services=True) + network_config: NetworkConfiguration | None = config.network_config + + if network_config: + network_config.install_network_config( + installation, + config.profile_config + ) installation.add_additional_packages(['nano', 'wget', 'git']) @@ -53,61 +63,39 @@ def perform_installation(mountpoint: Path) -> None: info(" * devel (password: devel)") -def prompt_disk_layout() -> None: - fs_type = None - if filesystem := archinstall.arguments.get('filesystem', None): - fs_type = disk.FilesystemType(filesystem) - - devices = select_devices() - modifications = suggest_single_disk_layout(devices[0], filesystem_type=fs_type) - - archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Default, - device_modifications=[modifications] - ) - - -def parse_disk_encryption() -> None: - if enc_password := archinstall.arguments.get('!encryption-password', None): - modification: list[disk.DeviceModification] = archinstall.arguments['disk_config'] - partitions: list[disk.PartitionModification] = [] - - # encrypt all partitions except the /boot - for mod in modification: - partitions += [p for p in mod.partitions if p.mountpoint != Path('/boot')] - - archinstall.arguments['disk_encryption'] = disk.DiskEncryption( - encryption_type=disk.EncryptionType.Luks, - encryption_password=enc_password, - partitions=partitions - ) - - -def minimal() -> None: +def _minimal() -> None: with Tui(): - prompt_disk_layout() - parse_disk_encryption() + disk_config = DiskLayoutConfigurationMenu(disk_layout_config=None).run() - config = ConfigurationOutput(archinstall.arguments) + disk_encryption = None + if disk_config: + disk_encryption = DiskEncryptionMenu(disk_config).run() + + arch_config_handler.config.disk_config = disk_config + arch_config_handler.config.disk_encryption = disk_encryption + + config = ConfigurationOutput(arch_config_handler.config) config.write_debug() config.save() - if archinstall.arguments.get('dry_run'): + if arch_config_handler.args.dry_run: exit(0) - if not archinstall.arguments.get('silent'): + if not arch_config_handler.args.silent: with Tui(): if not config.confirm_config(): debug('Installation aborted') - minimal() + _minimal() - fs_handler = disk.FilesystemHandler( - archinstall.arguments['disk_config'], - archinstall.arguments.get('disk_encryption', None) - ) + if arch_config_handler.config.disk_config: + fs_handler = FilesystemHandler( + arch_config_handler.config.disk_config, + arch_config_handler.config.disk_encryption + ) - fs_handler.perform_filesystem_operations() - perform_installation(archinstall.arguments.get('mount_point', Path('/mnt'))) + fs_handler.perform_filesystem_operations() + + perform_installation(arch_config_handler.args.mountpoint) -minimal() +_minimal() diff --git a/archinstall/scripts/only_hd.py b/archinstall/scripts/only_hd.py index 3188f535..c2134317 100644 --- a/archinstall/scripts/only_hd.py +++ b/archinstall/scripts/only_hd.py @@ -1,25 +1,28 @@ from pathlib import Path -import archinstall -from archinstall import debug -from archinstall.lib import disk +from archinstall import debug, error +from archinstall.lib.args import ArchConfig, arch_config_handler from archinstall.lib.configuration import ConfigurationOutput +from archinstall.lib.disk.filesystem import FilesystemHandler +from archinstall.lib.disk.utils import disk_layouts +from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer +from archinstall.lib.models.device_model import ( + DiskLayoutConfiguration, +) from archinstall.tui import Tui def ask_user_questions() -> None: with Tui(): - global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments) + global_menu = GlobalMenu(arch_config_handler.config) global_menu.disable_all() - global_menu.set_enabled('archinstall-language', True) + global_menu.set_enabled('archinstall_language', True) global_menu.set_enabled('disk_config', True) global_menu.set_enabled('disk_encryption', True) global_menu.set_enabled('swap', True) - global_menu.set_enabled('save_config', True) - global_menu.set_enabled('install', True) - global_menu.set_enabled('abort', True) + global_menu.set_enabled('__config__', True) global_menu.run() @@ -30,18 +33,24 @@ def perform_installation(mountpoint: Path) -> None: Only requirement is that the block devices are formatted and setup prior to entering this function. """ - disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] - disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + config: ArchConfig = arch_config_handler.config + + if not config.disk_config: + error("No disk configuration provided") + return + + disk_config: DiskLayoutConfiguration = config.disk_config + disk_encryption = config.disk_encryption with Installer( mountpoint, disk_config, disk_encryption=disk_encryption, - kernels=archinstall.arguments.get('kernels', ['linux']) + kernels=config.kernels ) as installation: # Mount all the drives to the desired mountpoint # This *can* be done outside of the installation, but the installer can deal with it. - if archinstall.arguments.get('disk_config'): + if disk_config: installation.mount_ordered_layout() # to generate a fstab directory holder. Avoids an error on exit and at the same time checks the procedure @@ -50,33 +59,35 @@ def perform_installation(mountpoint: Path) -> None: target.parent.mkdir(parents=True) # For support reasons, we'll log the disk layout post installation (crash or no crash) - debug(f"Disk states after installing:\n{disk.disk_layouts()}") + debug(f"Disk states after installing:\n{disk_layouts()}") -def only_hd() -> None: - if not archinstall.arguments.get('silent'): +def _only_hd() -> None: + if not arch_config_handler.args.silent: ask_user_questions() - config = ConfigurationOutput(archinstall.arguments) + config = ConfigurationOutput(arch_config_handler.config) config.write_debug() config.save() - if archinstall.arguments.get('dry_run'): + if arch_config_handler.args.dry_run: exit(0) - if not archinstall.arguments.get('silent'): + if not arch_config_handler.args.silent: with Tui(): if not config.confirm_config(): debug('Installation aborted') - only_hd() + _only_hd() - fs_handler = disk.FilesystemHandler( - archinstall.arguments['disk_config'], - archinstall.arguments.get('disk_encryption', None) - ) + if arch_config_handler.config.disk_config: + fs_handler = FilesystemHandler( + arch_config_handler.config.disk_config, + arch_config_handler.config.disk_encryption + ) - fs_handler.perform_filesystem_operations() - perform_installation(archinstall.arguments.get('mount_point', Path('/mnt'))) + fs_handler.perform_filesystem_operations() + + perform_installation(arch_config_handler.args.mountpoint) -only_hd() +_only_hd() diff --git a/archinstall/scripts/unattended.py b/archinstall/scripts/unattended.py index db7c8087..b59fbb4c 100644 --- a/archinstall/scripts/unattended.py +++ b/archinstall/scripts/unattended.py @@ -1,10 +1,11 @@ import time -import archinstall -from archinstall import info, profile +from archinstall.lib.output import info +from archinstall.lib.profile.profiles_handler import profile_handler +from archinstall.lib.storage import storage from archinstall.tui import Tui -for p in profile.profile_handler.get_mac_addr_profiles(): +for p in profile_handler.get_mac_addr_profiles(): # Tailored means it's a match for this machine # based on it's MAC address (or some other criteria # that fits the requirements for this machine specifically). @@ -15,5 +16,5 @@ for p in profile.profile_handler.get_mac_addr_profiles(): Tui.print(f'{i}...') time.sleep(1) - install_session = archinstall.storage['installation_session'] + install_session = storage['installation_session'] p.install(install_session) diff --git a/examples/auto_discovery_mounted.py b/examples/auto_discovery_mounted.py index e3cb80b7..df792bb2 100644 --- a/examples/auto_discovery_mounted.py +++ b/examples/auto_discovery_mounted.py @@ -1,12 +1,13 @@ from pathlib import Path -from archinstall import disk +from archinstall.lib.disk.device_handler import device_handler +from archinstall.lib.models.device_model import DiskLayoutConfiguration, DiskLayoutType root_mount_dir = Path('/mnt/archinstall') -mods = disk.device_handler.detect_pre_mounted_mods(root_mount_dir) +mods = device_handler.detect_pre_mounted_mods(root_mount_dir) -disk_config = disk.DiskLayoutConfiguration( - disk.DiskLayoutType.Pre_mount, +disk_config = DiskLayoutConfiguration( + DiskLayoutType.Pre_mount, device_modifications=mods, ) diff --git a/examples/full_automated_installation.py b/examples/full_automated_installation.py index b54a44de..ec70dc6a 100644 --- a/examples/full_automated_installation.py +++ b/examples/full_automated_installation.py @@ -1,39 +1,58 @@ from pathlib import Path -from archinstall import Installer, disk, models, profile from archinstall.default_profiles.minimal import MinimalProfile +from archinstall.lib.disk.device_handler import device_handler +from archinstall.lib.disk.filesystem import FilesystemHandler +from archinstall.lib.installer import Installer +from archinstall.lib.models.device_model import ( + DeviceModification, + DiskEncryption, + DiskLayoutConfiguration, + DiskLayoutType, + EncryptionType, + FilesystemType, + ModificationStatus, + PartitionFlag, + PartitionModification, + PartitionType, + Size, + Unit, +) +from archinstall.lib.models.profile_model import ProfileConfiguration +from archinstall.lib.models.users import User +from archinstall.lib.profile import profile_handler # we're creating a new ext4 filesystem installation -fs_type = disk.FilesystemType('ext4') +fs_type = FilesystemType('ext4') device_path = Path('/dev/sda') # get the physical disk device -device = disk.device_handler.get_device(device_path) +device = device_handler.get_device(device_path) if not device: raise ValueError('No device found for given path') # create a new modification for the specific device -device_modification = disk.DeviceModification(device, wipe=True) +device_modification = DeviceModification(device, wipe=True) # create a new boot partition -boot_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=disk.Size(1, disk.Unit.MiB, device.device_info.sector_size), - length=disk.Size(512, disk.Unit.MiB, device.device_info.sector_size), +boot_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, + start=Size(1, Unit.MiB, device.device_info.sector_size), + length=Size(512, Unit.MiB, device.device_info.sector_size), mountpoint=Path('/boot'), - fs_type=disk.FilesystemType.Fat32, - flags=[disk.PartitionFlag.BOOT] + fs_type=FilesystemType.Fat32, + flags=[PartitionFlag.BOOT] ) device_modification.add_partition(boot_partition) # create a root partition -root_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=disk.Size(513, disk.Unit.MiB, device.device_info.sector_size), - length=disk.Size(20, disk.Unit.GiB, device.device_info.sector_size), +root_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, + start=Size(513, Unit.MiB, device.device_info.sector_size), + length=Size(20, Unit.GiB, device.device_info.sector_size), mountpoint=None, fs_type=fs_type, mount_options=[], @@ -44,9 +63,9 @@ start_home = root_partition.length length_home = device.device_info.total_size - start_home # create a new home partition -home_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, +home_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, start=start_home, length=length_home, mountpoint=Path('/home'), @@ -55,21 +74,21 @@ home_partition = disk.PartitionModification( ) device_modification.add_partition(home_partition) -disk_config = disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Default, +disk_config = DiskLayoutConfiguration( + config_type=DiskLayoutType.Default, device_modifications=[device_modification] ) # disk encryption configuration (Optional) -disk_encryption = disk.DiskEncryption( +disk_encryption = DiskEncryption( encryption_password="enc_password", - encryption_type=disk.EncryptionType.Luks, + encryption_type=EncryptionType.Luks, partitions=[home_partition], hsm_device=None ) # initiate file handler with the disk config and the optional disk encryption config -fs_handler = disk.FilesystemHandler(disk_config, disk_encryption) +fs_handler = FilesystemHandler(disk_config, disk_encryption) # perform all file operations # WARNING: this will potentially format the filesystem and delete all data @@ -89,8 +108,8 @@ with Installer( # Optionally, install a profile of choice. # In this case, we install a minimal profile that is empty -profile_config = profile.ProfileConfiguration(MinimalProfile()) -profile.profile_handler.install_profile_config(installation, profile_config) +profile_config = ProfileConfiguration(MinimalProfile()) +profile_handler.install_profile_config(installation, profile_config) -user = models.User('archinstall', 'password', True) +user = User('archinstall', 'password', True) installation.create_users(user) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index c2de1725..47409be5 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -1,21 +1,24 @@ from pathlib import Path -import archinstall -from archinstall import SysInfo, debug, info -from archinstall.lib import disk, locale +from archinstall import SysInfo, debug, error, info +from archinstall.lib.args import ArchConfig, arch_config_handler from archinstall.lib.configuration import ConfigurationOutput +from archinstall.lib.disk.filesystem import FilesystemHandler +from archinstall.lib.disk.utils import disk_layouts +from archinstall.lib.general import run_custom_user_commands from archinstall.lib.global_menu import GlobalMenu -from archinstall.lib.installer import Installer +from archinstall.lib.installer import Installer, accessibility_tools_in_use from archinstall.lib.interactions.general_conf import ask_chroot from archinstall.lib.models import AudioConfiguration, Bootloader +from archinstall.lib.models.device_model import ( + DiskLayoutConfiguration, + DiskLayoutType, + EncryptionType, +) from archinstall.lib.models.network_configuration import NetworkConfiguration from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.tui import Tui -if archinstall.arguments.get('help'): - print("See `man archinstall` for help.") - exit(0) - def ask_user_questions() -> None: """ @@ -25,10 +28,10 @@ def ask_user_questions() -> None: """ with Tui(): - global_menu = GlobalMenu(data_store=archinstall.arguments) + global_menu = GlobalMenu(arch_config_handler.config) - if not archinstall.arguments.get('advanced', False): - global_menu.set_enabled('parallel downloads', False) + if not arch_config_handler.args.advanced: + global_menu.set_enabled('parallel_downloads', False) global_menu.run() @@ -40,111 +43,114 @@ def perform_installation(mountpoint: Path) -> None: formatted and setup prior to entering this function. """ info('Starting installation...') - disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + + config: ArchConfig = arch_config_handler.config + + if not config.disk_config: + error("No disk configuration provided") + return # Retrieve list of additional repositories and set boolean values appropriately - enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) - enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) - run_mkinitcpio = not archinstall.arguments.get('uki') - locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config'] - disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + disk_config: DiskLayoutConfiguration = config.disk_config + enable_testing = 'testing' in config.additional_repositories + enable_multilib = 'multilib' in config.additional_repositories + run_mkinitcpio = not config.uki + locale_config = config.locale_config + disk_encryption = config.disk_encryption with Installer( mountpoint, disk_config, disk_encryption=disk_encryption, - kernels=archinstall.arguments.get('kernels', ['linux']) + kernels=config.kernels ) as installation: # Mount all the drives to the desired mountpoint - if disk_config.config_type != disk.DiskLayoutType.Pre_mount: + if disk_config.config_type != DiskLayoutType.Pre_mount: installation.mount_ordered_layout() installation.sanity_check() - if disk_config.config_type != disk.DiskLayoutType.Pre_mount: - if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption: + if disk_config.config_type != DiskLayoutType.Pre_mount: + if disk_encryption and disk_encryption.encryption_type != EncryptionType.NoEncryption: # generate encryption key files for the mounted luks devices installation.generate_key_files() - if mirror_config := archinstall.arguments.get('mirror_config', None): + if mirror_config := config.mirror_config: installation.set_mirrors(mirror_config, on_target=False) installation.minimal_installation( testing=enable_testing, multilib=enable_multilib, mkinitcpio=run_mkinitcpio, - hostname=archinstall.arguments.get('hostname', 'archlinux'), + hostname=arch_config_handler.config.hostname, locale_config=locale_config ) - if mirror_config := archinstall.arguments.get('mirror_config', None): + if mirror_config := config.mirror_config: installation.set_mirrors(mirror_config, on_target=True) - if archinstall.arguments.get('swap'): + if config.swap: installation.setup_swap('zram') - if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi(): + if config.bootloader == Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages("grub") - installation.add_bootloader( - archinstall.arguments["bootloader"], - archinstall.arguments.get('uki', False) - ) + installation.add_bootloader(config.bootloader, config.uki) # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config: NetworkConfiguration | None = archinstall.arguments.get('network_config', None) + network_config: NetworkConfiguration | None = config.network_config if network_config: network_config.install_network_config( installation, - archinstall.arguments.get('profile_config', None) + config.profile_config ) - if users := archinstall.arguments.get('!users', None): + if users := config.users: installation.create_users(users) - audio_config: AudioConfiguration | None = archinstall.arguments.get('audio_config', None) + audio_config: AudioConfiguration | None = config.audio_config if audio_config: audio_config.install_audio_config(installation) else: info("No audio server will be installed") - if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', None)) + if config.packages and config.packages[0] != '': + installation.add_additional_packages(config.packages) - if profile_config := archinstall.arguments.get('profile_config', None): + if profile_config := config.profile_config: profile_handler.install_profile_config(installation, profile_config) - if timezone := archinstall.arguments.get('timezone', None): + if timezone := config.timezone: installation.set_timezone(timezone) - if archinstall.arguments.get('ntp', False): + if config.ntp: installation.activate_time_synchronization() - if archinstall.accessibility_tools_in_use(): + if accessibility_tools_in_use(): installation.enable_espeakup() - if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): + if (root_pw := config.root_password) and len(root_pw): installation.user_set_pw('root', root_pw) - if profile_config := archinstall.arguments.get('profile_config', None): + if (profile_config := config.profile_config) and profile_config.profile: profile_config.profile.post_install(installation) # If the user provided a list of services to be enabled, pass the list to the enable_service function. # Note that while it's called enable_service, it can actually take a list of services and iterate it. - if archinstall.arguments.get('services', None): - installation.enable_service(archinstall.arguments.get('services', [])) + if servies := config.services: + installation.enable_service(servies) # If the user provided custom commands to be run post-installation, execute them now. - if archinstall.arguments.get('custom-commands', None): - archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation) + if cc := config.custom_commands: + run_custom_user_commands(cc, installation) installation.genfstab() info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation") - if not archinstall.arguments.get('silent'): + if not arch_config_handler.args.silent: with Tui(): chroot = ask_chroot() @@ -154,33 +160,35 @@ def perform_installation(mountpoint: Path) -> None: except Exception: pass - debug(f"Disk states after installing:\n{disk.disk_layouts()}") + debug(f"Disk states after installing:\n{disk_layouts()}") -def _guided() -> None: - if not archinstall.arguments.get('silent'): +def guided() -> None: + if not arch_config_handler.args.silent: ask_user_questions() - config = ConfigurationOutput(archinstall.arguments) + config = ConfigurationOutput(arch_config_handler.config) config.write_debug() config.save() - if archinstall.arguments.get('dry_run'): + if arch_config_handler.args.dry_run: exit(0) - if not archinstall.arguments.get('silent'): + if not arch_config_handler.args.silent: with Tui(): if not config.confirm_config(): debug('Installation aborted') - _guided() + guided() - fs_handler = disk.FilesystemHandler( - archinstall.arguments['disk_config'], - archinstall.arguments.get('disk_encryption', None) - ) + if arch_config_handler.config.disk_config: + fs_handler = FilesystemHandler( + arch_config_handler.config.disk_config, + arch_config_handler.config.disk_encryption + ) - fs_handler.perform_filesystem_operations() - perform_installation(archinstall.arguments.get('mount_point', Path('/mnt'))) + fs_handler.perform_filesystem_operations() + + perform_installation(arch_config_handler.args.mountpoint) -_guided() +guided() diff --git a/examples/mac_address_installation.py b/examples/mac_address_installation.py index afa6f457..f569477b 100644 --- a/examples/mac_address_installation.py +++ b/examples/mac_address_installation.py @@ -1,10 +1,11 @@ import time -import archinstall -from archinstall import info, profile +from archinstall.lib.output import info +from archinstall.lib.profile import profile_handler +from archinstall.lib.storage import storage from archinstall.tui import Tui -for _profile in profile.profile_handler.get_mac_addr_profiles(): +for _profile in profile_handler.get_mac_addr_profiles(): # Tailored means it's a match for this machine # based on it's MAC address (or some other criteria # that fits the requirements for this machine specifically). @@ -15,5 +16,5 @@ for _profile in profile.profile_handler.get_mac_addr_profiles(): Tui.print(f'{i}...') time.sleep(1) - install_session = archinstall.storage['installation_session'] + install_session = storage['installation_session'] _profile.install(install_session) diff --git a/examples/minimal_installation.py b/examples/minimal_installation.py index f532206e..a7171a30 100644 --- a/examples/minimal_installation.py +++ b/examples/minimal_installation.py @@ -1,32 +1,37 @@ from pathlib import Path -import archinstall -from archinstall import ConfigurationOutput, Installer, debug, info from archinstall.default_profiles.minimal import MinimalProfile -from archinstall.lib import disk -from archinstall.lib.interactions import select_devices, suggest_single_disk_layout +from archinstall.lib.args import ArchConfig, arch_config_handler +from archinstall.lib.configuration import ConfigurationOutput +from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu +from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu +from archinstall.lib.disk.filesystem import FilesystemHandler +from archinstall.lib.installer import Installer from archinstall.lib.models import Bootloader, User -from archinstall.lib.profile import ProfileConfiguration, profile_handler +from archinstall.lib.models.device_model import DiskLayoutConfiguration +from archinstall.lib.models.network_configuration import NetworkConfiguration +from archinstall.lib.models.profile_model import ProfileConfiguration +from archinstall.lib.output import debug, error, info +from archinstall.lib.profile import profile_handler from archinstall.tui import Tui -info("Minimal only supports:") -info(" * Being installed to a single disk") - -if archinstall.arguments.get('help', None): - info(" - Optional disk encryption via --!encryption-password=") - info(" - Optional filesystem type via --filesystem=") - info(" - Optional systemd network via --network") - def perform_installation(mountpoint: Path) -> None: - disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] - disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + config: ArchConfig = arch_config_handler.config + + disk_config: DiskLayoutConfiguration | None = config.disk_config + + if disk_config is None: + error("No disk configuration provided") + return + + disk_encryption = config.disk_encryption with Installer( mountpoint, disk_config, disk_encryption=disk_encryption, - kernels=archinstall.arguments.get('kernels', ['linux']) + kernels=config.kernels ) as installation: # Strap in the base system, add a boot loader and configure # some other minor details as specified by this profile and user. @@ -34,9 +39,13 @@ def perform_installation(mountpoint: Path) -> None: installation.set_hostname('minimal-arch') installation.add_bootloader(Bootloader.Systemd) - # Optionally enable networking: - if archinstall.arguments.get('network', None): - installation.copy_iso_network_config(enable_services=True) + network_config: NetworkConfiguration | None = config.network_config + + if network_config: + network_config.install_network_config( + installation, + config.profile_config + ) installation.add_additional_packages(['nano', 'wget', 'git']) @@ -53,61 +62,39 @@ def perform_installation(mountpoint: Path) -> None: info(" * devel (password: devel)") -def prompt_disk_layout() -> None: - fs_type = None - if filesystem := archinstall.arguments.get('filesystem', None): - fs_type = disk.FilesystemType(filesystem) - - devices = select_devices() - modifications = suggest_single_disk_layout(devices[0], filesystem_type=fs_type) - - archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Default, - device_modifications=[modifications] - ) - - -def parse_disk_encryption() -> None: - if enc_password := archinstall.arguments.get('!encryption-password', None): - modification: list[disk.DeviceModification] = archinstall.arguments['disk_config'] - partitions: list[disk.PartitionModification] = [] - - # encrypt all partitions except the /boot - for mod in modification: - partitions += [p for p in mod.partitions if p.mountpoint != Path('/boot')] - - archinstall.arguments['disk_encryption'] = disk.DiskEncryption( - encryption_type=disk.EncryptionType.Luks, - encryption_password=enc_password, - partitions=partitions - ) - - def _minimal() -> None: with Tui(): - prompt_disk_layout() - parse_disk_encryption() + disk_config = DiskLayoutConfigurationMenu(disk_layout_config=None).run() - config = ConfigurationOutput(archinstall.arguments) + disk_encryption = None + if disk_config: + disk_encryption = DiskEncryptionMenu(disk_config).run() + + arch_config_handler.config.disk_config = disk_config + arch_config_handler.config.disk_encryption = disk_encryption + + config = ConfigurationOutput(arch_config_handler.config) config.write_debug() config.save() - if archinstall.arguments.get('dry_run'): + if arch_config_handler.args.dry_run: exit(0) - if not archinstall.arguments.get('silent'): + if not arch_config_handler.args.silent: with Tui(): if not config.confirm_config(): debug('Installation aborted') _minimal() - fs_handler = disk.FilesystemHandler( - archinstall.arguments['disk_config'], - archinstall.arguments.get('disk_encryption', None) - ) + if arch_config_handler.config.disk_config: + fs_handler = FilesystemHandler( + arch_config_handler.config.disk_config, + arch_config_handler.config.disk_encryption + ) - fs_handler.perform_filesystem_operations() - perform_installation(archinstall.arguments.get('mount_point', Path('/mnt'))) + fs_handler.perform_filesystem_operations() + + perform_installation(arch_config_handler.args.mountpoint) _minimal() diff --git a/examples/only_hd_installation.py b/examples/only_hd_installation.py index 7c6e37b6..402468ee 100644 --- a/examples/only_hd_installation.py +++ b/examples/only_hd_installation.py @@ -1,21 +1,24 @@ from pathlib import Path -import archinstall -from archinstall import debug -from archinstall.lib import disk +from archinstall.lib.args import ArchConfig, arch_config_handler from archinstall.lib.configuration import ConfigurationOutput +from archinstall.lib.disk.filesystem import FilesystemHandler +from archinstall.lib.disk.utils import disk_layouts +from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer +from archinstall.lib.models.device_model import DiskLayoutConfiguration +from archinstall.lib.output import debug, error from archinstall.tui import Tui def ask_user_questions() -> None: with Tui(): - global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments) + global_menu = GlobalMenu(arch_config_handler.config) global_menu.disable_all() global_menu.set_enabled('archinstall-language', True) - global_menu.set_enabled('disk_config', True) - global_menu.set_enabled('disk_encryption', True) + global_menu.set_enabled('_disk_config', True) + global_menu.set_enabled('_disk_encryption', True) global_menu.set_enabled('swap', True) global_menu.set_enabled('save_config', True) global_menu.set_enabled('install', True) @@ -30,18 +33,24 @@ def perform_installation(mountpoint: Path) -> None: Only requirement is that the block devices are formatted and setup prior to entering this function. """ - disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] - disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + config: ArchConfig = arch_config_handler.config + + if not config.disk_config: + error("No disk configuration provided") + return + + disk_config: DiskLayoutConfiguration = config.disk_config + disk_encryption = config.disk_encryption with Installer( mountpoint, disk_config, disk_encryption=disk_encryption, - kernels=archinstall.arguments.get('kernels', ['linux']) + kernels=config.kernels ) as installation: # Mount all the drives to the desired mountpoint # This *can* be done outside of the installation, but the installer can deal with it. - if archinstall.arguments.get('disk_config'): + if disk_config: installation.mount_ordered_layout() # to generate a fstab directory holder. Avoids an error on exit and at the same time checks the procedure @@ -50,33 +59,35 @@ def perform_installation(mountpoint: Path) -> None: target.parent.mkdir(parents=True) # For support reasons, we'll log the disk layout post installation (crash or no crash) - debug(f"Disk states after installing:\n{disk.disk_layouts()}") + debug(f"Disk states after installing:\n{disk_layouts()}") def _only_hd() -> None: - if not archinstall.arguments.get('silent'): + if not arch_config_handler.args.silent: ask_user_questions() - config = ConfigurationOutput(archinstall.arguments) + config = ConfigurationOutput(arch_config_handler.config) config.write_debug() config.save() - if archinstall.arguments.get('dry_run'): + if arch_config_handler.args.dry_run: exit(0) - if not archinstall.arguments.get('silent'): + if not arch_config_handler.args.silent: with Tui(): if not config.confirm_config(): debug('Installation aborted') _only_hd() - fs_handler = disk.FilesystemHandler( - archinstall.arguments['disk_config'], - archinstall.arguments.get('disk_encryption', None) - ) + if arch_config_handler.config.disk_config: + fs_handler = FilesystemHandler( + arch_config_handler.config.disk_config, + arch_config_handler.config.disk_encryption + ) - fs_handler.perform_filesystem_operations() - perform_installation(archinstall.arguments.get('mount_point', Path('/mnt'))) + fs_handler.perform_filesystem_operations() + + perform_installation(arch_config_handler.args.mountpoint) _only_hd() diff --git a/pyproject.toml b/pyproject.toml index 623be55c..d7288ce2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "archinstall" -dynamic = ["version"] +version = "3.0.2" description = "Arch Linux installer - guided, templates etc." authors = [ {name = "Anton Hvornum", email = "anton@hvornum.se"}, @@ -44,7 +44,6 @@ doc = ["sphinx"] archinstall = "archinstall:run_as_a_module" [tool.setuptools.dynamic] -version = {attr = "archinstall.__version__"} readme = {file = ["README.rst", "USAGE.rst"]} [tool.setuptools] @@ -106,6 +105,11 @@ module = "archinstall.lib.disk.*" # 'Any' imports are allowed because pyparted doesn't have type hints disallow_any_unimported = false +[[tool.mypy.overrides]] +module = "archinstall.lib.models.*" +# 'Any' imports are allowed because pyparted doesn't have type hints +disallow_any_unimported = false + [[tool.mypy.overrides]] module = "archinstall.lib.packages" disallow_any_explicit = true diff --git a/tests/data/test_config.json b/tests/data/test_config.json index 90d64dec..4e50417b 100644 --- a/tests/data/test_config.json +++ b/tests/data/test_config.json @@ -3,6 +3,7 @@ "archinstall-language": "English", "audio_config": {"audio": "pipewire"}, "bootloader": "Systemd-boot", + "services": ["service_1", "service_2"], "disk_config": { "config_type": "default_layout", "device_modifications": [ @@ -107,7 +108,8 @@ "sys_lang": "en_US" }, "mirror_config": { - "mirror-regions": { + "custom_mirrors": [], + "mirror_regions": { "Australia": [ "http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch" ] @@ -150,7 +152,10 @@ "main": "Desktop" } }, + "custom_commands": [ + "echo 'Hello, World!'" + ], "swap": false, "timezone": "UTC", - "version": "2.8.6" + "version": "3.0.2" } diff --git a/tests/test_args.py b/tests/test_args.py index b1642f16..6c81a897 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -2,16 +2,14 @@ from pathlib import Path from pytest import MonkeyPatch -import archinstall from archinstall.default_profiles.profile import GreeterType from archinstall.lib.args import ArchConfig, ArchConfigHandler, Arguments -from archinstall.lib.disk import DiskLayoutConfiguration, DiskLayoutType from archinstall.lib.hardware import GfxDriver -from archinstall.lib.locale import LocaleConfiguration -from archinstall.lib.mirrors import MirrorConfiguration -from archinstall.lib.models import Audio, AudioConfiguration, Bootloader, NetworkConfiguration, User +from archinstall.lib.mirrors import MirrorConfiguration, MirrorRegion +from archinstall.lib.models import Audio, AudioConfiguration, Bootloader, DiskLayoutConfiguration, DiskLayoutType, NetworkConfiguration, User +from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.network_configuration import Nic, NicType -from archinstall.lib.profile.profile_model import ProfileConfiguration +from archinstall.lib.models.profile_model import ProfileConfiguration from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.translationhandler import translation_handler @@ -27,7 +25,7 @@ def test_default_args(monkeypatch: MonkeyPatch) -> None: silent=False, dry_run=False, script='guided', - mount_point=Path('/mnt'), + mountpoint=Path('/mnt'), skip_ntp=False, debug=False, offline=False, @@ -77,7 +75,7 @@ def test_correct_parsing_args( silent=True, dry_run=True, script='execution_script', - mount_point=Path('/tmp'), + mountpoint=Path('/mnt'), skip_ntp=True, debug=True, offline=True, @@ -102,13 +100,13 @@ def test_config_file_parsing( ]) handler = ArchConfigHandler() - arch_config = handler.arch_config + arch_config = handler.config # TODO: Use the real values from the test fixture instead of clearing out the entries arch_config.disk_config.device_modifications = [] # type: ignore[union-attr] assert arch_config == ArchConfig( - version=archinstall.__version__, + version='3.0.2', locale_config=LocaleConfiguration( kb_layout='us', sys_lang='en_US', @@ -141,7 +139,12 @@ def test_config_file_parsing( greeter=GreeterType.Lightdm ), mirror_config=MirrorConfiguration( - mirror_regions=[], + mirror_regions=[ + MirrorRegion( + name='Australia', + urls=['http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch'] + ) + ], custom_mirrors=[] ), network_config=NetworkConfiguration( @@ -170,6 +173,9 @@ def test_config_file_parsing( swap=False, timezone='UTC', additional_repositories=["testing"], - _users=[User(username='user_name', password='user_pwd', sudo=True)], - _disk_encryption=None + users=[User(username='user_name', password='user_pwd', sudo=True)], + disk_encryption=None, + services=['service_1', 'service_2'], + root_password='super_pwd', + custom_commands=["echo 'Hello, World!'"] ) diff --git a/tests/test_configuration_output.py b/tests/test_configuration_output.py new file mode 100644 index 00000000..efa6ccc1 --- /dev/null +++ b/tests/test_configuration_output.py @@ -0,0 +1,58 @@ +import json +from pathlib import Path + +from pytest import MonkeyPatch + +from archinstall.lib.args import ArchConfigHandler +from archinstall.lib.configuration import ConfigurationOutput + + +def test_user_config_roundtrip( + monkeypatch: MonkeyPatch, + config_fixture: Path, +) -> None: + monkeypatch.setattr('sys.argv', ['archinstall', '--config', str(config_fixture)]) + + handler = ArchConfigHandler() + arch_config = handler.config + + config_output = ConfigurationOutput(arch_config) + + test_out_dir = Path('/tmp/') + test_out_file = test_out_dir / config_output.user_configuration_file + + config_output.save(test_out_dir) + + result = json.loads(test_out_file.read_text()) + expected = json.loads(config_fixture.read_text()) + + # the parsed config will check if the given device exists otherwise + # it will ignore the modification; as this test will run on various local systems + # and the CI pipeline there's no good way specify a real device so we'll simply + # copy the expected result to the actual result + result['disk_config']['config_type'] = expected['disk_config']['config_type'] + result['disk_config']['device_modifications'] = expected['disk_config']['device_modifications'] + + assert sorted(result.items()) == sorted(expected.items()) + + +def test_creds_roundtrip( + monkeypatch: MonkeyPatch, + creds_fixture: Path, +) -> None: + monkeypatch.setattr('sys.argv', ['archinstall', '--creds', str(creds_fixture)]) + + handler = ArchConfigHandler() + arch_config = handler.config + + config_output = ConfigurationOutput(arch_config) + + test_out_dir = Path('/tmp/') + test_out_file = test_out_dir / config_output.user_credentials_file + + config_output.save(test_out_dir) + + result = json.loads(test_out_file.read_text()) + expected = json.loads(creds_fixture.read_text()) + + assert sorted(result.items()) == sorted(expected.items())