From 00b0ae7ba439a5a420095175b3bedd52c569db51 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Wed, 19 Apr 2023 20:55:42 +1000 Subject: PyParted and a large rewrite of the underlying partitioning (#1604) * Invert mypy files * Add optional pre-commit hooks * New profile structure * Serialize profiles * Use profile instead of classmethod * Custom profile setup * Separator between back * Support profile import via url * Move profiles module * Refactor files * Remove symlink * Add user to docker group * Update schema description * Handle list services * mypy fixes * mypy fixes * Rename profilesv2 to profiles * flake8 * mypy again * Support selecting DM * Fix mypy * Cleanup * Update greeter setting * Update schema * Revert toml changes * Poc external dependencies * Dependency support * New encryption menu * flake8 * Mypy and flake8 * Unify lsblk command * Update bootloader configuration * Git hooks * Fix import * Pyparted * Remove custom font setting * flake8 * Remove default preview * Manual partitioning menu * Update structure * Disk configuration * Update filesystem * luks2 encryption * Everything works until installation * Btrfsutil * Btrfs handling * Update btrfs * Save encryption config * Fix pipewire issue * Update mypy version * Update all pre-commit * Update package versions * Revert audio/pipewire * Merge master PRs * Add master changes * Merge master changes * Small renaming * Pull master changes * Reset disk enc after disk config change * Generate locals * Update naming * Fix imports * Fix broken sync * Fix pre selection on table menu * Profile menu * Update profile * Fix post_install * Added python-pyparted to PKGBUILD, this requires [testing] to be enabled in order to run makepkg. Package still works via python -m build etc. * Swaped around some setuptools logic in pyproject Since we define `package-data` and `packages` there should be no need for: ``` [tool.setuptools.packages.find] where = ["archinstall", "archinstall.*"] ``` * Removed pyproject collisions. Duplicate definitions. * Made sure pyproject.toml includes languages * Add example and update README * Fix pyproject issues * Generate locale * Refactor imports * Simplify imports * Add profile description and package examples * Align code * Fix mypy * Simplify imports * Fix saving config * Fix wrong luks merge * Refactor installation * Fix cdrom device loading * Fix wrongly merged code * Fix imports and greeter * Don't terminate on partprobe error * Use specific path on partprobe from luks * Update archinstall/lib/disk/device_model.py Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> * Update archinstall/lib/disk/device_model.py Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> * Update github workflow to test archinstall installation * Update sway merge * Generate locales * Update workflow --------- Co-authored-by: Daniel Girtler Co-authored-by: Anton Hvornum Co-authored-by: Anton Hvornum Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> --- archinstall/scripts/__init__.py | 0 archinstall/scripts/guided.py | 276 +++++++++++++++++++++++++++++ archinstall/scripts/minimal.py | 104 +++++++++++ archinstall/scripts/only_hd.py | 104 +++++++++++ archinstall/scripts/swiss.py | 353 ++++++++++++++++++++++++++++++++++++++ archinstall/scripts/unattended.py | 18 ++ 6 files changed, 855 insertions(+) create mode 100644 archinstall/scripts/__init__.py create mode 100644 archinstall/scripts/guided.py create mode 100644 archinstall/scripts/minimal.py create mode 100644 archinstall/scripts/only_hd.py create mode 100644 archinstall/scripts/swiss.py create mode 100644 archinstall/scripts/unattended.py (limited to 'archinstall/scripts') diff --git a/archinstall/scripts/__init__.py b/archinstall/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py new file mode 100644 index 00000000..d9c5837c --- /dev/null +++ b/archinstall/scripts/guided.py @@ -0,0 +1,276 @@ +import logging +import os +from pathlib import Path +from typing import Any, TYPE_CHECKING + +import archinstall +from archinstall.lib import disk +from archinstall.lib.global_menu import GlobalMenu +from archinstall.default_profiles.applications.pipewire import PipewireProfile +from archinstall.lib.configuration import ConfigurationOutput +from archinstall.lib.installer import Installer +from archinstall.lib.menu import Menu +from archinstall.lib.mirrors import use_mirrors +from archinstall.lib.models.bootloader import Bootloader +from archinstall.lib.models.network_configuration import NetworkConfigurationHandler +from archinstall.lib.output import log +from archinstall.lib.profile.profiles_handler import profile_handler + +if TYPE_CHECKING: + _: Any + + +if archinstall.arguments.get('help'): + print("See `man archinstall` for help.") + exit(0) + +if os.getuid() != 0: + print(_("Archinstall requires root privileges to run. See --help for more.")) + exit(1) + +# Log various information about hardware before starting the installation. This might assist in troubleshooting +archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) +archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) +archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) +archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) +archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) + +# For support reasons, we'll log the disk layout pre installation to match against post-installation layout +archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +def ask_user_questions(): + """ + First, we'll ask the user for a bunch of user input. + Not until we're satisfied with what we want to install + will we continue with the actual installation steps. + """ + + # ref: https://github.com/archlinux/archinstall/pull/831 + # we'll set NTP to true by default since this is also + # the default value specified in the menu options; in + # case it will be changed by the user we'll also update + # the system immediately + global_menu = GlobalMenu(data_store=archinstall.arguments) + + global_menu.enable('archinstall-language') + + global_menu.enable('keyboard-layout') + + # Set which region to download packages from during the installation + global_menu.enable('mirror-region') + + global_menu.enable('sys-language') + + global_menu.enable('sys-encoding') + + global_menu.enable('disk_config', mandatory=True) + + # Specify disk encryption options + global_menu.enable('disk_encryption') + + # Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB) + global_menu.enable('bootloader') + + global_menu.enable('swap') + + # Get the hostname for the machine + global_menu.enable('hostname') + + # Ask for a root password (optional, but triggers requirement for super-user if skipped) + global_menu.enable('!root-password', mandatory=True) + + global_menu.enable('!users', mandatory=True) + + # Ask for archinstall-specific profiles_bck (such as desktop environments etc) + global_menu.enable('profile_config') + + # Ask about audio server selection if one is not already set + global_menu.enable('audio') + + # Ask for preferred kernel: + global_menu.enable('kernels') + + global_menu.enable('packages') + + if archinstall.arguments.get('advanced', False): + # Enable parallel downloads + global_menu.enable('parallel downloads') + + # Ask or Call the helper function that asks the user to optionally configure a network. + global_menu.enable('nic') + + global_menu.enable('timezone') + + global_menu.enable('ntp') + + global_menu.enable('additional-repositories') + + global_menu.enable('__separator__') + + global_menu.enable('save_config') + global_menu.enable('install') + global_menu.enable('abort') + + global_menu.run() + + +def perform_installation(mountpoint: Path): + """ + Performs the installation steps on a block device. + Only requirement is that the block devices are + formatted and setup prior to entering this function. + """ + log('Starting installation', level=logging.INFO) + disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + + # Retrieve list of additional repositories and set boolean values appropriately + enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) + enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) + + locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" + + disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) as installation: + # Mount all the drives to the desired mountpoint + if disk_config.config_type != disk.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: + # generate encryption key files for the mounted luks devices + installation.generate_key_files() + + if archinstall.arguments.get('ntp', False): + installation.activate_ntp() + + # Set mirrors used by pacstrap (outside of installation) + if archinstall.arguments.get('mirror-region', None): + use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + + installation.minimal_installation( + testing=enable_testing, + multilib=enable_multilib, + hostname=archinstall.arguments.get('hostname', 'archlinux'), + locales=[locale] + ) + + if archinstall.arguments.get('mirror-region') is not None: + if archinstall.arguments.get("mirrors", None) is not None: + installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + + if archinstall.arguments.get('swap'): + installation.setup_swap('zram') + + if archinstall.arguments.get("bootloader") == Bootloader.Grub and archinstall.has_uefi(): + installation.add_additional_packages("grub") + + installation.add_bootloader(archinstall.arguments["bootloader"]) + + # If user selected to copy the current ISO network configuration + # Perform a copy of the config + network_config = archinstall.arguments.get('nic', None) + + if network_config: + handler = NetworkConfigurationHandler(network_config) + handler.config_installer(installation) + + if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': + installation.add_additional_packages(archinstall.arguments.get('packages', None)) + + if users := archinstall.arguments.get('!users', None): + installation.create_users(users) + + if audio := archinstall.arguments.get('audio', None): + log(f'Installing audio server: {audio}', level=logging.INFO) + if audio == 'pipewire': + PipewireProfile().install(installation) + elif audio == 'pulseaudio': + installation.add_additional_packages("pulseaudio") + else: + installation.log("No audio server will be installed.", level=logging.INFO) + + if profile_config := archinstall.arguments.get('profile_config', None): + profile_handler.install_profile_config(installation, profile_config) + + if timezone := archinstall.arguments.get('timezone', None): + installation.set_timezone(timezone) + + if archinstall.arguments.get('ntp', False): + installation.activate_time_syncronization() + + if archinstall.accessibility_tools_in_use(): + installation.enable_espeakup() + + if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): + installation.user_set_pw('root', root_pw) + + # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # After which, this step will set the language both for console and x11 if x11 was installed for instance. + installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + + if profile_config := archinstall.arguments.get('profile_config', None): + 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['services']) + + # 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) + + installation.genfstab() + + installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") + + if not archinstall.arguments.get('silent'): + prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run() + if choice.value == Menu.yes(): + try: + installation.drop_to_shell() + except: + pass + + archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +if archinstall.arguments.get('skip-mirror-check', False) is False and archinstall.check_mirror_reachable() is False: + log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) + archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + exit(1) + +if not archinstall.arguments.get('silent'): + ask_user_questions() + +config_output = ConfigurationOutput(archinstall.arguments) + +if not archinstall.arguments.get('silent'): + config_output.show() + +config_output.save() + +if archinstall.arguments.get('dry_run'): + exit(0) + +if not archinstall.arguments.get('silent'): + input(str(_('Press Enter to continue.'))) + +fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) +) + +fs_handler.perform_filesystem_operations() + +perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt'))) diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py new file mode 100644 index 00000000..0cdbdcef --- /dev/null +++ b/archinstall/scripts/minimal.py @@ -0,0 +1,104 @@ +from pathlib import Path +from typing import TYPE_CHECKING, Any, List + +import archinstall +from archinstall import ConfigurationOutput, Installer, ProfileConfiguration, profile_handler +from archinstall.default_profiles.minimal import MinimalProfile +from archinstall import disk +from archinstall import models +from archinstall.lib.user_interaction.disk_conf import select_devices, suggest_single_disk_layout + +if TYPE_CHECKING: + _: Any + + +archinstall.log("Minimal only supports:") +archinstall.log(" * Being installed to a single disk") + +if archinstall.arguments.get('help', None): + archinstall.log(" - Optional disk encryption via --!encryption-password=") + archinstall.log(" - Optional filesystem type via --filesystem=") + archinstall.log(" - Optional systemd network via --network") + + +def perform_installation(mountpoint: Path): + disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) as installation: + # Strap in the base system, add a boot loader and configure + # some other minor details as specified by this profile and user. + if installation.minimal_installation(): + installation.set_hostname('minimal-arch') + installation.add_bootloader(models.Bootloader.Systemd) + + # Optionally enable networking: + if archinstall.arguments.get('network', None): + installation.copy_iso_network_config(enable_services=True) + + installation.add_additional_packages(['nano', 'wget', 'git']) + + profile_config = ProfileConfiguration(MinimalProfile()) + profile_handler.install_profile_config(installation, profile_config) + + user = models.User('devel', 'devel', False) + installation.create_users(user) + + # Once this is done, we output some useful information to the user + # And the installation is complete. + archinstall.log("There are two new accounts in your installation after reboot:") + archinstall.log(" * root (password: airoot)") + archinstall.log(" * devel (password: devel)") + + +def prompt_disk_layout(): + 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(): + 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 += list(filter(lambda x: x.mountpoint != Path('/boot'), mod.partitions)) + + archinstall.arguments['disk_encryption'] = disk.DiskEncryption( + encryption_type=disk.EncryptionType.Partition, + encryption_password=enc_password, + partitions=partitions + ) + + +prompt_disk_layout() +parse_disk_encryption() + +config_output = ConfigurationOutput(archinstall.arguments) +config_output.show() + +input(str(_('Press Enter to continue.'))) + +fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) +) + +fs_handler.perform_filesystem_operations() + +perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt'))) diff --git a/archinstall/scripts/only_hd.py b/archinstall/scripts/only_hd.py new file mode 100644 index 00000000..a903c5fe --- /dev/null +++ b/archinstall/scripts/only_hd.py @@ -0,0 +1,104 @@ +import logging +import os +from pathlib import Path + +import archinstall +from archinstall import Installer +from archinstall.lib.configuration import ConfigurationOutput +from archinstall import disk + +if archinstall.arguments.get('help'): + print("See `man archinstall` for help.") + exit(0) + + +if os.getuid() != 0: + print("Archinstall requires root privileges to run. See --help for more.") + exit(1) + + +def ask_user_questions(): + global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments) + + global_menu.enable('archinstall-language') + + global_menu.enable('disk_config', mandatory=True) + global_menu.enable('disk_encryption') + global_menu.enable('swap') + + global_menu.enable('save_config') + global_menu.enable('install') + global_menu.enable('abort') + + global_menu.run() + + +def perform_installation(mountpoint: Path): + """ + Performs the installation steps on a block device. + 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) + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) 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'): + installation.mount_ordered_layout() + + # to generate a fstab directory holder. Avoids an error on exit and at the same time checks the procedure + target = Path(f"{mountpoint}/etc/fstab") + if not target.parent.exists(): + target.parent.mkdir(parents=True) + + # For support reasons, we'll log the disk layout post installation (crash or no crash) + archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +# Log various information about hardware before starting the installation. This might assist in troubleshooting +archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) +archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) +archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) +archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) +archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) + +# For support reasons, we'll log the disk layout pre installation to match against post-installation layout +archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +if archinstall.arguments.get('skip-mirror-check', False) is False and archinstall.check_mirror_reachable() is False: + log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) + archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + exit(1) + +if not archinstall.arguments.get('silent'): + ask_user_questions() + +config_output = ConfigurationOutput(archinstall.arguments) +if not archinstall.arguments.get('silent'): + config_output.show() + +config_output.save() + +if archinstall.arguments.get('dry_run'): + exit(0) + +if not archinstall.arguments.get('silent'): + input('Press Enter to continue.') + +fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) +) + +fs_handler.perform_filesystem_operations() + + +perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt'))) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py new file mode 100644 index 00000000..e2ee6fcb --- /dev/null +++ b/archinstall/scripts/swiss.py @@ -0,0 +1,353 @@ +import logging +import os +from enum import Enum +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict + +import archinstall +from archinstall.lib.mirrors import use_mirrors +from archinstall import models +from archinstall import disk +from archinstall.lib.profile.profiles_handler import profile_handler +from archinstall import menu +from archinstall.lib.global_menu import GlobalMenu +from archinstall.lib.output import log +from archinstall import Installer +from archinstall.lib.configuration import ConfigurationOutput +from archinstall.default_profiles.applications.pipewire import PipewireProfile + +if TYPE_CHECKING: + _: Any + + +if archinstall.arguments.get('help'): + print("See `man archinstall` for help.") + exit(0) + + +if os.getuid() != 0: + print("Archinstall requires root privileges to run. See --help for more.") + exit(1) + + +class ExecutionMode(Enum): + Full = 'full' + Lineal = 'lineal' + Only_HD = 'only-hd' + Only_OS = 'only-os' + Minimal = 'minimal' + + +def select_mode() -> ExecutionMode: + options = [str(e.value) for e in ExecutionMode] + choice = menu.Menu( + str(_('Select an execution mode')), + options, + default_option=ExecutionMode.Full.value, + skip=False + ).run() + + return ExecutionMode(choice.single_value) + + +class SetupMenu(GlobalMenu): + def __init__(self, storage_area: Dict[str, Any]): + super().__init__(data_store=storage_area) + + def setup_selection_menu_options(self): + super().setup_selection_menu_options() + + self._menu_options['mode'] = menu.Selector( + 'Excution mode', + lambda x : select_mode(), + display_func=lambda x: x.value if x else '', + default=ExecutionMode.Full) + + self._menu_options['continue'] = menu.Selector( + 'Continue', + exec_func=lambda n,v: True) + + self.enable('archinstall-language') + self.enable('ntp') + self.enable('mode') + self.enable('continue') + self.enable('abort') + + def exit_callback(self): + if self._data_store.get('ntp', False): + archinstall.SysCommand('timedatectl set-ntp true') + + if self._data_store.get('mode', None): + archinstall.arguments['mode'] = self._data_store['mode'] + log(f"Archinstall will execute under {archinstall.arguments['mode']} mode") + + +class SwissMainMenu(GlobalMenu): + def __init__( + self, + data_store: Dict[str, Any], + exec_mode: ExecutionMode = ExecutionMode.Full + ): + self._execution_mode = exec_mode + super().__init__(data_store) + + def setup_selection_menu_options(self): + super().setup_selection_menu_options() + + options_list = [] + mandatory_list = [] + + match self._execution_mode: + case ExecutionMode.Full | ExecutionMode.Lineal: + options_list = [ + 'keyboard-layout', 'mirror-region', 'disk_config', + 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', + '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', + 'timezone', 'ntp' + ] + + if archinstall.arguments.get('advanced', False): + options_list.extend(['sys-language', 'sys-encoding']) + + mandatory_list = ['disk_config', 'bootloader', 'hostname'] + case ExecutionMode.Only_HD: + options_list = ['disk_config', 'disk_encryption','swap'] + mandatory_list = ['disk_config'] + case ExecutionMode.Only_OS: + options_list = [ + 'keyboard-layout', 'mirror-region','bootloader', 'hostname', + '!root-password', '!users', 'profile_config', 'audio', 'kernels', + 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' + ] + + mandatory_list = ['hostname'] + + if archinstall.arguments.get('advanced', False): + options_list += ['sys-language','sys-encoding'] + case ExecutionMode.Minimal: + pass + case _: + archinstall.log(f' Execution mode {self._execution_mode} not supported') + exit(1) + + if self._execution_mode != ExecutionMode.Lineal: + options_list += ['save_config', 'install'] + + if not archinstall.arguments.get('advanced', False): + options_list.append('archinstall-language') + + options_list += ['abort'] + + for entry in mandatory_list: + self.enable(entry, mandatory=True) + + for entry in options_list: + self.enable(entry) + + +def ask_user_questions(exec_mode: ExecutionMode = ExecutionMode.Full): + """ + First, we'll ask the user for a bunch of user input. + Not until we're satisfied with what we want to install + will we continue with the actual installation steps. + """ + if archinstall.arguments.get('advanced', None): + setup_area: Dict[str, Any] = {} + setup = SetupMenu(setup_area) + + if exec_mode == ExecutionMode.Lineal: + for entry in setup.list_enabled_options(): + if entry in ('continue', 'abort'): + continue + if not setup.option(entry).enabled: + continue + setup.exec_option(entry) + else: + setup.run() + + archinstall.arguments['archinstall-language'] = setup_area.get('archinstall-language') + + with SwissMainMenu(data_store=archinstall.arguments, exec_mode=exec_mode) as menu: + if mode == ExecutionMode.Lineal: + for entry in menu.list_enabled_options(): + if entry in ('install', 'abort'): + continue + menu.exec_option(entry) + archinstall.arguments[entry] = menu.option(entry).get_selection() + else: + menu.run() + + +def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): + disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + + enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) + enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) + + locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) as installation: + if exec_mode in [ExecutionMode.Full, ExecutionMode.Only_HD]: + 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: + # generate encryption key files for the mounted luks devices + installation.generate_key_files() + + if archinstall.arguments.get('ntp', False): + installation.activate_ntp() + + # Set mirrors used by pacstrap (outside of installation) + if archinstall.arguments.get('mirror-region', None): + use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + + installation.minimal_installation( + testing=enable_testing, + multilib=enable_multilib, + hostname=archinstall.arguments.get('hostname', 'archlinux'), + locales=[locale] + ) + + if archinstall.arguments.get('mirror-region') is not None: + if archinstall.arguments.get("mirrors", None) is not None: + installation.set_mirrors( + archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + + if archinstall.arguments.get('swap'): + installation.setup_swap('zram') + + if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and archinstall.has_uefi(): + installation.add_additional_packages("grub") + + installation.add_bootloader(archinstall.arguments["bootloader"]) + + # If user selected to copy the current ISO network configuration + # Perform a copy of the config + network_config = archinstall.arguments.get('nic', None) + + if network_config: + handler = models.NetworkConfigurationHandler(network_config) + handler.config_installer(installation) + + if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': + installation.add_additional_packages(archinstall.arguments.get('packages', None)) + + if users := archinstall.arguments.get('!users', None): + installation.create_users(users) + + if audio := archinstall.arguments.get('audio', None): + log(f'Installing audio server: {audio}', level=logging.INFO) + if audio == 'pipewire': + PipewireProfile().install(installation) + elif audio == 'pulseaudio': + installation.add_additional_packages("pulseaudio") + else: + installation.log("No audio server will be installed.", level=logging.INFO) + + if profile_config := archinstall.arguments.get('profile_config', None): + profile_handler.install_profile_config(installation, profile_config) + + if timezone := archinstall.arguments.get('timezone', None): + installation.set_timezone(timezone) + + if archinstall.arguments.get('ntp', False): + installation.activate_time_syncronization() + + if archinstall.accessibility_tools_in_use(): + installation.enable_espeakup() + + if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): + installation.user_set_pw('root', root_pw) + + # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # After which, this step will set the language both for console and x11 if x11 was installed for instance. + installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + + if profile_config := archinstall.arguments.get('profile_config', None): + 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['services']) + + # 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) + + installation.genfstab() + + installation.log( + "For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", + fg="yellow") + + if not archinstall.arguments.get('silent'): + prompt = str( + _('Would you like to chroot into the newly created installation and perform post-installation configuration?')) + choice = menu.Menu(prompt, menu.Menu.yes_no(), default_option=menu.Menu.yes()).run() + if choice.value == menu.Menu.yes(): + try: + installation.drop_to_shell() + except: + pass + + archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +# Log various information about hardware before starting the installation. This might assist in troubleshooting +archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) +archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) +archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) +archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) +archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) + +# For support reasons, we'll log the disk layout pre installation to match against post-installation layout +archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +if not archinstall.check_mirror_reachable(): + log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) + archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + exit(1) + +param_mode = archinstall.arguments.get('mode', ExecutionMode.Full.value).lower() + +try: + mode = ExecutionMode(param_mode) +except KeyError: + log(f'Mode "{param_mode}" is not supported') + exit(1) + +if not archinstall.arguments.get('silent'): + ask_user_questions(mode) + +config_output = ConfigurationOutput(archinstall.arguments) +if not archinstall.arguments.get('silent'): + config_output.show() + +config_output.save() + +if archinstall.arguments.get('dry_run'): + exit(0) + +if not archinstall.arguments.get('silent'): + input('Press Enter to continue.') + +if mode in (ExecutionMode.Full, ExecutionMode.Only_HD): + fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) + ) + + fs_handler.perform_filesystem_operations() + +perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')), mode) diff --git a/archinstall/scripts/unattended.py b/archinstall/scripts/unattended.py new file mode 100644 index 00000000..0a1c5160 --- /dev/null +++ b/archinstall/scripts/unattended.py @@ -0,0 +1,18 @@ +import time + +import archinstall +from archinstall.lib.profile.profiles_handler import profile_handler + +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). + archinstall.log(f'Found a tailored profile for this machine called: "{profile.name}"') + + print('Starting install in:') + for i in range(10, 0, -1): + print(f'{i}...') + time.sleep(1) + + install_session = archinstall.storage['installation_session'] + profile.install(install_session) -- cgit v1.2.3-54-g00ecf From ec4ecbcb7a839ab06b739f01ce42bfd18376c620 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Thu, 4 May 2023 00:36:46 +1000 Subject: Full mypy compliance and small fixes (#1777) * Fix mypy compliance --------- Co-authored-by: Daniel Girtler --- .github/workflows/mypy.yaml | 2 +- archinstall/__init__.py | 3 +- archinstall/lib/disk/device_handler.py | 18 +- archinstall/lib/disk/device_model.py | 2 +- archinstall/lib/disk/fido.py | 9 +- archinstall/lib/general.py | 55 +++--- archinstall/lib/hardware.py | 55 +++--- archinstall/lib/installer.py | 239 +++++++++++------------ archinstall/lib/menu/abstract_menu.py | 4 +- archinstall/lib/menu/menu.py | 104 ++++++---- archinstall/lib/mirrors.py | 46 +++-- archinstall/lib/models/network_configuration.py | 78 ++++---- archinstall/lib/plugins.py | 66 ++++--- archinstall/lib/profile/profiles_handler.py | 16 +- archinstall/lib/systemd.py | 71 ++----- archinstall/lib/user_interaction/general_conf.py | 68 ++++--- archinstall/lib/user_interaction/locale_conf.py | 22 ++- archinstall/scripts/guided.py | 2 +- archinstall/scripts/swiss.py | 4 +- examples/interactive_installation.py | 4 +- mypy.ini | 14 -- pyproject.toml | 1 + 22 files changed, 454 insertions(+), 429 deletions(-) delete mode 100644 mypy.ini (limited to 'archinstall/scripts') diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index 8689570f..e0db6f06 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -15,4 +15,4 @@ jobs: # one day this will be enabled # run: mypy --strict --module archinstall || exit 0 - name: run mypy - run: mypy --config-file mypy.ini + run: mypy --config-file pyproject.toml diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 3d0768a5..29b70b7a 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -233,7 +233,8 @@ def post_process_arguments(arguments): log(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!", fg="red", level=logging.WARNING) if arguments.get('plugin', None): - load_plugin(arguments['plugin']) + path = arguments['plugin'] + load_plugin(path) load_config() diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index ba325cda..8f92cf3b 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -269,13 +269,13 @@ class DeviceHandler(object): # partition will be encrypted if enc_conf is not None and part_mod in enc_conf.partitions: self._perform_enc_formatting( - part_mod.real_dev_path, + part_mod.safe_dev_path, part_mod.mapper_name, part_mod.fs_type, enc_conf ) else: - self._perform_formatting(part_mod.fs_type, part_mod.real_dev_path) + self._perform_formatting(part_mod.fs_type, part_mod.safe_dev_path) def _perform_partitioning( self, @@ -287,11 +287,11 @@ class DeviceHandler(object): # when we require a delete and the partition to be (re)created # already exists then we have to delete it first if requires_delete and part_mod.status in [ModificationStatus.Modify, ModificationStatus.Delete]: - log(f'Delete existing partition: {part_mod.real_dev_path}', level=logging.INFO) - part_info = self.find_partition(part_mod.real_dev_path) + log(f'Delete existing partition: {part_mod.safe_dev_path}', level=logging.INFO) + part_info = self.find_partition(part_mod.safe_dev_path) if not part_info: - raise DiskError(f'No partition for dev path found: {part_mod.real_dev_path}') + raise DiskError(f'No partition for dev path found: {part_mod.safe_dev_path}') disk.deletePartition(part_info.partition) disk.commit() @@ -375,7 +375,7 @@ class DeviceHandler(object): part_mod: PartitionModification, enc_conf: Optional['DiskEncryption'] = None ): - log(f'Creating subvolumes: {part_mod.real_dev_path}', level=logging.INFO) + log(f'Creating subvolumes: {part_mod.safe_dev_path}', level=logging.INFO) luks_handler = None @@ -385,7 +385,7 @@ class DeviceHandler(object): raise ValueError('No device path specified for modification') luks_handler = self.unlock_luks2_dev( - part_mod.real_dev_path, + part_mod.safe_dev_path, part_mod.mapper_name, enc_conf.encryption_password ) @@ -395,7 +395,7 @@ class DeviceHandler(object): self.mount(luks_handler.mapper_dev, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) else: - self.mount(part_mod.real_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + self.mount(part_mod.safe_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) for sub_vol in part_mod.btrfs_subvols: log(f'Creating subvolume: {sub_vol.name}', level=logging.DEBUG) @@ -419,7 +419,7 @@ class DeviceHandler(object): self.umount(luks_handler.mapper_dev) luks_handler.lock() else: - self.umount(part_mod.real_dev_path) + self.umount(part_mod.safe_dev_path) def unlock_luks2_dev(self, dev_path: Path, mapper_name: str, enc_password: str) -> Luks2: luks_handler = Luks2(dev_path, mapper_name=mapper_name, password=enc_password) diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 0270a4dd..987a1e8a 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -603,7 +603,7 @@ class PartitionModification: return '' @property - def real_dev_path(self) -> Path: + def safe_dev_path(self) -> Path: if self.dev_path is None: raise ValueError('Device path was not set') return self.dev_path diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 436be4d4..2a53b551 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -2,7 +2,8 @@ from __future__ import annotations import getpass import logging -from typing import List +from pathlib import Path +from typing import List, Optional from .device_model import PartitionModification, Fido2Device from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes @@ -36,12 +37,12 @@ class Fido2: # to prevent continous reloading which will slow # down moving the cursor in the menu if not cls._loaded or reload: - ret = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') + ret: Optional[str] = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') if not ret: log('Unable to retrieve fido2 devices', level=logging.ERROR) return [] - fido_devices = clear_vt100_escape_codes(ret) + fido_devices: str = clear_vt100_escape_codes(ret) # type: ignore manufacturer_pos = 0 product_pos = 0 @@ -58,7 +59,7 @@ class Fido2: product = line[product_pos:] devices.append( - Fido2Device(path, manufacturer, product) + Fido2Device(Path(path), manufacturer, product) ) cls._loaded = True diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 57f13288..997b7d67 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -19,9 +19,15 @@ import pathlib from datetime import datetime, date from typing import Callable, Optional, Dict, Any, List, Union, Iterator, TYPE_CHECKING +from .exceptions import RequirementError, SysCallError +from .output import log +from .storage import storage + + if TYPE_CHECKING: from .installer import Installer + if sys.platform == 'linux': from select import epoll, EPOLLIN, EPOLLHUP else: @@ -53,30 +59,15 @@ else: except OSError: return [] -from .exceptions import RequirementError, SysCallError -from .output import log -from .storage import storage def gen_uid(entropy_length :int = 256) -> str: return hashlib.sha512(os.urandom(entropy_length)).hexdigest() + def generate_password(length :int = 64) -> str: haystack = string.printable # digits, ascii_letters, punctiation (!"#$[] etc) and whitespace return ''.join(secrets.choice(haystack) for i in range(length)) -def multisplit(s :str, splitters :List[str]) -> str: - s = [s, ] - for key in splitters: - ns = [] - for obj in s: - x = obj.split(key) - for index, part in enumerate(x): - if len(part): - ns.append(part) - if index < len(x) - 1: - ns.append(key) - s = ns - return s def locate_binary(name :str) -> str: for PATH in os.environ['PATH'].split(':'): @@ -88,20 +79,20 @@ def locate_binary(name :str) -> str: raise RequirementError(f"Binary {name} does not exist.") -def clear_vt100_escape_codes(data :Union[bytes, str]): + +def clear_vt100_escape_codes(data :Union[bytes, str]) -> Union[bytes, str]: # https://stackoverflow.com/a/43627833/929999 if type(data) == bytes: - vt100_escape_regex = bytes(r'\x1B\[[?0-9;]*[a-zA-Z]', 'UTF-8') - else: + byte_vt100_escape_regex = bytes(r'\x1B\[[?0-9;]*[a-zA-Z]', 'UTF-8') + data = re.sub(byte_vt100_escape_regex, b'', data) + elif type(data) == str: vt100_escape_regex = r'\x1B\[[?0-9;]*[a-zA-Z]' - - for match in re.findall(vt100_escape_regex, data, re.IGNORECASE): - data = data.replace(match, '' if type(data) == str else b'') + data = re.sub(vt100_escape_regex, '', data) + else: + raise ValueError(f'Unsupported data type: {type(data)}') return data -def json_dumps(*args :str, **kwargs :str) -> str: - return json.dumps(*args, **{**kwargs, 'cls': JSON}) class JsonEncoder: @staticmethod @@ -245,10 +236,12 @@ class SysCommandWorker: def __iter__(self, *args :str, **kwargs :Dict[str, Any]) -> Iterator[bytes]: for line in self._trace_log[self._trace_log_pos:self._trace_log.rfind(b'\n')].split(b'\n'): if line: + escaped_line: bytes = line + if self.remove_vt100_escape_codes_from_lines: - line = clear_vt100_escape_codes(line) + escaped_line = clear_vt100_escape_codes(line) # type: ignore - yield line + b'\n' + yield escaped_line + b'\n' self._trace_log_pos = self._trace_log.rfind(b'\n') @@ -279,7 +272,11 @@ class SysCommandWorker: log(args[1], level=logging.DEBUG, fg='red') if self.exit_code != 0: - raise SysCallError(f"{self.cmd} exited with abnormal exit code [{self.exit_code}]: {self._trace_log[-500:]}", self.exit_code, worker=self) + raise SysCallError( + f"{self.cmd} exited with abnormal exit code [{self.exit_code}]: {str(self._trace_log[-500:])}", + self.exit_code, + worker=self + ) def is_alive(self) -> bool: self.poll() @@ -328,7 +325,7 @@ class SysCommandWorker: change_perm = True with peak_logfile.open("a") as peek_output_log: - peek_output_log.write(output) + peek_output_log.write(str(output)) if change_perm: os.chmod(str(peak_logfile), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) @@ -497,7 +494,7 @@ class SysCommand: clears any printed output if ``.peek_output=True``. """ if self.session: - return self.session + return True with SysCommandWorker( self.cmd, diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 9660ea95..3759725f 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -2,7 +2,7 @@ import os import logging from functools import partial from pathlib import Path -from typing import Iterator, Optional, Union +from typing import Iterator, Optional, Dict from .general import SysCommand from .networking import list_interfaces, enrich_iface_types @@ -61,15 +61,15 @@ AVAILABLE_GFX_DRIVERS = { "VMware / VirtualBox (open-source)": ["mesa", "xf86-video-vmware"], } -CPUINFO = Path("/proc/cpuinfo") -MEMINFO = Path("/proc/meminfo") - def cpuinfo() -> Iterator[dict[str, str]]: - """Yields information about the CPUs of the system.""" - cpu = {} + """ + Yields information about the CPUs of the system + """ + cpu_info_path = Path("/proc/cpuinfo") + cpu: Dict[str, str] = {} - with CPUINFO.open() as file: + with cpu_info_path.open() as file: for line in file: if not (line := line.strip()): yield cpu @@ -80,24 +80,31 @@ def cpuinfo() -> Iterator[dict[str, str]]: cpu[key.strip()] = value.strip() -def meminfo(key: Optional[str] = None) -> Union[dict[str, int], Optional[int]]: - """Returns a dict with memory info if called with no args +def all_meminfo() -> Dict[str, int]: + """ + Returns a dict with memory info if called with no args or the value of the given key of said dict. """ - with MEMINFO.open() as file: - mem_info = { - (columns := line.strip().split())[0].rstrip(':'): int(columns[1]) - for line in file - } + mem_info_path = Path("/proc/meminfo") + mem_info: Dict[str, int] = {} - if key is None: - return mem_info + with mem_info_path.open() as file: + for line in file: + key, value = line.strip().split(':') + num = value.split()[0] + mem_info[key] = int(num) + + return mem_info - return mem_info.get(key) + +def meminfo_for_key(key: str) -> int: + info = all_meminfo() + return info[key] def has_wifi() -> bool: - return 'WIRELESS' in enrich_iface_types(list_interfaces().values()).values() + ifaces = list(list_interfaces().values()) + return 'WIRELESS' in enrich_iface_types(ifaces).values() def has_cpu_vendor(vendor_id: str) -> bool: @@ -160,15 +167,15 @@ def product_name() -> Optional[str]: def mem_available() -> Optional[int]: - return meminfo('MemAvailable') + return meminfo_for_key('MemAvailable') def mem_free() -> Optional[int]: - return meminfo('MemFree') + return meminfo_for_key('MemFree') def mem_total() -> Optional[int]: - return meminfo('MemTotal') + return meminfo_for_key('MemTotal') def virtualization() -> Optional[str]: @@ -182,9 +189,9 @@ def virtualization() -> Optional[str]: def is_vm() -> bool: try: - return b"none" not in b"".join(SysCommand("systemd-detect-virt")).lower() + result = SysCommand("systemd-detect-virt") + return b"none" not in b"".join(result).lower() except SysCallError as error: log(f"System is not running in a VM: {error}", level=logging.DEBUG) - return None -# TODO: Add more identifiers + return False diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index ddbcc2f2..b6eaa797 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -7,7 +7,7 @@ import shutil import subprocess import time from pathlib import Path -from typing import Any, Iterator, List, Mapping, Optional, TYPE_CHECKING, Union, Dict +from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable, Iterable from . import disk from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError @@ -36,32 +36,6 @@ __packages__ = ["base", "base-devel", "linux-firmware", "linux", "linux-lts", "l __accessibility_packages__ = ["brltty", "espeakup", "alsa-utils"] -class InstallationFile: - def __init__(self, installation :'Installer', filename :str, owner :str, mode :str = "w"): - self.installation = installation - self.filename = filename - self.owner = owner - self.mode = mode - self.fh = None - - def __enter__(self) -> 'InstallationFile': - self.fh = open(self.filename, self.mode) - return self - - def __exit__(self, *args :str) -> None: - self.fh.close() - self.installation.chown(self.owner, self.filename) - - def write(self, data: Union[str, bytes]) -> int: - return self.fh.write(data) - - def read(self, *args) -> Union[str, bytes]: - return self.fh.read(*args) - -# def poll(self, *args) -> bool: -# return self.fh.poll(*args) - - def accessibility_tools_in_use() -> bool: return os.system('systemctl is-active --quiet espeakup.service') == 0 @@ -106,15 +80,17 @@ class Installer: self.kernels = kernels self._disk_config = disk_config - self._disk_encryption = disk_encryption - if self._disk_encryption is None: + if disk_encryption is None: self._disk_encryption = disk.DiskEncryption(disk.EncryptionType.NoEncryption) + else: + self._disk_encryption = disk_encryption + + self.target: Path = target - self.target = target self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S') self.milliseconds = int(str(time.time()).split('.')[1]) - self.helper_flags = {'base': False, 'bootloader': False} + self.helper_flags: Dict[str, Any] = {'base': False, 'bootloader': None} self.base_packages = base_packages for kernel in self.kernels: @@ -124,31 +100,33 @@ class Installer: if accessibility_tools_in_use(): self.base_packages.extend(__accessibility_packages__) - self.post_base_install = [] + self.post_base_install: List[Callable] = [] # TODO: Figure out which one of these two we'll use.. But currently we're mixing them.. storage['session'] = self storage['installation_session'] = self - self.MODULES = [] - self.BINARIES = [] - self.FILES = [] + self.modules: List[str] = [] + self._binaries: List[str] = [] + self._files: List[str] = [] + # systemd, sd-vconsole and sd-encrypt will be replaced by udev, keymap and encrypt # if HSM is not used to encrypt the root volume. Check mkinitcpio() function for that override. - self.HOOKS = ["base", "systemd", "autodetect", "keyboard", "sd-vconsole", "modconf", "block", "filesystems", "fsck"] - self.KERNEL_PARAMS = [] - self.FSTAB_ENTRIES = [] + self._hooks: List[str] = [ + "base", "systemd", "autodetect", "keyboard", + "sd-vconsole", "modconf", "block", "filesystems", "fsck" + ] + self._kernel_params: List[str] = [] + self._fstab_entries: List[str] = [] self._zram_enabled = False - def __enter__(self, *args: str, **kwargs: str) -> 'Installer': + def __enter__(self) -> 'Installer': return self - def __exit__(self, *args :str, **kwargs :str) -> bool: - # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager - - if len(args) >= 2 and args[1]: - self.log(args[1], level=logging.ERROR, fg='red') + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None: + log(exc_val, fg='red', level=logging.ERROR) self.sync_log_to_install_medium() @@ -156,7 +134,7 @@ class Installer: # and then reboot, and a identical log file will be found in the ISO medium anyway. print(_("[!] A log file has been created here: {}").format(os.path.join(storage['LOG_PATH'], storage['LOG_FILE']))) print(_(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")) - raise args[1] + raise exc_val if not (missing_steps := self.post_install_check()): self.log('Installation completed without any errors. You may now reboot.', fg='green', level=logging.INFO) @@ -164,6 +142,7 @@ class Installer: return True else: self.log('Some required steps were not successfully installed/configured before leaving the installer:', fg='red', level=logging.WARNING) + for step in missing_steps: self.log(f' - {step}', fg='red', level=logging.WARNING) @@ -247,31 +226,32 @@ class Installer: luks_handlers = {} for part_mod in partitions: - luks_handler = disk.device_handler.unlock_luks2_dev( - part_mod.dev_path, - part_mod.mapper_name, - self._disk_encryption.encryption_password - ) - luks_handlers[part_mod] = luks_handler + if part_mod.mapper_name and part_mod.dev_path: + luks_handler = disk.device_handler.unlock_luks2_dev( + part_mod.dev_path, + part_mod.mapper_name, + self._disk_encryption.encryption_password + ) + luks_handlers[part_mod] = luks_handler return luks_handlers def _mount_partition(self, part_mod: disk.PartitionModification): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined - if part_mod.mountpoint is not None: + if part_mod.mountpoint and part_mod.dev_path: target = self.target / part_mod.relative_mountpoint disk.device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options) - if part_mod.fs_type == disk.FilesystemType.Btrfs: + if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.dev_path: self._mount_btrfs_subvol(part_mod.dev_path, part_mod.btrfs_subvols) def _mount_luks_partiton(self, part_mod: disk.PartitionModification, luks_handler: Luks2): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined - if part_mod.mountpoint is not None: + if part_mod.mountpoint and luks_handler.mapper_dev: target = self.target / part_mod.relative_mountpoint disk.device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options) - if part_mod.fs_type == disk.FilesystemType.Btrfs: + if part_mod.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev: self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols) def _mount_btrfs_subvol(self, dev_path: Path, subvolumes: List[disk.SubvolumeModification]): @@ -346,15 +326,15 @@ class Installer: SysCommand(f'chmod 0600 {self.target}{file}') SysCommand(f'mkswap {self.target}{file}') - self.FSTAB_ENTRIES.append(f'{file} none swap defaults 0 0') + self._fstab_entries.append(f'{file} none swap defaults 0 0') if enable_resume: resume_uuid = SysCommand(f'findmnt -no UUID -T {self.target}{file}').decode('UTF-8').strip() resume_offset = SysCommand(f'/usr/bin/filefrag -v {self.target}{file}').decode('UTF-8').split('0:', 1)[1].split(":", 1)[1].split("..", 1)[0].strip() - self.HOOKS.append('resume') - self.KERNEL_PARAMS.append(f'resume=UUID={resume_uuid}') - self.KERNEL_PARAMS.append(f'resume_offset={resume_offset}') + self._hooks.append('resume') + self._kernel_params.append(f'resume=UUID={resume_uuid}') + self._kernel_params.append(f'resume_offset={resume_offset}') def post_install_check(self, *args :str, **kwargs :str) -> List[str]: return [step for step, flag in self.helper_flags.items() if flag is False] @@ -411,7 +391,7 @@ class Installer: else: pacman_conf.write(line) - def pacstrap(self, *packages: Union[str, List[str]], **kwargs :str) -> bool: + def _pacstrap(self, packages: Union[str, List[str]]) -> bool: if type(packages[0]) in (list, tuple): packages = packages[0] @@ -430,9 +410,9 @@ class Installer: if storage['arguments'].get('silent', False) is False: if input('Would you like to re-try this download? (Y/n): ').lower().strip() in ('', 'y'): - return self.pacstrap(*packages, **kwargs) + return self._pacstrap(packages) - raise RequirementError(f'Could not sync mirrors: {error}', level=logging.ERROR, fg="red") + raise RequirementError(f'Could not sync mirrors: {error}') try: SysCommand(f'/usr/bin/pacstrap -C /etc/pacman.conf -K {self.target} {" ".join(packages)} --noconfirm', peek_output=True) @@ -442,40 +422,44 @@ class Installer: if storage['arguments'].get('silent', False) is False: if input('Would you like to re-try this download? (Y/n): ').lower().strip() in ('', 'y'): - return self.pacstrap(*packages, **kwargs) + return self._pacstrap(packages) raise RequirementError("Pacstrap failed. See /var/log/archinstall/install.log or above message for error details.") - def set_mirrors(self, mirrors :Mapping[str, Iterator[str]]) -> None: + def set_mirrors(self, mirrors: Dict[str, Iterable[str]]): for plugin in plugins.values(): if hasattr(plugin, 'on_mirrors'): if result := plugin.on_mirrors(mirrors): mirrors = result - return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist') + destination = f'{self.target}/etc/pacman.d/mirrorlist' + use_mirrors(mirrors, destination=destination) def genfstab(self, flags :str = '-pU'): self.log(f"Updating {self.target}/etc/fstab", level=logging.INFO) try: - fstab = SysCommand(f'/usr/bin/genfstab {flags} {self.target}') + gen_fstab = SysCommand(f'/usr/bin/genfstab {flags} {self.target}').decode() except SysCallError as error: raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {error}') - with open(f"{self.target}/etc/fstab", 'a') as fstab_fh: - fstab_fh.write(fstab.decode()) + if not gen_fstab: + raise RequirementError(f'Genrating fstab returned empty value') + + with open(f"{self.target}/etc/fstab", 'a') as fp: + fp.write(gen_fstab) if not os.path.isfile(f'{self.target}/etc/fstab'): - raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {fstab}') + raise RequirementError(f'Could not create fstab file') for plugin in plugins.values(): if hasattr(plugin, 'on_genfstab'): if plugin.on_genfstab(self) is True: break - with open(f"{self.target}/etc/fstab", 'a') as fstab_fh: - for entry in self.FSTAB_ENTRIES: - fstab_fh.write(f'{entry}\n') + with open(f"{self.target}/etc/fstab", 'a') as fp: + for entry in self._fstab_entries: + fp.write(f'{entry}\n') for mod in self._disk_config.device_modifications: for part_mod in mod.partitions: @@ -583,7 +567,7 @@ class Installer: # fstrim is owned by util-linux, a dependency of both base and systemd. self.enable_service("fstrim.timer") - def enable_service(self, *services: Union[str, List[str]]) -> None: + def enable_service(self, services: Union[str, List[str]]) -> None: if type(services[0]) in (list, tuple): services = services[0] @@ -611,19 +595,7 @@ class Installer: subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True) def configure_nic(self, network_config: NetworkConfiguration) -> None: - from .systemd import Networkd - - if network_config.dhcp: - conf = Networkd(Match={"Name": network_config.iface}, Network={"DHCP": "yes"}) - else: - network = {"Address": network_config.ip} - if network_config.gateway: - network["Gateway"] = network_config.gateway - if network_config.dns: - dns = network_config.dns - network["DNS"] = dns if isinstance(dns, list) else [dns] - - conf = Networkd(Match={"Name": network_config.iface}, Network=network) + conf = network_config.as_systemd_config() for plugin in plugins.values(): if hasattr(plugin, 'on_configure_nic'): @@ -663,7 +635,7 @@ class Installer: # Otherwise, we can go ahead and add the required package # and enable it's service: else: - self.pacstrap('iwd') + self._pacstrap('iwd') self.enable_service('iwd') for psk in psk_files: @@ -682,12 +654,12 @@ class Installer: if self.helper_flags.get('base', False) is False: def post_install_enable_networkd_resolved(*args :str, **kwargs :str): - self.enable_service('systemd-networkd', 'systemd-resolved') + self.enable_service(['systemd-networkd', 'systemd-resolved']) self.post_base_install.append(post_install_enable_networkd_resolved) # Otherwise, we can go ahead and enable the services else: - self.enable_service('systemd-networkd', 'systemd-resolved') + self.enable_service(['systemd-networkd', 'systemd-resolved']) return True @@ -704,9 +676,9 @@ class Installer: fh.write(f"KEYMAP={storage['arguments']['keyboard-layout']}\n") with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit: - mkinit.write(f"MODULES=({' '.join(self.MODULES)})\n") - mkinit.write(f"BINARIES=({' '.join(self.BINARIES)})\n") - mkinit.write(f"FILES=({' '.join(self.FILES)})\n") + mkinit.write(f"MODULES=({' '.join(self.modules)})\n") + mkinit.write(f"BINARIES=({' '.join(self._binaries)})\n") + mkinit.write(f"FILES=({' '.join(self._files)})\n") if not self._disk_encryption.hsm_device: # For now, if we don't use HSM we revert to the old @@ -714,9 +686,9 @@ class Installer: # This is purely for stability reasons, we're going away from this. # * systemd -> udev # * sd-vconsole -> keymap - self.HOOKS = [hook.replace('systemd', 'udev').replace('sd-vconsole', 'keymap') for hook in self.HOOKS] + self._hooks = [hook.replace('systemd', 'udev').replace('sd-vconsole', 'keymap') for hook in self._hooks] - mkinit.write(f"HOOKS=({' '.join(self.HOOKS)})\n") + mkinit.write(f"HOOKS=({' '.join(self._hooks)})\n") try: SysCommand(f'/usr/bin/arch-chroot {self.target} mkinitcpio {" ".join(flags)}') @@ -736,25 +708,25 @@ class Installer: if (pkg := part.fs_type.installation_pkg) is not None: self.base_packages.append(pkg) if (module := part.fs_type.installation_module) is not None: - self.MODULES.append(module) + self.modules.append(module) if (binary := part.fs_type.installation_binary) is not None: - self.BINARIES.append(binary) + self._binaries.append(binary) # There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed. if part.fs_type.fs_type_mount == 'ntfs3' and part.mountpoint == self.target: - if 'fsck' in self.HOOKS: - self.HOOKS.remove('fsck') + if 'fsck' in self._hooks: + self._hooks.remove('fsck') if part in self._disk_encryption.partitions: if self._disk_encryption.hsm_device: # Required bby mkinitcpio to add support for fido2-device options - self.pacstrap('libfido2') + self._pacstrap('libfido2') - if 'sd-encrypt' not in self.HOOKS: - self.HOOKS.insert(self.HOOKS.index('filesystems'), 'sd-encrypt') + if 'sd-encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('filesystems'), 'sd-encrypt') else: - if 'encrypt' not in self.HOOKS: - self.HOOKS.insert(self.HOOKS.index('filesystems'), 'encrypt') + if 'encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('filesystems'), 'encrypt') if not has_uefi(): self.base_packages.append('grub') @@ -786,7 +758,7 @@ class Installer: else: self.log("The testing flag is not set. This system will be installed without testing repositories enabled.") - self.pacstrap(self.base_packages) + self._pacstrap(self.base_packages) self.helper_flags['base-strapped'] = True # This handles making sure that the repositories we enabled persist on the installed system @@ -826,7 +798,7 @@ class Installer: def setup_swap(self, kind :str = 'zram'): if kind == 'zram': self.log(f"Setting up swap on zram") - self.pacstrap('zram-generator') + self._pacstrap('zram-generator') # We could use the default example below, but maybe not the best idea: https://github.com/archlinux/archinstall/pull/678#issuecomment-962124813 # zram_example_location = '/usr/share/doc/zram-generator/zram-generator.conf.example' @@ -853,7 +825,7 @@ class Installer: return None def _add_systemd_bootloader(self, root_partition: disk.PartitionModification): - self.pacstrap('efibootmgr') + self._pacstrap('efibootmgr') if not has_uefi(): raise HardwareIncompatibilityError @@ -919,7 +891,7 @@ class Installer: # blkid doesn't trigger on loopback devices really well, # so we'll use the old manual method until we get that sorted out. - options_entry = f'rw rootfstype={root_partition.fs_type.fs_type_mount} {" ".join(self.KERNEL_PARAMS)}\n' + options_entry = f'rw rootfstype={root_partition.fs_type.fs_type_mount} {" ".join(self._kernel_params)}\n' for sub_vol in root_partition.btrfs_subvols: if sub_vol.is_root(): @@ -958,7 +930,7 @@ class Installer: boot_partition: disk.PartitionModification, root_partition: disk.PartitionModification ): - self.pacstrap('grub') # no need? + self._pacstrap('grub') # no need? _file = "/etc/default/grub" @@ -977,7 +949,7 @@ class Installer: log(f"GRUB boot partition: {boot_partition.dev_path}", level=logging.INFO) if has_uefi(): - self.pacstrap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? + self._pacstrap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? try: SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable', peek_output=True) @@ -987,8 +959,20 @@ class Installer: except SysCallError as error: raise DiskError(f"Could not install GRUB to {self.target}/boot: {error}") else: + device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) + + if not device: + raise ValueError(f'Can not find block device: {boot_partition.safe_dev_path}') + try: - SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=i386-pc --recheck {boot_partition.parent}', peek_output=True) + cmd = f'/usr/bin/arch-chroot' \ + f' {self.target}' \ + f' grub-install' \ + f' --debug' \ + f' --target=i386-pc' \ + f' --recheck {device.device_info.path}' + + SysCommand(cmd, peek_output=True) except SysCallError as error: raise DiskError(f"Failed to install GRUB boot on {boot_partition.dev_path}: {error}") @@ -1004,7 +988,7 @@ class Installer: boot_partition: disk.PartitionModification, root_partition: disk.PartitionModification ): - self.pacstrap('efibootmgr') + self._pacstrap('efibootmgr') if not has_uefi(): raise HardwareIncompatibilityError @@ -1038,17 +1022,30 @@ class Installer: # TODO: We need to detect if the encrypted device is a whole disk encryption, # or simply a partition encryption. Right now we assume it's a partition (and we always have) log(f'Identifying root partition by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) - kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.fs_type.value} {" ".join(self.KERNEL_PARAMS)}') + kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}') else: log(f'Root partition is an encrypted device identifying by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) - kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.fs_type.value} {" ".join(self.KERNEL_PARAMS)}') + kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}') + + device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) - device = disk.device_handler.get_device_by_partition_path(boot_partition.dev_path) - SysCommand(f'efibootmgr --disk {device.path} --part {device.path} --create --label "{label}" --loader {loader} --unicode \'{" ".join(kernel_parameters)}\' --verbose') + if not device: + raise ValueError(f'Unable to find block device: {boot_partition.safe_dev_path}') + + cmd = f'efibootmgr ' \ + f'--disk {device.device_info.path} ' \ + f'--part {boot_partition.safe_dev_path} ' \ + f'--create ' \ + f'--label "{label}" ' \ + f'--loader {loader} ' \ + f'--unicode \'{" ".join(kernel_parameters)}\' ' \ + f'--verbose' + + SysCommand(cmd) self.helper_flags['bootloader'] = "efistub" - def add_bootloader(self, bootloader: Bootloader) -> bool: + def add_bootloader(self, bootloader: Bootloader): """ Adds a bootloader to the installation instance. Archinstall supports one of three types: @@ -1056,8 +1053,7 @@ class Installer: * grub * efistub (beta) - :param bootloader: Can be one of the three strings - 'systemd-bootctl', 'grub' or 'efistub' (beta) + :param bootloader: Type of bootloader to be added """ for plugin in plugins.values(): @@ -1089,8 +1085,8 @@ class Installer: case Bootloader.Efistub: self._add_efistub_bootloader(boot_partition, root_partition) - def add_additional_packages(self, *packages: Union[str, List[str]]) -> bool: - return self.pacstrap(*packages) + def add_additional_packages(self, packages: Union[str, List[str]]) -> bool: + return self._pacstrap(packages) def _enable_users(self, service: str, users: List[User]): for user in users: @@ -1201,9 +1197,6 @@ class Installer: except SysCallError: return False - def create_file(self, filename :str, owner :Optional[str] = None) -> InstallationFile: - return InstallationFile(self, filename, owner) - def set_keyboard_language(self, language: str) -> bool: log(f"Setting keyboard language to {language}", level=logging.INFO) if len(language.strip()): diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 53816655..e44d65a4 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -482,9 +482,9 @@ class AbstractMenu: if item in self._menus_to_enable(): yield item - def _select_archinstall_language(self, preset_value: Language) -> Language: + def _select_archinstall_language(self, preset: Language) -> Language: from ..user_interaction.general_conf import select_archinstall_language - language = select_archinstall_language(self.translation_handler.translated_languages, preset_value) + language = select_archinstall_language(self.translation_handler.translated_languages, preset) self._translation_handler.activate(language) return language diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 44ac33a6..12dbf1f5 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -3,7 +3,7 @@ from enum import Enum, auto from os import system from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional, Callable -from simple_term_menu import TerminalMenu +from simple_term_menu import TerminalMenu # type: ignore from ..exceptions import RequirementError from ..output import log @@ -29,11 +29,11 @@ class MenuSelection: @property def single_value(self) -> Any: - return self.value + return self.value # type: ignore @property def multi_value(self) -> List[Any]: - return self.value + return self.value # type: ignore class Menu(TerminalMenu): @@ -67,7 +67,7 @@ class Menu(TerminalMenu): preview_command: Optional[Callable] = None, preview_size: float = 0.0, preview_title: str = 'Info', - header: Union[List[str],str] = None, + header: Union[List[str], str] = [], allow_reset: bool = False, allow_reset_warning_msg: Optional[str] = None, clear_screen: bool = True, @@ -141,8 +141,6 @@ class Menu(TerminalMenu): log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) raise RequirementError("Menu() requires an iterable as option.") - self._default_str = str(_('(default)')) - if isinstance(p_options,dict): options = list(p_options.keys()) else: @@ -193,8 +191,7 @@ class Menu(TerminalMenu): if default_option: # if a default value was specified we move that one # to the top of the list and mark it as default as well - default = f'{default_option} {self._default_str}' - self._menu_options = [default] + [o for o in self._menu_options if default_option != o] + self._menu_options = [self._default_menu_value] + [o for o in self._menu_options if default_option != o] if display_back_option and not multi and skip: skip_empty_entries = True @@ -204,7 +201,18 @@ class Menu(TerminalMenu): skip_empty_entries = True self._menu_options += [''] - self._preselection(preset_values,cursor_index) + preset_list: Optional[List[str]] = None + + if preset_values and isinstance(preset_values, str): + preset_list = [preset_values] + + calc_cursor_idx = self._determine_cursor_pos(preset_list, cursor_index) + + # when we're not in multi selection mode we don't care about + # passing the pre-selection list to the menu as the position + # of the cursor is the one determining the pre-selection + if not self._multi: + preset_values = None cursor = "> " main_menu_cursor_style = ("fg_cyan", "bold") @@ -217,8 +225,8 @@ class Menu(TerminalMenu): menu_cursor_style=main_menu_cursor_style, menu_highlight_style=main_menu_style, multi_select=multi, - preselected_entries=self.preset_values, - cursor_index=self.cursor_index, + preselected_entries=preset_values, + cursor_index=calc_cursor_idx, preview_command=lambda x: self._show_preview(preview_command, x), preview_size=preview_size, preview_title=preview_title, @@ -231,12 +239,17 @@ class Menu(TerminalMenu): skip_empty_entries=skip_empty_entries ) + @property + def _default_menu_value(self) -> str: + default_str = str(_('(default)')) + return f'{self._default_option} {default_str}' + def _show_preview(self, preview_command: Optional[Callable], selection: str) -> Optional[str]: if selection == self.back(): return None if preview_command: - if self._default_option is not None and f'{self._default_option} {self._default_str}' == selection: + if self._default_option is not None and self._default_menu_value == selection: selection = self._default_option return preview_command(selection) @@ -249,7 +262,7 @@ class Menu(TerminalMenu): return MenuSelection(type_=MenuSelectionType.Reset) def check_default(elem): - if self._default_option is not None and f'{self._default_option} {self._default_str}' in elem: + if self._default_option is not None and self._default_menu_value in elem: return self._default_option else: return elem @@ -297,31 +310,44 @@ class Menu(TerminalMenu): pos = self._menu_entries.index(value) self.set_cursor_pos(pos) - def _preselection(self,preset_values :Union[str, List[str]] = [], cursor_index : Optional[int] = None): - def from_preset_to_cursor(): - if preset_values: - # if the value is not extant return 0 as cursor index + def _determine_cursor_pos( + self, + preset: Optional[List[str]] = None, + cursor_index: Optional[int] = None + ) -> Optional[int]: + """ + The priority order to determine the cursor position is: + 1. A static cursor position was provided + 2. Preset values have been provided so the cursor will be + positioned on those + 3. A default value for a selection is given so the cursor + will be placed on such + """ + if cursor_index: + return cursor_index + + if preset: + indexes = [] + + for p in preset: try: - if isinstance(preset_values,str): - self.cursor_index = self._menu_options.index(self.preset_values) - else: # should return an error, but this is smoother - self.cursor_index = self._menu_options.index(self.preset_values[0]) - except ValueError: - self.cursor_index = 0 - - self.cursor_index = cursor_index - if not preset_values: - self.preset_values = None - return - - self.preset_values = preset_values + # the options of the table selection menu + # are already escaped so we have to escape + # the preset values as well for the comparison + if '|' in p: + p = p.replace('|', '\\|') + + idx = self._menu_options.index(p) + indexes.append(idx) + except (IndexError, ValueError): + log(f'Error finding index of {p}: {self._menu_options}', level=logging.DEBUG) + + if len(indexes) == 0: + indexes.append(0) + + return indexes[0] + if self._default_option: - if isinstance(preset_values,str) and self._default_option == preset_values: - self.preset_values = f"{preset_values} {self._default_str}" - elif isinstance(preset_values,(list,tuple)) and self._default_option in preset_values: - idx = preset_values.index(self._default_option) - self.preset_values[idx] = f"{preset_values[idx]} {self._default_str}" - if cursor_index is None or not self._multi: - from_preset_to_cursor() - if not self._multi: # Not supported by the infraestructure - self.preset_values = None + return self._menu_options.index(self._default_menu_value) + + return None diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 4bae6d8b..15d0fd6b 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -3,12 +3,22 @@ import pathlib import urllib.error import urllib.request from typing import Union, Iterable, Dict, Any, List +from dataclasses import dataclass from .general import SysCommand from .output import log from .storage import storage -def sort_mirrorlist(raw_data :bytes, sort_order=["https", "http"]) -> bytes: + +@dataclass +class CustomMirror: + url: str + signcheck: str + signoptions: str + name: str + + +def sort_mirrorlist(raw_data :bytes, sort_order: List[str] = ['https', 'http']) -> bytes: """ This function can sort /etc/pacman.d/mirrorlist according to the mirror's URL prefix. By default places HTTPS before HTTP but it also @@ -28,8 +38,9 @@ def sort_mirrorlist(raw_data :bytes, sort_order=["https", "http"]) -> bytes: from server url definitions (commented or uncommented). """ comments_and_whitespaces = b"" + sort_order += ['Unknown'] + categories: Dict[str, List] = {key: [] for key in sort_order} - categories = {key: [] for key in sort_order + ["Unknown"]} for line in raw_data.split(b"\n"): if line[0:2] in (b'##', b''): comments_and_whitespaces += line + b'\n' @@ -82,18 +93,18 @@ def filter_mirrors_by_region(regions :str, return new_list.decode('UTF-8') -def add_custom_mirrors(mirrors: List[str], *args :str, **kwargs :str) -> bool: +def add_custom_mirrors(mirrors: List[CustomMirror]) -> bool: """ This will append custom mirror definitions in pacman.conf - :param mirrors: A list of mirror data according to: `{'url': 'http://url.com', 'signcheck': 'Optional', 'signoptions': 'TrustAll', 'name': 'testmirror'}` - :type mirrors: dict + :param mirrors: A list of custom mirrors + :type mirrors: List[CustomMirror] """ with open('/etc/pacman.conf', 'a') as pacman: for mirror in mirrors: - pacman.write(f"[{mirror['name']}]\n") - pacman.write(f"SigLevel = {mirror['signcheck']} {mirror['signoptions']}\n") - pacman.write(f"Server = {mirror['url']}\n") + pacman.write(f"[{mirror.name}]\n") + pacman.write(f"SigLevel = {mirror.signcheck} {mirror.signoptions}\n") + pacman.write(f"Server = {mirror.url}\n") return True @@ -123,7 +134,7 @@ def insert_mirrors(mirrors :Dict[str, Any], *args :str, **kwargs :str) -> bool: def use_mirrors( regions: Dict[str, Iterable[str]], destination: str = '/etc/pacman.d/mirrorlist' -) -> None: +): log(f'A new package mirror-list has been created: {destination}', level=logging.INFO) with open(destination, 'w') as mirrorlist: for region, mirrors in regions.items(): @@ -146,7 +157,7 @@ def re_rank_mirrors( def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]: - regions = {} + regions: Dict[str, Dict[str, Any]] = {} if storage['arguments']['offline']: with pathlib.Path('/etc/pacman.d/mirrorlist').open('rb') as fh: @@ -170,18 +181,19 @@ def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]: if len(line.strip()) == 0: continue - line = line.decode('UTF-8').strip('\n').strip('\r') - if line[:3] == '## ': - region = line[3:] - elif line[:10] == '#Server = ': + clean_line = line.decode('UTF-8').strip('\n').strip('\r') + + if clean_line[:3] == '## ': + region = clean_line[3:] + elif clean_line[:10] == '#Server = ': regions.setdefault(region, {}) - url = line.lstrip('#Server = ') + url = clean_line.lstrip('#Server = ') regions[region][url] = True - elif line.startswith('Server = '): + elif clean_line.startswith('Server = '): regions.setdefault(region, {}) - url = line.lstrip('Server = ') + url = clean_line.lstrip('Server = ') regions[region][url] = True return regions diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index b7ab690d..66230e24 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -1,8 +1,9 @@ from __future__ import annotations -from dataclasses import dataclass +import logging +from dataclasses import dataclass, field from enum import Enum -from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING +from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING, Tuple from ..output import log from ..storage import storage @@ -24,7 +25,7 @@ class NetworkConfiguration: ip: Optional[str] = None dhcp: bool = True gateway: Optional[str] = None - dns: Union[None, List[str]] = None + dns: List[str] = field(default_factory=list) def __str__(self): if self.is_iso(): @@ -53,6 +54,33 @@ class NetworkConfiguration: return data + def as_systemd_config(self) -> str: + match: List[Tuple[str, str]] = [] + network: List[Tuple[str, str]] = [] + + if self.iface: + match.append(('Name', self.iface)) + + if self.dhcp: + network.append(('DHCP', 'yes')) + else: + if self.ip: + network.append(('Address', self.ip)) + if self.gateway: + network.append(('Gateway', self.gateway)) + for dns in self.dns: + network.append(('DNS', dns)) + + config = {'Match': match, 'Network': network} + + config_str = '' + for top, entries in config.items(): + config_str += f'[{top}]\n' + config_str += '\n'.join([f'{k}={v}' for k, v in entries]) + config_str += '\n\n' + + return config_str + def json(self) -> Dict: # for json serialization when calling json.dumps(...) on this class return self.__dict__ @@ -90,41 +118,14 @@ class NetworkConfigurationHandler: # Perform a copy of the config if self._configuration.is_iso(): installation.copy_iso_network_config( - enable_services=True) # Sources the ISO network configuration to the install medium. + enable_services=True # Sources the ISO network configuration to the install medium. + ) elif self._configuration.is_network_manager(): installation.add_additional_packages(["networkmanager"]) if (profile := storage['arguments'].get('profile_config')) and profile.is_desktop_type_profile: installation.add_additional_packages(["network-manager-applet"]) installation.enable_service('NetworkManager.service') - def _backwards_compability_config(self, config: Union[str,Dict[str, str]]) -> Union[List[NetworkConfiguration], NetworkConfiguration, None]: - def get(config: Dict[str, str], key: str) -> List[str]: - if (value := config.get(key, None)) is not None: - return [value] - return [] - - if isinstance(config, str): # is a ISO network - return NetworkConfiguration(NicType.ISO) - elif config.get('NetworkManager'): # is a network manager configuration - return NetworkConfiguration(NicType.NM) - elif 'ip' in config: - return [NetworkConfiguration( - NicType.MANUAL, - iface=config.get('nic', ''), - ip=config.get('ip'), - gateway=config.get('gateway', ''), - dns=get(config, 'dns'), - dhcp=False - )] - elif 'nic' in config: - return [NetworkConfiguration( - NicType.MANUAL, - iface=config.get('nic', ''), - dhcp=True - )] - else: # not recognized - return None - def _parse_manual_config(self, configs: List[Dict[str, Any]]) -> Optional[List[NetworkConfiguration]]: configurations = [] @@ -145,13 +146,17 @@ class NetworkConfigurationHandler: log(_('Manual nic configuration with no auto DHCP requires an IP address'), fg='red') exit(1) + dns = manual_config.get('dns', []) + if not isinstance(dns, list): + dns = [dns] + configurations.append( NetworkConfiguration( NicType.MANUAL, iface=iface, ip=ip, gateway=manual_config.get('gateway', ''), - dns=manual_config.get('dns', []), + dns=dns, dhcp=False ) ) @@ -176,8 +181,5 @@ class NetworkConfigurationHandler: self._configuration = NetworkConfiguration(type_) else: # manual configuration settings self._configuration = self._parse_manual_config([config]) - else: # old style definitions - network_config = self._backwards_compability_config(config) - if network_config: - return network_config - return None + else: + log(f'Unable to parse network configuration: {config}', level=logging.DEBUG) diff --git a/archinstall/lib/plugins.py b/archinstall/lib/plugins.py index 0ff63610..b1ece04f 100644 --- a/archinstall/lib/plugins.py +++ b/archinstall/lib/plugins.py @@ -3,77 +3,86 @@ import importlib import logging import os import sys -import pathlib import urllib.parse import urllib.request from importlib import metadata +from pathlib import Path from typing import Optional, List -from types import ModuleType from .output import log from .storage import storage plugins = {} + # 1: List archinstall.plugin definitions # 2: Load the plugin entrypoint # 3: Initiate the plugin and store it as .name in plugins for plugin_definition in metadata.entry_points().select(group='archinstall.plugin'): plugin_entrypoint = plugin_definition.load() + try: plugins[plugin_definition.name] = plugin_entrypoint() except Exception as err: - log(err, level=logging.ERROR) + log(f'Error: {err}', level=logging.ERROR) log(f"The above error was detected when loading the plugin: {plugin_definition}", fg="red", level=logging.ERROR) -# The following functions and core are support structures for load_plugin() -def localize_path(profile_path :str) -> str: - if (url := urllib.parse.urlparse(profile_path)).scheme and url.scheme in ('https', 'http'): - converted_path = f"/tmp/{os.path.basename(profile_path).replace('.py', '')}_{hashlib.md5(os.urandom(12)).hexdigest()}.py" +def localize_path(path: Path) -> Path: + """ + Support structures for load_plugin() + """ + url = urllib.parse.urlparse(str(path)) + + if url.scheme and url.scheme in ('https', 'http'): + converted_path = Path(f'/tmp/{path.stem}_{hashlib.md5(os.urandom(12)).hexdigest()}.py') with open(converted_path, "w") as temp_file: temp_file.write(urllib.request.urlopen(url.geturl()).read().decode('utf-8')) return converted_path else: - return profile_path + return path -def import_via_path(path :str, namespace :Optional[str] = None) -> ModuleType: +def import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str]: if not namespace: namespace = os.path.basename(path) if namespace == '__init__.py': - path = pathlib.PurePath(path) namespace = path.parent.name try: spec = importlib.util.spec_from_file_location(namespace, path) - imported = importlib.util.module_from_spec(spec) - sys.modules[namespace] = imported - spec.loader.exec_module(sys.modules[namespace]) + if spec and spec.loader: + imported = importlib.util.module_from_spec(spec) + sys.modules[namespace] = imported + spec.loader.exec_module(sys.modules[namespace]) return namespace except Exception as err: - log(err, level=logging.ERROR) + log(f'Error: {err}', level=logging.ERROR) log(f"The above error was detected when loading the plugin: {path}", fg="red", level=logging.ERROR) try: - del(sys.modules[namespace]) # noqa: E275 - except: + del sys.modules[namespace] + except Exception: pass -def find_nth(haystack :List[str], needle :str, n :int) -> int: - start = haystack.find(needle) - while start >= 0 and n > 1: - start = haystack.find(needle, start + len(needle)) - n -= 1 - return start + return namespace + + +def find_nth(haystack: List[str], needle: str, n: int) -> Optional[int]: + indices = [idx for idx, elem in enumerate(haystack) if elem == needle] + if n <= len(indices): + return indices[n - 1] + return None + -def load_plugin(path :str) -> ModuleType: - parsed_url = urllib.parse.urlparse(path) - log(f"Loading plugin {parsed_url}.", fg="gray", level=logging.INFO) +def load_plugin(path: Path): + namespace: Optional[str] = None + parsed_url = urllib.parse.urlparse(str(path)) + log(f"Loading plugin from url {parsed_url}.", level=logging.INFO) # The Profile was not a direct match on a remote URL if not parsed_url.scheme: @@ -81,9 +90,10 @@ def load_plugin(path :str) -> ModuleType: if os.path.isfile(path): namespace = import_via_path(path) elif parsed_url.scheme in ('https', 'http'): - namespace = import_via_path(localize_path(path)) + localized = localize_path(path) + namespace = import_via_path(localized) - if namespace in sys.modules: + if namespace and namespace in sys.modules: # Version dependency via __archinstall__version__ variable (if present) in the plugin # Any errors in version inconsistency will be handled through normal error handling if not defined. if hasattr(sys.modules[namespace], '__archinstall__version__'): @@ -99,7 +109,7 @@ def load_plugin(path :str) -> ModuleType: plugins[namespace] = sys.modules[namespace].Plugin() log(f"Plugin {plugins[namespace]} has been loaded.", fg="gray", level=logging.INFO) except Exception as err: - log(err, level=logging.ERROR) + log(f'Error: {err}', level=logging.ERROR) log(f"The above error was detected when initiating the plugin: {path}", fg="red", level=logging.ERROR) else: log(f"Plugin '{path}' is missing a valid entry-point or is corrupt.", fg="yellow", level=logging.WARNING) diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index a8b5cc22..824849c3 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -194,23 +194,23 @@ class ProfileHandler: install_session.add_additional_packages(f"{kernel}-headers") # I've had kernel regen fail if it wasn't installed before nvidia-dkms - install_session.add_additional_packages("dkms xorg-server xorg-xinit nvidia-dkms") + install_session.add_additional_packages(['dkms', 'xorg-server', 'xorg-xinit', 'nvidia-dkms']) return elif 'amdgpu' in driver_pkgs: # The order of these two are important if amdgpu is installed #808 - if 'amdgpu' in install_session.MODULES: - install_session.MODULES.remove('amdgpu') - install_session.MODULES.append('amdgpu') + if 'amdgpu' in install_session.modules: + install_session.modules.remove('amdgpu') + install_session.modules.append('amdgpu') - if 'radeon' in install_session.MODULES: - install_session.MODULES.remove('radeon') - install_session.MODULES.append('radeon') + if 'radeon' in install_session.modules: + install_session.modules.remove('radeon') + install_session.modules.append('radeon') install_session.add_additional_packages(additional_pkg) except Exception as err: log(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}", level=logging.WARNING, fg="yellow") # Prep didn't run, so there's no driver to install - install_session.add_additional_packages("xorg-server xorg-xinit") + install_session.add_additional_packages(['xorg-server', 'xorg-xinit']) def install_profile_config(self, install_session: 'Installer', profile_config: ProfileConfiguration): profile = profile_config.profile diff --git a/archinstall/lib/systemd.py b/archinstall/lib/systemd.py index 64ffcae4..6ccbc5f6 100644 --- a/archinstall/lib/systemd.py +++ b/archinstall/lib/systemd.py @@ -1,6 +1,6 @@ import logging import time -from typing import Iterator +from typing import Iterator, Optional from .exceptions import SysCallError from .general import SysCommand, SysCommandWorker, locate_binary from .installer import Installer @@ -8,51 +8,11 @@ from .output import log from .storage import storage -class Ini: - def __init__(self, *args :str, **kwargs :str): - """ - Limited INI handler for now. - Supports multiple keywords through dictionary list items. - """ - self.kwargs = kwargs - - def __str__(self) -> str: - result = '' - first_row_done = False - for top_level in self.kwargs: - if first_row_done: - result += f"\n[{top_level}]\n" - else: - result += f"[{top_level}]\n" - first_row_done = True - - for key, val in self.kwargs[top_level].items(): - if type(val) == list: - for item in val: - result += f"{key}={item}\n" - else: - result += f"{key}={val}\n" - - return result - - -class Systemd(Ini): - """ - Placeholder class to do systemd specific setups. - """ - - -class Networkd(Systemd): - """ - Placeholder class to do systemd-network specific setups. - """ - - class Boot: def __init__(self, installation: Installer): self.instance = installation self.container_name = 'archinstall' - self.session = None + self.session: Optional[SysCommandWorker] = None self.ready = False def __enter__(self) -> 'Boot': @@ -63,17 +23,18 @@ class Boot: self.session = existing_session.session self.ready = existing_session.ready else: + # '-P' or --console=pipe could help us not having to do a bunch + # of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual. self.session = SysCommandWorker([ '/usr/bin/systemd-nspawn', - '-D', self.instance.target, + '-D', str(self.instance.target), '--timezone=off', '-b', '--no-pager', '--machine', self.container_name ]) - # '-P' or --console=pipe could help us not having to do a bunch of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual. - if not self.ready: + if not self.ready and self.session: while self.session.is_alive(): if b' login:' in self.session: self.ready = True @@ -91,25 +52,31 @@ class Boot: log(f"The error above occurred in a temporary boot-up of the installation {self.instance}", level=logging.ERROR, fg="red") shutdown = None - shutdown_exit_code = -1 + shutdown_exit_code: Optional[int] = -1 try: shutdown = SysCommand(f'systemd-run --machine={self.container_name} --pty shutdown now') except SysCallError as error: shutdown_exit_code = error.exit_code - while self.session.is_alive(): - time.sleep(0.25) + if self.session: + while self.session.is_alive(): + time.sleep(0.25) - if shutdown: + if shutdown and shutdown.exit_code: shutdown_exit_code = shutdown.exit_code - if self.session.exit_code == 0 or shutdown_exit_code == 0: + if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0): storage['active_boot'] = None else: - raise SysCallError(f"Could not shut down temporary boot of {self.instance}: {self.session.exit_code}/{shutdown_exit_code}", exit_code=next(filter(bool, [self.session.exit_code, shutdown_exit_code]))) + session_exit_code = self.session.exit_code if self.session else -1 + + raise SysCallError( + f"Could not shut down temporary boot of {self.instance}: {session_exit_code}/{shutdown_exit_code}", + exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])) + ) - def __iter__(self) -> Iterator[str]: + def __iter__(self) -> Iterator[bytes]: if self.session: for value in self.session: yield value diff --git a/archinstall/lib/user_interaction/general_conf.py b/archinstall/lib/user_interaction/general_conf.py index 7a6bb358..9722dc4d 100644 --- a/archinstall/lib/user_interaction/general_conf.py +++ b/archinstall/lib/user_interaction/general_conf.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging import pathlib from typing import List, Any, Optional, Dict, TYPE_CHECKING -from typing import Union from ..locale_helpers import list_keyboard_languages, list_timezones from ..menu import MenuSelectionType, Menu, TextInput @@ -29,13 +28,18 @@ def ask_ntp(preset: bool = True) -> bool: return False if choice.value == Menu.no() else True -def ask_hostname(preset: str = None) -> str: +def ask_hostname(preset: str = '') -> str: while True: - hostname = TextInput(_('Desired hostname for the installation: '), preset).run().strip() + hostname = TextInput( + str(_('Desired hostname for the installation: ')), + preset + ).run().strip() + if hostname: return hostname -def ask_for_a_timezone(preset: str = None) -> str: + +def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: timezones = list_timezones() default = 'UTC' @@ -48,10 +52,12 @@ def ask_for_a_timezone(preset: str = None) -> str: match choice.type_: case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.value + case MenuSelectionType.Selection: return choice.single_value + + return None -def ask_for_audio_selection(desktop: bool = True, preset: Union[str, None] = None) -> Union[str, None]: +def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]: no_audio = str(_('No audio server')) choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] default = 'pipewire' if desktop else no_audio @@ -60,10 +66,12 @@ def ask_for_audio_selection(desktop: bool = True, preset: Union[str, None] = Non match choice.type_: case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.value + case MenuSelectionType.Selection: return choice.single_value + return None -def select_language(preset_value: str = None) -> str: + +def select_language(preset: Optional[str] = None) -> Optional[str]: """ Asks the user to select a language Usually this is combined with :ref:`archinstall.list_keyboard_languages`. @@ -75,17 +83,18 @@ def select_language(preset_value: str = None) -> str: # sort alphabetically and then by length sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) - selected_lang = Menu( + choice = Menu( _('Select keyboard layout'), sorted_kb_lang, - preset_values=preset_value, + preset_values=preset, sort=False ).run() - if selected_lang.value is None: - return preset_value + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value - return selected_lang.value + return None def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: @@ -100,8 +109,10 @@ def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: preselected = None else: preselected = list(preset_values.keys()) + mirrors = list_mirrors() - selected_mirror = Menu( + + choice = Menu( _('Select one of the regions to download packages from'), list(mirrors.keys()), preset_values=preselected, @@ -109,13 +120,18 @@ def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: allow_reset=True ).run() - match selected_mirror.type_: - case MenuSelectionType.Reset: return {} - case MenuSelectionType.Skip: return preset_values - case _: return {selected: mirrors[selected] for selected in selected_mirror.value} + match choice.type_: + case MenuSelectionType.Reset: + return {} + case MenuSelectionType.Skip: + return preset_values + case MenuSelectionType.Selection: + return {selected: mirrors[selected] for selected in choice.multi_value} + + return {} -def select_archinstall_language(languages: List[Language], preset_value: Language) -> Language: +def select_archinstall_language(languages: List[Language], preset: Language) -> Language: # these are the displayed language names which can either be # the english name of a language or, if present, the # name of the language in its own language @@ -128,15 +144,15 @@ def select_archinstall_language(languages: List[Language], preset_value: Languag choice = Menu( title, list(options.keys()), - default_option=preset_value.display_name, + default_option=preset.display_name, preview_size=0.5 ).run() match choice.type_: - case MenuSelectionType.Skip: - return preset_value - case MenuSelectionType.Selection: - return options[choice.value] + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return options[choice.single_value] + + raise ValueError('Language selection not handled') def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: @@ -223,4 +239,6 @@ def select_additional_repositories(preset: List[str]) -> List[str]: match choice.type_: case MenuSelectionType.Skip: return preset case MenuSelectionType.Reset: return [] - case MenuSelectionType.Selection: return choice.value + case MenuSelectionType.Selection: return choice.single_value + + return [] diff --git a/archinstall/lib/user_interaction/locale_conf.py b/archinstall/lib/user_interaction/locale_conf.py index 88aec64e..cdc3423a 100644 --- a/archinstall/lib/user_interaction/locale_conf.py +++ b/archinstall/lib/user_interaction/locale_conf.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, Optional from ..locale_helpers import list_locales from ..menu import Menu, MenuSelectionType @@ -9,33 +9,37 @@ if TYPE_CHECKING: _: Any -def select_locale_lang(preset: str = None) -> str: +def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: locales = list_locales() locale_lang = set([locale.split()[0] for locale in locales]) - selected_locale = Menu( + choice = Menu( _('Choose which locale language to use'), list(locale_lang), sort=True, preset_values=preset ).run() - match selected_locale.type_: - case MenuSelectionType.Selection: return selected_locale.value + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value case MenuSelectionType.Skip: return preset + return None -def select_locale_enc(preset: str = None) -> str: + +def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: locales = list_locales() locale_enc = set([locale.split()[1] for locale in locales]) - selected_locale = Menu( + choice = Menu( _('Choose which locale encoding to use'), list(locale_enc), sort=True, preset_values=preset ).run() - match selected_locale.type_: - case MenuSelectionType.Selection: return selected_locale.value + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value case MenuSelectionType.Skip: return preset + + return None diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index d9c5837c..48d141b7 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -223,7 +223,7 @@ def perform_installation(mountpoint: Path): # 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['services']) + installation.enable_service(archinstall.arguments.get('services', [])) # If the user provided custom commands to be run post-installation, execute them now. if archinstall.arguments.get('custom-commands', None): diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index e2ee6fcb..34e4c022 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -239,7 +239,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): handler.config_installer(installation) if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', None)) + installation.add_additional_packages(archinstall.arguments.get('packages', [])) if users := archinstall.arguments.get('!users', None): installation.create_users(users) @@ -278,7 +278,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): # 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['services']) + installation.enable_service(archinstall.arguments.get('services', [])) # If the user provided custom commands to be run post-installation, execute them now. if archinstall.arguments.get('custom-commands', None): diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index a78b1712..f72f110b 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -147,7 +147,7 @@ def perform_installation(mountpoint: Path): handler.config_installer(installation) if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', None)) + installation.add_additional_packages(archinstall.arguments.get('packages', [])) if users := archinstall.arguments.get('!users', None): installation.create_users(users) @@ -186,7 +186,7 @@ def perform_installation(mountpoint: Path): # 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['services']) + installation.enable_service(archinstall.arguments.get('services', [])) # If the user provided custom commands to be run post-installation, execute them now. if archinstall.arguments.get('custom-commands', None): diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index a08b2d88..00000000 --- a/mypy.ini +++ /dev/null @@ -1,14 +0,0 @@ -[mypy] -python_version = 3.10 -follow_imports = silent -exclude = (?x)(^archinstall/lib/disk/btrfs/btrfssubvolumeinfo\.py$ - | ^archinstall/lib/general\.py$ - | ^archinstall/lib/hardware\.py$ - | ^archinstall/lib/menu/menu\.py$ - | ^archinstall/lib/mirrors\.py$ - | ^archinstall/lib/plugins\.py$ - | ^archinstall/lib/installer\.py$ - | ^archinstall/lib/systemd\.py$ - | ^archinstall/lib/user_interaction/general_conf\.py$ - | ^archinstall/lib/user_interaction/locale_conf\.py$) -files = archinstall/ diff --git a/pyproject.toml b/pyproject.toml index 557418cc..f837ebdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ packages = ["archinstall"] [tool.mypy] python_version = "3.10" +files = "archinstall/" exclude = "tests" [tool.bandit] -- cgit v1.2.3-54-g00ecf From 2d06cf592a15e69911ffa82e7d86d832bf8ca89c Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Thu, 4 May 2023 00:37:55 +1000 Subject: Fix 1772 (#1773) Co-authored-by: Daniel Girtler --- archinstall/lib/models/network_configuration.py | 13 +++++++++---- archinstall/lib/profile/__init__.py | 3 +++ archinstall/scripts/guided.py | 5 ++++- archinstall/scripts/swiss.py | 5 ++++- examples/interactive_installation.py | 5 ++++- 5 files changed, 24 insertions(+), 7 deletions(-) (limited to 'archinstall/scripts') diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index 66230e24..a8795fc1 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -6,7 +6,7 @@ from enum import Enum from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING, Tuple from ..output import log -from ..storage import storage +from ..profile import ProfileConfiguration if TYPE_CHECKING: _: Any @@ -103,7 +103,11 @@ class NetworkConfigurationHandler: def configuration(self): return self._configuration - def config_installer(self, installation: Any): + def config_installer( + self, + installation: Any, + profile_config: Optional[ProfileConfiguration] = None + ): if self._configuration is None: return @@ -122,8 +126,9 @@ class NetworkConfigurationHandler: ) elif self._configuration.is_network_manager(): installation.add_additional_packages(["networkmanager"]) - if (profile := storage['arguments'].get('profile_config')) and profile.is_desktop_type_profile: - installation.add_additional_packages(["network-manager-applet"]) + if profile_config and profile_config.profile: + if profile_config.profile.is_desktop_type_profile(): + installation.add_additional_packages(["network-manager-applet"]) installation.enable_service('NetworkManager.service') def _parse_manual_config(self, configs: List[Dict[str, Any]]) -> Optional[List[NetworkConfiguration]]: diff --git a/archinstall/lib/profile/__init__.py b/archinstall/lib/profile/__init__.py index e69de29b..6e74b0d8 100644 --- a/archinstall/lib/profile/__init__.py +++ b/archinstall/lib/profile/__init__.py @@ -0,0 +1,3 @@ +from .profile_menu import ProfileMenu, select_greeter, select_profile +from .profiles_handler import profile_handler +from .profile_model import ProfileConfiguration diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 48d141b7..6419c1dc 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -181,7 +181,10 @@ def perform_installation(mountpoint: Path): if network_config: handler = NetworkConfigurationHandler(network_config) - handler.config_installer(installation) + handler.config_installer( + installation, + archinstall.arguments.get('profile_config', None) + ) if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': installation.add_additional_packages(archinstall.arguments.get('packages', None)) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 34e4c022..2712807a 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -236,7 +236,10 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): if network_config: handler = models.NetworkConfigurationHandler(network_config) - handler.config_installer(installation) + handler.config_installer( + installation, + archinstall.arguments.get('profile_config', None) + ) if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': installation.add_additional_packages(archinstall.arguments.get('packages', [])) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index f72f110b..8b4e393f 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -144,7 +144,10 @@ def perform_installation(mountpoint: Path): if network_config: handler = NetworkConfigurationHandler(network_config) - handler.config_installer(installation) + handler.config_installer( + installation, + archinstall.arguments.get('profile_config', None) + ) if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': installation.add_additional_packages(archinstall.arguments.get('packages', [])) -- cgit v1.2.3-54-g00ecf From f211906a5afcfad553070748a779c3166d2b3b4b Mon Sep 17 00:00:00 2001 From: Daemon Coder <11915375+codefiles@users.noreply.github.com> Date: Thu, 4 May 2023 10:04:35 -0400 Subject: Remove obsolete enabling of NTP in ISO (#1729) * Remove obsolete enabling of NTP in ISO * Fixed flake8 linting * Remove `activate_ntp()` * Update comment --------- Co-authored-by: Anton Hvornum --- archinstall/lib/global_menu.py | 12 +----------- archinstall/lib/installer.py | 28 ++++++---------------------- archinstall/scripts/guided.py | 3 --- archinstall/scripts/swiss.py | 6 ------ examples/interactive_installation.py | 3 --- 5 files changed, 7 insertions(+), 45 deletions(-) (limited to 'archinstall/scripts') diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index bc9164ee..0ec057f3 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING from . import disk -from .general import SysCommand, secret +from .general import secret from .menu import Selector, AbstractMenu from .models import NetworkConfiguration from .models.bootloader import Bootloader @@ -18,7 +18,6 @@ from .user_interaction import ask_for_audio_selection from .user_interaction import ask_for_bootloader from .user_interaction import ask_for_swap from .user_interaction import ask_hostname -from .user_interaction import ask_ntp from .user_interaction import ask_to_configure_network from .user_interaction import get_password, ask_for_a_timezone from .user_interaction import select_additional_repositories @@ -164,7 +163,6 @@ class GlobalMenu(AbstractMenu): self._menu_options['ntp'] = \ Selector( _('Automatic time sync (NTP)'), - lambda preset: self._select_ntp(preset), default=True) self._menu_options['__separator__'] = \ Selector('') @@ -323,14 +321,6 @@ class GlobalMenu(AbstractMenu): password = get_password(prompt=prompt) return password - def _select_ntp(self, preset :bool = True) -> bool: - ntp = ask_ntp(preset) - - value = str(ntp).lower() - SysCommand(f'timedatectl set-ntp {value}') - - return ntp - def _select_disk_config( self, preset: Optional[disk.DiskLayoutConfiguration] = None diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index c51019fd..726ff3d0 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -162,10 +162,14 @@ class Installer: def _verify_service_stop(self): """ Certain services might be running that affects the system during installation. - Currently, only one such service is "reflector.service" which updates /etc/pacman.d/mirrorlist + One such service is "reflector.service" which updates /etc/pacman.d/mirrorlist We need to wait for it before we continue since we opted in to use a custom mirror/region. """ - log('Waiting for automatic mirror selection (reflector) to complete...', level=logging.INFO) + log('Waiting for time sync (systemd-timesyncd.service) to complete.', level=logging.INFO) + while SysCommand('timedatectl show --property=NTPSynchronized --value').decode().rstrip() != 'yes': + time.sleep(1) + + log('Waiting for automatic mirror selection (reflector) to complete.', level=logging.INFO) while service_state('reflector') not in ('dead', 'failed', 'exited'): time.sleep(1) @@ -282,26 +286,6 @@ class Installer: self._disk_encryption.encryption_password ) - def activate_ntp(self): - """ - If NTP is activated, confirm activiation in the ISO and at least one time-sync finishes - """ - SysCommand('timedatectl set-ntp true') - - logged = False - while service_state('dbus-org.freedesktop.timesync1.service') not in ['running']: - if not logged: - log(f"Waiting for dbus-org.freedesktop.timesync1.service to enter running state", level=logging.INFO) - logged = True - time.sleep(1) - - logged = False - while 'Server: n/a' in SysCommand('timedatectl timesync-status --no-pager --property=Server --value'): - if not logged: - log(f"Waiting for timedatectl timesync-status to report a timesync against a server", level=logging.INFO) - logged = True - time.sleep(1) - def sync_log_to_install_medium(self) -> bool: # Copy over the install log (if there is one) to the install medium if # at least the base has been strapped in, otherwise we won't have a filesystem/structure to copy to. diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 6419c1dc..9906e0a9 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -149,9 +149,6 @@ def perform_installation(mountpoint: Path): # generate encryption key files for the mounted luks devices installation.generate_key_files() - if archinstall.arguments.get('ntp', False): - installation.activate_ntp() - # Set mirrors used by pacstrap (outside of installation) if archinstall.arguments.get('mirror-region', None): use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 2712807a..3bf847b1 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -74,9 +74,6 @@ class SetupMenu(GlobalMenu): self.enable('abort') def exit_callback(self): - if self._data_store.get('ntp', False): - archinstall.SysCommand('timedatectl set-ntp true') - if self._data_store.get('mode', None): archinstall.arguments['mode'] = self._data_store['mode'] log(f"Archinstall will execute under {archinstall.arguments['mode']} mode") @@ -203,9 +200,6 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): # generate encryption key files for the mounted luks devices installation.generate_key_files() - if archinstall.arguments.get('ntp', False): - installation.activate_ntp() - # Set mirrors used by pacstrap (outside of installation) if archinstall.arguments.get('mirror-region', None): use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 8b4e393f..a27ec0f9 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -112,9 +112,6 @@ def perform_installation(mountpoint: Path): # generate encryption key files for the mounted luks devices installation.generate_key_files() - if archinstall.arguments.get('ntp', False): - installation.activate_ntp() - # Set mirrors used by pacstrap (outside of installation) if archinstall.arguments.get('mirror-region', None): use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium -- cgit v1.2.3-54-g00ecf From 89cefb9a1c7d4c4968e7d8645149078e601c9d1c Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 12 May 2023 02:30:09 +1000 Subject: Cleanup imports and unused code (#1801) * Cleanup imports and unused code * Update build check * Keep deprecation exception * Simplify logging --------- Co-authored-by: Daniel Girtler --- .github/workflows/python-build.yml | 5 +- README.md | 2 +- archinstall/__init__.py | 85 +++-- archinstall/default_profiles/desktop.py | 4 +- archinstall/default_profiles/server.py | 9 +- archinstall/lib/boot.py | 111 ++++++ archinstall/lib/configuration.py | 114 +++++- archinstall/lib/disk/device_handler.py | 99 +++--- archinstall/lib/disk/device_model.py | 50 +-- archinstall/lib/disk/encryption_menu.py | 2 +- archinstall/lib/disk/fido.py | 11 +- archinstall/lib/disk/filesystem.py | 11 +- archinstall/lib/disk/partitioning_menu.py | 9 +- archinstall/lib/exceptions.py | 23 +- archinstall/lib/general.py | 88 ++--- archinstall/lib/global_menu.py | 36 +- archinstall/lib/hardware.py | 274 +++++++------- archinstall/lib/installer.py | 225 ++++++------ archinstall/lib/interactions/__init__.py | 20 ++ archinstall/lib/interactions/disk_conf.py | 393 +++++++++++++++++++++ archinstall/lib/interactions/general_conf.py | 243 +++++++++++++ archinstall/lib/interactions/locale_conf.py | 43 +++ archinstall/lib/interactions/manage_users_conf.py | 106 ++++++ archinstall/lib/interactions/network_conf.py | 172 +++++++++ archinstall/lib/interactions/system_conf.py | 117 ++++++ archinstall/lib/interactions/utils.py | 34 ++ archinstall/lib/locale.py | 68 ++++ archinstall/lib/locale_helpers.py | 176 --------- archinstall/lib/luks.py | 31 +- archinstall/lib/menu/abstract_menu.py | 9 +- archinstall/lib/menu/menu.py | 27 +- archinstall/lib/mirrors.py | 7 +- archinstall/lib/models/bootloader.py | 9 +- archinstall/lib/models/network_configuration.py | 14 +- archinstall/lib/networking.py | 53 +-- archinstall/lib/output.py | 154 +++++--- archinstall/lib/pacman.py | 7 +- archinstall/lib/plugins.py | 43 ++- archinstall/lib/profile/profile_menu.py | 2 +- archinstall/lib/profile/profiles_handler.py | 21 +- archinstall/lib/services.py | 11 - archinstall/lib/storage.py | 4 +- archinstall/lib/systemd.py | 110 ------ archinstall/lib/translationhandler.py | 14 +- archinstall/lib/user_interaction/__init__.py | 10 - archinstall/lib/user_interaction/disk_conf.py | 391 -------------------- archinstall/lib/user_interaction/general_conf.py | 244 ------------- archinstall/lib/user_interaction/locale_conf.py | 45 --- .../lib/user_interaction/manage_users_conf.py | 106 ------ archinstall/lib/user_interaction/network_conf.py | 173 --------- archinstall/lib/user_interaction/save_conf.py | 113 ------ archinstall/lib/user_interaction/system_conf.py | 117 ------ archinstall/lib/user_interaction/utils.py | 34 -- archinstall/lib/utils/util.py | 4 +- archinstall/scripts/guided.py | 35 +- archinstall/scripts/minimal.py | 30 +- archinstall/scripts/only_hd.py | 29 +- archinstall/scripts/swiss.py | 50 +-- archinstall/scripts/unattended.py | 9 +- examples/full_automated_installation.py | 11 +- examples/interactive_installation.py | 29 +- examples/mac_address_installation.py | 8 +- examples/minimal_installation.py | 21 +- examples/only_hd_installation.py | 6 +- pyproject.toml | 1 + 65 files changed, 2124 insertions(+), 2388 deletions(-) create mode 100644 archinstall/lib/boot.py create mode 100644 archinstall/lib/interactions/__init__.py create mode 100644 archinstall/lib/interactions/disk_conf.py create mode 100644 archinstall/lib/interactions/general_conf.py create mode 100644 archinstall/lib/interactions/locale_conf.py create mode 100644 archinstall/lib/interactions/manage_users_conf.py create mode 100644 archinstall/lib/interactions/network_conf.py create mode 100644 archinstall/lib/interactions/system_conf.py create mode 100644 archinstall/lib/interactions/utils.py create mode 100644 archinstall/lib/locale.py delete mode 100644 archinstall/lib/locale_helpers.py delete mode 100644 archinstall/lib/services.py delete mode 100644 archinstall/lib/systemd.py delete mode 100644 archinstall/lib/user_interaction/__init__.py delete mode 100644 archinstall/lib/user_interaction/disk_conf.py delete mode 100644 archinstall/lib/user_interaction/general_conf.py delete mode 100644 archinstall/lib/user_interaction/locale_conf.py delete mode 100644 archinstall/lib/user_interaction/manage_users_conf.py delete mode 100644 archinstall/lib/user_interaction/network_conf.py delete mode 100644 archinstall/lib/user_interaction/save_conf.py delete mode 100644 archinstall/lib/user_interaction/system_conf.py delete mode 100644 archinstall/lib/user_interaction/utils.py (limited to 'archinstall/scripts') diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index f98ce160..950ff8f4 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -36,7 +36,10 @@ jobs: - name: Run archinstall run: | python -V - archinstall -v + archinstall --script guided -v + archinstall --script swiss -v + archinstall --script only_hd -v + archinstall --script minimal -v - uses: actions/upload-artifact@v3 with: name: archinstall diff --git a/README.md b/README.md index 720bd487..15646170 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ from archinstall.default_profiles.minimal import MinimalProfile from archinstall.lib.disk.device_model import FilesystemType from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu from archinstall.lib.disk.filesystem import FilesystemHandler -from archinstall.lib.user_interaction.disk_conf import select_disk_config +from archinstall.lib.interactions.disk_conf import select_disk_config fs_type = FilesystemType('ext4') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 6f67d20f..992bd9fa 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -1,42 +1,75 @@ """Arch Linux installer - guided, templates etc.""" import importlib +import os from argparse import ArgumentParser, Namespace +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, Union from .lib import disk from .lib import menu from .lib import models from .lib import packages - -from .lib.exceptions import * -from .lib.general import * -from .lib.hardware import * -from .lib.installer import __packages__, Installer, accessibility_tools_in_use -from .lib.locale_helpers import * -from .lib.luks import * -from .lib.mirrors import * -from .lib.networking import * -from .lib.output import * -from archinstall.lib.profile.profiles_handler import ProfileHandler, profile_handler -from .lib.profile.profile_menu import ProfileConfiguration -from .lib.services import * -from .lib.storage import * -from .lib.systemd import * -from .lib.user_interaction import * +from .lib import exceptions +from .lib import luks +from .lib import locale +from .lib import mirrors +from .lib import networking +from .lib import profile +from .lib import interactions +from . import default_profiles + +from .lib.hardware import SysInfo, AVAILABLE_GFX_DRIVERS +from .lib.installer import Installer, accessibility_tools_in_use +from .lib.output import ( + FormattedOutput, log, error, + check_log_permissions, debug, warn, info +) +from .lib.storage import storage from .lib.global_menu import GlobalMenu -from .lib.translationhandler import TranslationHandler, DeferredTranslation -from .lib.plugins import plugins, load_plugin # This initiates the plugin loading ceremony -from .lib.configuration import * +from .lib.boot import Boot +from .lib.translationhandler import TranslationHandler, Language, DeferredTranslation +from .lib.plugins import plugins, load_plugin +from .lib.configuration import ConfigurationOutput +from .lib.general import ( + generate_password, locate_binary, clear_vt100_escape_codes, + JsonEncoder, JSON, UNSAFE_JSON, SysCommandWorker, SysCommand, + run_custom_user_commands, json_stream_to_structure, secret +) + + +if TYPE_CHECKING: + _: Any -parser = ArgumentParser() __version__ = "2.5.6" storage['__version__'] = __version__ + # add the custome _ as a builtin, it can now be used anywhere in the # project to mark strings as translatable with _('translate me') DeferredTranslation.install() +check_log_permissions() + +# Log various information about hardware before starting the installation. This might assist in troubleshooting +debug(f"Hardware model detected: {SysInfo.sys_vendor()} {SysInfo.product_name()}; UEFI mode: {SysInfo.has_uefi()}") +debug(f"Processor model detected: {SysInfo.cpu_model()}") +debug(f"Memory statistics: {SysInfo.mem_available()} available out of {SysInfo.mem_total()} total installed") +debug(f"Virtualization detected: {SysInfo.virtualization()}; is VM: {SysInfo.is_vm()}") +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: {disk.disk_layouts()}") + + +if os.getuid() != 0: + print(_("Archinstall requires root privileges to run. See --help for more.")) + exit(1) + + +parser = ArgumentParser() + def define_arguments(): """ @@ -61,7 +94,7 @@ def define_arguments(): parser.add_argument("--plugin", nargs="?", type=str) -def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, error :bool = False) -> dict: +def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, err :bool = False) -> dict: """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 @@ -105,14 +138,14 @@ def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, erro config[last_key] = [config[last_key],element] else: config[last_key].append(element) - elif error: + 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: Union[Namespace, dict]) -> dict: +def cleanup_empty_args(args: Union[Namespace, Dict]) -> Dict: """ Takes arguments (dictionary or argparse Namespace) and removes any None values. This ensures clean mergers during dict.update(args) @@ -190,14 +223,14 @@ def load_config(): arguments['disk_config'] = disk.DiskLayoutConfiguration.parse_arg(disk_config) if profile_config := arguments.get('profile_config', None): - arguments['profile_config'] = ProfileConfiguration.parse_arg(profile_config) + arguments['profile_config'] = profile.ProfileConfiguration.parse_arg(profile_config) if arguments.get('mirror-region', None) is not None: if type(arguments.get('mirror-region', None)) is dict: arguments['mirror-region'] = arguments.get('mirror-region', None) else: selected_region = arguments.get('mirror-region', None) - arguments['mirror-region'] = {selected_region: list_mirrors()[selected_region]} + arguments['mirror-region'] = {selected_region: mirrors.list_mirrors()[selected_region]} if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) @@ -230,7 +263,7 @@ def post_process_arguments(arguments): storage['MOUNT_POINT'] = Path(mountpoint) if arguments.get('debug', False): - log(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!", fg="red", level=logging.WARNING) + warn(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!") if arguments.get('plugin', None): path = arguments['plugin'] diff --git a/archinstall/default_profiles/desktop.py b/archinstall/default_profiles/desktop.py index 2351bd08..9d92f822 100644 --- a/archinstall/default_profiles/desktop.py +++ b/archinstall/default_profiles/desktop.py @@ -1,7 +1,7 @@ from typing import Any, TYPE_CHECKING, List, Optional, Dict from archinstall.lib import menu -from archinstall.lib.output import log +from archinstall.lib.output import info from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult, GreeterType @@ -79,7 +79,7 @@ class DesktopProfile(Profile): install_session.add_additional_packages(self.packages) for profile in self._current_selection: - log(f'Installing profile {profile.name}...') + info(f'Installing profile {profile.name}...') install_session.add_additional_packages(profile.packages) install_session.enable_service(profile.services) diff --git a/archinstall/default_profiles/server.py b/archinstall/default_profiles/server.py index e240b3ef..ab758975 100644 --- a/archinstall/default_profiles/server.py +++ b/archinstall/default_profiles/server.py @@ -1,7 +1,6 @@ -import logging from typing import Any, TYPE_CHECKING, List -from archinstall.lib.output import log +from archinstall.lib.output import info from archinstall.lib.menu import MenuSelectionType from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.default_profiles.profile import ProfileType, Profile, SelectResult, TProfile @@ -46,12 +45,12 @@ class ServerProfile(Profile): def install(self, install_session: 'Installer'): server_info = self.current_selection_names() details = ', '.join(server_info) - log(f'Now installing the selected servers: {details}', level=logging.INFO) + info(f'Now installing the selected servers: {details}') for server in self._current_selection: - log(f'Installing {server.name}...', level=logging.INFO) + info(f'Installing {server.name}...') install_session.add_additional_packages(server.packages) install_session.enable_service(server.services) server.install(install_session) - log('If your selections included multiple servers with the same port, you may have to reconfigure them.', fg="yellow", level=logging.INFO) + info('If your selections included multiple servers with the same port, you may have to reconfigure them.') diff --git a/archinstall/lib/boot.py b/archinstall/lib/boot.py new file mode 100644 index 00000000..62c50df3 --- /dev/null +++ b/archinstall/lib/boot.py @@ -0,0 +1,111 @@ +import time +from typing import Iterator, Optional +from .exceptions import SysCallError +from .general import SysCommand, SysCommandWorker, locate_binary +from .installer import Installer +from .output import error +from .storage import storage + + +class Boot: + def __init__(self, installation: Installer): + self.instance = installation + self.container_name = 'archinstall' + self.session: Optional[SysCommandWorker] = None + self.ready = False + + def __enter__(self) -> 'Boot': + if (existing_session := storage.get('active_boot', None)) and existing_session.instance != self.instance: + raise KeyError("Archinstall only supports booting up one instance, and a active session is already active and it is not this one.") + + if existing_session: + self.session = existing_session.session + self.ready = existing_session.ready + else: + # '-P' or --console=pipe could help us not having to do a bunch + # of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual. + self.session = SysCommandWorker([ + '/usr/bin/systemd-nspawn', + '-D', str(self.instance.target), + '--timezone=off', + '-b', + '--no-pager', + '--machine', self.container_name + ]) + + if not self.ready and self.session: + while self.session.is_alive(): + if b' login:' in self.session: + self.ready = True + break + + storage['active_boot'] = self + return self + + def __exit__(self, *args :str, **kwargs :str) -> None: + # b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync. + # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager + + if len(args) >= 2 and args[1]: + error( + args[1], + f"The error above occurred in a temporary boot-up of the installation {self.instance}" + ) + + shutdown = None + shutdown_exit_code: Optional[int] = -1 + + try: + shutdown = SysCommand(f'systemd-run --machine={self.container_name} --pty shutdown now') + except SysCallError as err: + shutdown_exit_code = err.exit_code + + if self.session: + while self.session.is_alive(): + time.sleep(0.25) + + if shutdown and shutdown.exit_code: + shutdown_exit_code = shutdown.exit_code + + if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0): + storage['active_boot'] = None + else: + session_exit_code = self.session.exit_code if self.session else -1 + + raise SysCallError( + f"Could not shut down temporary boot of {self.instance}: {session_exit_code}/{shutdown_exit_code}", + exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])) + ) + + def __iter__(self) -> Iterator[bytes]: + if self.session: + for value in self.session: + yield value + + def __contains__(self, key: bytes) -> bool: + if self.session is None: + return False + + return key in self.session + + def is_alive(self) -> bool: + if self.session is None: + return False + + return self.session.is_alive() + + def SysCommand(self, cmd: list, *args, **kwargs) -> SysCommand: + if cmd[0][0] != '/' and cmd[0][:2] != './': + # This check is also done in SysCommand & SysCommandWorker. + # However, that check is done for `machinectl` and not for our chroot command. + # So this wrapper for SysCommand will do this additionally. + + cmd[0] = locate_binary(cmd[0]) + + return SysCommand(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) + + def SysCommandWorker(self, cmd: list, *args, **kwargs) -> SysCommandWorker: + if cmd[0][0] != '/' and cmd[0][:2] != './': + cmd[0] = locate_binary(cmd[0]) + + return SysCommandWorker(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index 22c41c0d..c3af3a83 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -1,13 +1,13 @@ import os import json import stat -import logging from pathlib import Path from typing import Optional, Dict, Any, TYPE_CHECKING +from .menu import Menu, MenuSelectionType from .storage import storage -from .general import JSON, UNSAFE_JSON -from .output import log +from .general import JSON, UNSAFE_JSON, SysCommand +from .output import debug, info, warn if TYPE_CHECKING: _: Any @@ -69,18 +69,18 @@ class ConfigurationOutput: def show(self): print(_('\nThis is your chosen configuration:')) - log(" -- Chosen configuration --", level=logging.DEBUG) + debug(" -- Chosen configuration --") user_conig = self.user_config_to_json() - log(user_conig, level=logging.INFO) + info(user_conig) print() def _is_valid_path(self, dest_path: Path) -> bool: if (not dest_path.exists()) or not (dest_path.is_dir()): - log( - 'Destination directory {} does not exist or is not a directory,\n Configuration files can not be saved'.format(dest_path.resolve()), - fg="yellow" + warn( + f'Destination directory {dest_path.resolve()} does not exist or is not a directory\n.', + 'Configuration files can not be saved' ) return False return True @@ -111,3 +111,101 @@ class ConfigurationOutput: if self._is_valid_path(dest_path): self.save_user_config(dest_path) self.save_user_creds(dest_path) + + +def save_config(config: Dict): + def preview(selection: str): + if options['user_config'] == selection: + serialized = config_output.user_config_to_json() + return f'{config_output.user_configuration_file}\n{serialized}' + elif options['user_creds'] == selection: + if maybe_serial := config_output.user_credentials_to_json(): + return f'{config_output.user_credentials_file}\n{maybe_serial}' + else: + return str(_('No configuration')) + elif options['all'] == selection: + output = f'{config_output.user_configuration_file}\n' + if config_output.user_credentials_to_json(): + output += f'{config_output.user_credentials_file}\n' + return output[:-1] + return None + + config_output = ConfigurationOutput(config) + + options = { + 'user_config': str(_('Save user configuration')), + 'user_creds': str(_('Save user credentials')), + 'disk_layout': str(_('Save disk layout')), + 'all': str(_('Save all')) + } + + choice = Menu( + _('Choose which configuration to save'), + list(options.values()), + sort=False, + skip=True, + preview_size=0.75, + preview_command=preview + ).run() + + if choice.type_ == MenuSelectionType.Skip: + return + + save_config_value = choice.single_value + saving_key = [k for k, v in options.items() if v == save_config_value][0] + + dirs_to_exclude = [ + '/bin', + '/dev', + '/lib', + '/lib64', + '/lost+found', + '/opt', + '/proc', + '/run', + '/sbin', + '/srv', + '/sys', + '/usr', + '/var', + ] + + debug('Ignore configuration option folders: ' + ','.join(dirs_to_exclude)) + info(_('Finding possible directories to save configuration files ...')) + + find_exclude = '-path ' + ' -prune -o -path '.join(dirs_to_exclude) + ' -prune ' + file_picker_command = f'find / {find_exclude} -o -type d -print0' + + directories = SysCommand(file_picker_command).decode() + + if directories is None: + raise ValueError('Failed to retrieve possible configuration directories') + + possible_save_dirs = list(filter(None, directories.split('\x00'))) + + selection = Menu( + _('Select directory (or directories) for saving configuration files'), + possible_save_dirs, + multi=True, + skip=True, + allow_reset=False, + ).run() + + match selection.type_: + case MenuSelectionType.Skip: + return + + save_dirs = selection.multi_value + + debug(f'Saving {saving_key} configuration files to {save_dirs}') + + if save_dirs is not None: + for save_dir_str in save_dirs: + save_dir = Path(save_dir_str) + if options['user_config'] == save_config_value: + config_output.save_user_config(save_dir) + elif options['user_creds'] == save_config_value: + config_output.save_user_creds(save_dir) + elif options['all'] == save_config_value: + config_output.save_user_config(save_dir) + config_output.save_user_creds(save_dir) diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 13bde77a..4341c53c 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -import logging import os import time from pathlib import Path @@ -24,7 +23,7 @@ from .device_model import ( from ..exceptions import DiskError, UnknownFilesystemFormat from ..general import SysCommand, SysCallError, JSON from ..luks import Luks2 -from ..output import log +from ..output import debug, error, info, warn from ..utils.util import is_subpath if TYPE_CHECKING: @@ -48,11 +47,11 @@ class DeviceHandler(object): for device in getAllDevices(): try: disk = Disk(device) - except DiskLabelException as error: - if 'unrecognised disk label' in getattr(error, 'message', str(error)): + except DiskLabelException as err: + if 'unrecognised disk label' in getattr(error, 'message', str(err)): disk = freshDisk(device, PartitionTable.GPT.value) else: - log(f'Unable to get disk from device: {device}', level=logging.DEBUG) + debug(f'Unable to get disk from device: {device}') continue device_info = _DeviceInfo.from_disk(disk) @@ -93,7 +92,7 @@ class DeviceHandler(object): return FilesystemType(lsblk_info.fstype) if lsblk_info.fstype else None return None except ValueError: - log(f'Could not determine the filesystem: {partition.fileSystem}', level=logging.DEBUG) + debug(f'Could not determine the filesystem: {partition.fileSystem}') return None @@ -137,7 +136,7 @@ class DeviceHandler(object): try: result = SysCommand(f'btrfs subvolume list {mountpoint}') except SysCallError as err: - log(f'Failed to read btrfs subvolume information: {err}', level=logging.DEBUG) + debug(f'Failed to read btrfs subvolume information: {err}') return subvol_infos try: @@ -150,7 +149,7 @@ class DeviceHandler(object): sub_vol_mountpoint = lsblk_info.btrfs_subvol_info.get(name, None) subvol_infos.append(_BtrfsSubvolumeInfo(name, sub_vol_mountpoint)) except json.decoder.JSONDecodeError as err: - log(f"Could not decode lsblk JSON: {result}", fg="red", level=logging.ERROR) + error(f"Could not decode lsblk JSON: {result}") raise err if not lsblk_info.mountpoint: @@ -203,14 +202,14 @@ class DeviceHandler(object): options += additional_parted_options options_str = ' '.join(options) - log(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') + info(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') try: SysCommand(f"/usr/bin/{command} {options_str} {path}") - except SysCallError as error: - msg = f'Could not format {path} with {fs_type.value}: {error.message}' - log(msg, fg='red') - raise DiskError(msg) from error + except SysCallError as err: + msg = f'Could not format {path} with {fs_type.value}: {err.message}' + error(msg) + raise DiskError(msg) from err def _perform_enc_formatting( self, @@ -227,16 +226,16 @@ class DeviceHandler(object): key_file = luks_handler.encrypt() - log(f'Unlocking luks2 device: {dev_path}', level=logging.DEBUG) + debug(f'Unlocking luks2 device: {dev_path}') luks_handler.unlock(key_file=key_file) if not luks_handler.mapper_dev: raise DiskError('Failed to unlock luks device') - log(f'luks2 formatting mapper dev: {luks_handler.mapper_dev}', level=logging.INFO) + info(f'luks2 formatting mapper dev: {luks_handler.mapper_dev}') self._perform_formatting(fs_type, luks_handler.mapper_dev) - log(f'luks2 locking device: {dev_path}', level=logging.INFO) + info(f'luks2 locking device: {dev_path}') luks_handler.lock() def format( @@ -285,7 +284,7 @@ class DeviceHandler(object): # when we require a delete and the partition to be (re)created # already exists then we have to delete it first if requires_delete and part_mod.status in [ModificationStatus.Modify, ModificationStatus.Delete]: - log(f'Delete existing partition: {part_mod.safe_dev_path}', level=logging.INFO) + info(f'Delete existing partition: {part_mod.safe_dev_path}') part_info = self.find_partition(part_mod.safe_dev_path) if not part_info: @@ -325,9 +324,9 @@ class DeviceHandler(object): for flag in part_mod.flags: partition.setFlag(flag.value) - log(f'\tType: {part_mod.type.value}', level=logging.DEBUG) - log(f'\tFilesystem: {part_mod.fs_type.value}', level=logging.DEBUG) - log(f'\tGeometry: {start_sector.value} start sector, {length_sector.value} length', level=logging.DEBUG) + debug(f'\tType: {part_mod.type.value}') + debug(f'\tFilesystem: {part_mod.fs_type.value}') + debug(f'\tGeometry: {start_sector.value} start sector, {length_sector.value} length') try: disk.addPartition(partition=partition, constraint=disk.device.optimalAlignedConstraint) @@ -339,41 +338,41 @@ class DeviceHandler(object): # the partition has a real path now as it was created part_mod.dev_path = Path(partition.path) - info = self._fetch_partuuid(part_mod.dev_path) + lsblk_info = self._fetch_partuuid(part_mod.dev_path) - part_mod.partuuid = info.partuuid - part_mod.uuid = info.uuid + part_mod.partuuid = lsblk_info.partuuid + part_mod.uuid = lsblk_info.uuid except PartitionException as ex: raise DiskError(f'Unable to add partition, most likely due to overlapping sectors: {ex}') from ex def _fetch_partuuid(self, path: Path) -> LsblkInfo: attempts = 3 - info: Optional[LsblkInfo] = None + lsblk_info: Optional[LsblkInfo] = None self.partprobe(path) for attempt_nr in range(attempts): time.sleep(attempt_nr + 1) - info = get_lsblk_info(path) + lsblk_info = get_lsblk_info(path) - if info.partuuid: + if lsblk_info.partuuid: break self.partprobe(path) - if not info or not info.partuuid: - log(f'Unable to determine new partition uuid: {path}\n{info}', level=logging.DEBUG) + if not lsblk_info or not lsblk_info.partuuid: + debug(f'Unable to determine new partition uuid: {path}\n{lsblk_info}') raise DiskError(f'Unable to determine new partition uuid: {path}') - log(f'partuuid found: {info.json()}', level=logging.DEBUG) + debug(f'partuuid found: {lsblk_info.json()}') - return info + return lsblk_info def create_btrfs_volumes( self, part_mod: PartitionModification, enc_conf: Optional['DiskEncryption'] = None ): - log(f'Creating subvolumes: {part_mod.safe_dev_path}', level=logging.INFO) + info(f'Creating subvolumes: {part_mod.safe_dev_path}') luks_handler = None @@ -396,7 +395,7 @@ class DeviceHandler(object): self.mount(part_mod.safe_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) for sub_vol in part_mod.btrfs_subvols: - log(f'Creating subvolume: {sub_vol.name}', level=logging.DEBUG) + debug(f'Creating subvolume: {sub_vol.name}') if luks_handler is not None: subvol_path = self._TMP_BTRFS_MOUNT / sub_vol.name @@ -408,14 +407,14 @@ class DeviceHandler(object): if sub_vol.nodatacow: try: SysCommand(f'chattr +C {subvol_path}') - except SysCallError as error: - raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {error}') + except SysCallError as err: + raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {err}') if sub_vol.compress: try: SysCommand(f'chattr +c {subvol_path}') - except SysCallError as error: - raise DiskError(f'Could not set compress attribute at {subvol_path}: {error}') + except SysCallError as err: + raise DiskError(f'Could not set compress attribute at {subvol_path}: {err}') if luks_handler is not None and luks_handler.mapper_dev is not None: self.umount(luks_handler.mapper_dev) @@ -435,12 +434,12 @@ class DeviceHandler(object): return luks_handler def _umount_all_existing(self, modification: DeviceModification): - log(f'Unmounting all partitions: {modification.device_path}', level=logging.INFO) + info(f'Unmounting all partitions: {modification.device_path}') existing_partitions = self._devices[modification.device_path].partition_infos for partition in existing_partitions: - log(f'Unmounting: {partition.path}', level=logging.DEBUG) + debug(f'Unmounting: {partition.path}') # un-mount for existing encrypted partitions if partition.fs_type == FilesystemType.Crypto_luks: @@ -472,10 +471,10 @@ class DeviceHandler(object): part_table = partition_table.value if partition_table else None disk = freshDisk(modification.device.disk.device, part_table) else: - log(f'Use existing device: {modification.device_path}') + info(f'Use existing device: {modification.device_path}') disk = modification.device.disk - log(f'Creating partitions: {modification.device_path}') + info(f'Creating partitions: {modification.device_path}') # TODO sort by delete first @@ -507,7 +506,7 @@ class DeviceHandler(object): lsblk_info = get_lsblk_info(dev_path) if target_mountpoint in lsblk_info.mountpoints: - log(f'Device already mounted at {target_mountpoint}') + info(f'Device already mounted at {target_mountpoint}') return str_options = ','.join(options) @@ -517,7 +516,7 @@ class DeviceHandler(object): command = f'mount {mount_fs} {str_options} {dev_path} {target_mountpoint}' - log(f'Mounting {dev_path}: command', level=logging.DEBUG) + debug(f'Mounting {dev_path}: command') try: SysCommand(command) @@ -536,10 +535,10 @@ class DeviceHandler(object): raise ex if len(lsblk_info.mountpoints) > 0: - log(f'Partition {mountpoint} is currently mounted at: {[str(m) for m in lsblk_info.mountpoints]}', level=logging.DEBUG) + debug(f'Partition {mountpoint} is currently mounted at: {[str(m) for m in lsblk_info.mountpoints]}') for mountpoint in lsblk_info.mountpoints: - log(f'Unmounting mountpoint: {mountpoint}', level=logging.DEBUG) + debug(f'Unmounting mountpoint: {mountpoint}') command = 'umount' @@ -574,10 +573,10 @@ class DeviceHandler(object): command = 'partprobe' try: - log(f'Calling partprobe: {command}', level=logging.DEBUG) + debug(f'Calling partprobe: {command}') SysCommand(command) - except SysCallError as error: - log(f'"{command}" failed to run: {error}', level=logging.DEBUG) + except SysCallError as err: + error(f'"{command}" failed to run: {err}') def _wipe(self, dev_path: Path): """ @@ -594,7 +593,7 @@ class DeviceHandler(object): This is not intended to be secure, but rather to ensure that auto-discovery tools don't recognize anything here. """ - log(f'Wiping partitions and metadata: {block_device.device_info.path}') + info(f'Wiping partitions and metadata: {block_device.device_info.path}') for partition in block_device.partition_infos: self._wipe(partition.path) @@ -609,8 +608,8 @@ def disk_layouts() -> str: lsblk_info = get_all_lsblk_info() return json.dumps(lsblk_info, indent=4, sort_keys=True, cls=JSON) except SysCallError as err: - log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") + warn(f"Could not return disk layouts: {err}") return '' except json.decoder.JSONDecodeError as err: - log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") + warn(f"Could not return disk layouts: {err}") return '' diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index d57347b7..36dd0c4f 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -2,7 +2,6 @@ from __future__ import annotations import dataclasses import json -import logging import math import time import uuid @@ -18,7 +17,7 @@ from parted import Disk, Geometry, Partition from ..exceptions import DiskError, SysCallError from ..general import SysCommand -from ..output import log +from ..output import debug, error from ..storage import storage if TYPE_CHECKING: @@ -282,7 +281,7 @@ class _PartitionInfo: btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list) def as_json(self) -> Dict[str, Any]: - info = { + part_info = { 'Name': self.name, 'Type': self.type.value, 'Filesystem': self.fs_type.value if self.fs_type else str(_('Unknown')), @@ -293,9 +292,9 @@ class _PartitionInfo: } if self.btrfs_subvol_infos: - info['Btrfs vol.'] = f'{len(self.btrfs_subvol_infos)} subvolumes' + part_info['Btrfs vol.'] = f'{len(self.btrfs_subvol_infos)} subvolumes' - return info + return part_info @classmethod def from_partition( @@ -392,7 +391,7 @@ class SubvolumeModification: mods = [] for entry in subvol_args: if not entry.get('name', None) or not entry.get('mountpoint', None): - log(f'Subvolume arg is missing name: {entry}', level=logging.DEBUG) + debug(f'Subvolume arg is missing name: {entry}') continue mountpoint = Path(entry['mountpoint']) if entry['mountpoint'] else None @@ -705,7 +704,7 @@ class PartitionModification: """ Called for displaying data in table format """ - info = { + part_mod = { 'Status': self.status.value, 'Device': str(self.dev_path) if self.dev_path else '', 'Type': self.type.value, @@ -718,9 +717,9 @@ class PartitionModification: } if self.btrfs_subvols: - info['Btrfs vol.'] = f'{len(self.btrfs_subvols)} subvolumes' + part_mod['Btrfs vol.'] = f'{len(self.btrfs_subvols)} subvolumes' - return info + return part_mod @dataclass @@ -916,36 +915,36 @@ class LsblkInfo: @classmethod def from_json(cls, blockdevice: Dict[str, Any]) -> LsblkInfo: - info = cls() + lsblk_info = cls() for f in cls.fields(): lsblk_field = _clean_field(f, CleanType.Blockdevice) data_field = _clean_field(f, CleanType.Dataclass) val: Any = None - if isinstance(getattr(info, data_field), Path): + if isinstance(getattr(lsblk_info, data_field), Path): val = Path(blockdevice[lsblk_field]) - elif isinstance(getattr(info, data_field), Size): + elif isinstance(getattr(lsblk_info, data_field), Size): val = Size(blockdevice[lsblk_field], Unit.B) else: val = blockdevice[lsblk_field] - setattr(info, data_field, val) + setattr(lsblk_info, data_field, val) - info.children = [LsblkInfo.from_json(child) for child in blockdevice.get('children', [])] + lsblk_info.children = [LsblkInfo.from_json(child) for child in blockdevice.get('children', [])] # sometimes lsblk returns 'mountpoints': [null] - info.mountpoints = [Path(mnt) for mnt in info.mountpoints if mnt] + lsblk_info.mountpoints = [Path(mnt) for mnt in lsblk_info.mountpoints if mnt] fs_roots = [] - for r in info.fsroots: + for r in lsblk_info.fsroots: if r: path = Path(r) # store the fsroot entries without the leading / fs_roots.append(path.relative_to(path.anchor)) - info.fsroots = fs_roots + lsblk_info.fsroots = fs_roots - return info + return lsblk_info class CleanType(Enum): @@ -978,16 +977,16 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = try: result = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}') break - except SysCallError as error: + except SysCallError as err: # Get the output minus the message/info from lsblk if it returns a non-zero exit code. - if error.worker: - err = error.worker.decode('UTF-8') - log(f'Error calling lsblk: {err}', level=logging.DEBUG) + if err.worker: + err_str = err.worker.decode('UTF-8') + debug(f'Error calling lsblk: {err_str}') else: - raise error + raise err if retry_attempt == retry - 1: - raise error + raise err time.sleep(1) @@ -997,11 +996,12 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = blockdevices = block_devices['blockdevices'] return [LsblkInfo.from_json(device) for device in blockdevices] except json.decoder.JSONDecodeError as err: - log(f"Could not decode lsblk JSON: {result}", fg="red", level=logging.ERROR) + error(f"Could not decode lsblk JSON: {result}") raise err raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') + def get_lsblk_info(dev_path: Union[Path, str]) -> LsblkInfo: if infos := _fetch_lsblk_info(dev_path): return infos[0] diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 285270fb..8c64e65e 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -13,7 +13,7 @@ from ..menu import ( MenuSelectionType, TableMenu ) -from ..user_interaction.utils import get_password +from ..interactions.utils import get_password from ..menu import Menu from ..general import secret from .fido import Fido2Device, Fido2 diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 2a53b551..97c38d84 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -1,13 +1,12 @@ from __future__ import annotations import getpass -import logging from pathlib import Path from typing import List, Optional from .device_model import PartitionModification, Fido2Device from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes -from ..output import log +from ..output import error, info class Fido2: @@ -39,7 +38,7 @@ class Fido2: if not cls._loaded or reload: ret: Optional[str] = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') if not ret: - log('Unable to retrieve fido2 devices', level=logging.ERROR) + error('Unable to retrieve fido2 devices') return [] fido_devices: str = clear_vt100_escape_codes(ret) # type: ignore @@ -88,8 +87,4 @@ class Fido2: worker.write(bytes(getpass.getpass(" "), 'UTF-8')) pin_inputted = True - log( - f"You might need to touch the FIDO2 device to unlock it if no prompt comes up after 3 seconds.", - level=logging.INFO, - fg="yellow" - ) + info('You might need to touch the FIDO2 device to unlock it if no prompt comes up after 3 seconds') diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 6ea99340..dc99afce 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging import signal import sys import time @@ -8,8 +7,8 @@ from typing import Any, Optional, TYPE_CHECKING from .device_model import DiskLayoutConfiguration, DiskLayoutType, PartitionTable, FilesystemType, DiskEncryption from .device_handler import device_handler -from ..hardware import has_uefi -from ..output import log +from ..hardware import SysInfo +from ..output import debug from ..menu import Menu if TYPE_CHECKING: @@ -27,13 +26,13 @@ class FilesystemHandler: def perform_filesystem_operations(self, show_countdown: bool = True): if self._disk_config.config_type == DiskLayoutType.Pre_mount: - log('Disk layout configuration is set to pre-mount, not performing any operations', level=logging.DEBUG) + debug('Disk layout configuration is set to pre-mount, not performing any operations') return device_mods = list(filter(lambda x: len(x.partitions) > 0, self._disk_config.device_modifications)) if not device_mods: - log('No modifications required', level=logging.DEBUG) + debug('No modifications required') return device_paths = ', '.join([str(mod.device.device_info.path) for mod in device_mods]) @@ -48,7 +47,7 @@ class FilesystemHandler: # Setup the blockdevice, filesystem (and optionally encryption). # Once that's done, we'll hand over to perform_installation() partition_table = PartitionTable.GPT - if has_uefi() is False: + if SysInfo.has_uefi() is False: partition_table = PartitionTable.MBR for mod in device_mods: diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 686e8c29..89cf6293 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -1,13 +1,12 @@ from __future__ import annotations -import logging from pathlib import Path from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ ModificationStatus from ..menu import Menu, ListManager, MenuSelection, TextInput -from ..output import FormattedOutput, log +from ..output import FormattedOutput, warn from .subvolume_menu import SubvolumeMenu if TYPE_CHECKING: @@ -229,7 +228,7 @@ class PartitioningList(ListManager): if not start_sector or self._validate_sector(start_sector): break - log(f'Invalid start sector entered: {start_sector}', fg='red', level=logging.INFO) + warn(f'Invalid start sector entered: {start_sector}') if not start_sector: start_sector = str(largest_free_area.start) @@ -245,7 +244,7 @@ class PartitioningList(ListManager): if not end_value or self._validate_sector(start_sector, end_value): break - log(f'Invalid end sector entered: {start_sector}', fg='red', level=logging.INFO) + warn(f'Invalid end sector entered: {start_sector}') # override the default value with the user value if end_value: @@ -300,7 +299,7 @@ class PartitioningList(ListManager): if choice.value == Menu.no(): return [] - from ..user_interaction.disk_conf import suggest_single_disk_layout + from ..interactions.disk_conf import suggest_single_disk_layout device_modification = suggest_single_disk_layout(self._device) return device_modification.partitions diff --git a/archinstall/lib/exceptions.py b/archinstall/lib/exceptions.py index a66e4e04..53458d2c 100644 --- a/archinstall/lib/exceptions.py +++ b/archinstall/lib/exceptions.py @@ -3,6 +3,7 @@ from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: from .general import SysCommandWorker + class RequirementError(BaseException): pass @@ -15,10 +16,6 @@ class UnknownFilesystemFormat(BaseException): pass -class ProfileError(BaseException): - pass - - class SysCallError(BaseException): def __init__(self, message :str, exit_code :Optional[int] = None, worker :Optional['SysCommandWorker'] = None) -> None: super(SysCallError, self).__init__(message) @@ -27,22 +24,10 @@ class SysCallError(BaseException): self.worker = worker -class PermissionError(BaseException): - pass - - -class ProfileNotFound(BaseException): - pass - - class HardwareIncompatibilityError(BaseException): pass -class UserError(BaseException): - pass - - class ServiceException(BaseException): pass @@ -51,9 +36,5 @@ class PackageError(BaseException): pass -class TranslationError(BaseException): - pass - - class Deprecated(BaseException): - pass \ No newline at end of file + pass diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 997b7d67..777ee90e 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -1,8 +1,6 @@ from __future__ import annotations -import hashlib import json -import logging import os import secrets import shlex @@ -18,9 +16,10 @@ import urllib.error import pathlib from datetime import datetime, date from typing import Callable, Optional, Dict, Any, List, Union, Iterator, TYPE_CHECKING +from select import epoll, EPOLLIN, EPOLLHUP from .exceptions import RequirementError, SysCallError -from .output import log +from .output import debug, error, info from .storage import storage @@ -28,42 +27,6 @@ if TYPE_CHECKING: from .installer import Installer -if sys.platform == 'linux': - from select import epoll, EPOLLIN, EPOLLHUP -else: - import select - EPOLLIN = 0 - EPOLLHUP = 0 - - class epoll(): - """ #!if windows - Create a epoll() implementation that simulates the epoll() behavior. - This so that the rest of the code doesn't need to worry weither we're using select() or epoll(). - """ - def __init__(self) -> None: - self.sockets: Dict[str, Any] = {} - self.monitoring: Dict[int, Any] = {} - - def unregister(self, fileno :int, *args :List[Any], **kwargs :Dict[str, Any]) -> None: - try: - del(self.monitoring[fileno]) # noqa: E275 - except: - pass - - def register(self, fileno :int, *args :int, **kwargs :Dict[str, Any]) -> None: - self.monitoring[fileno] = True - - def poll(self, timeout: float = 0.05, *args :str, **kwargs :Dict[str, Any]) -> List[Any]: - try: - return [[fileno, 1] for fileno in select.select(list(self.monitoring.keys()), [], [], timeout)[0]] - except OSError: - return [] - - -def gen_uid(entropy_length :int = 256) -> str: - return hashlib.sha512(os.urandom(entropy_length)).hexdigest() - - def generate_password(length :int = 64) -> str: haystack = string.printable # digits, ascii_letters, punctiation (!"#$[] etc) and whitespace return ''.join(secrets.choice(haystack) for i in range(length)) @@ -156,6 +119,7 @@ class JsonEncoder: else: return JsonEncoder._encode(obj) + class JSON(json.JSONEncoder, json.JSONDecoder): """ A safe JSON encoder that will omit private information in dicts (starting with !) @@ -166,6 +130,7 @@ class JSON(json.JSONEncoder, json.JSONDecoder): def encode(self, obj :Any) -> Any: return super(JSON, self).encode(self._encode(obj)) + class UNSAFE_JSON(json.JSONEncoder, json.JSONDecoder): """ UNSAFE_JSON will call/encode and keep private information in dicts (starting with !) @@ -269,7 +234,7 @@ class SysCommandWorker: sys.stdout.flush() if len(args) >= 2 and args[1]: - log(args[1], level=logging.DEBUG, fg='red') + debug(args[1]) if self.exit_code != 0: raise SysCallError( @@ -350,7 +315,7 @@ class SysCommandWorker: self.ended = time.time() break - if self.ended or (got_output is False and pid_exists(self.pid) is False): + if self.ended or (got_output is False and _pid_exists(self.pid) is False): self.ended = time.time() try: wait_status = os.waitpid(self.pid, 0)[1] @@ -396,15 +361,15 @@ class SysCommandWorker: pass except Exception as e: exception_type = type(e).__name__ - log(f"Unexpected {exception_type} occurred in {self.cmd}: {e}", level=logging.ERROR) + error(f"Unexpected {exception_type} occurred in {self.cmd}: {e}") raise e os.execve(self.cmd[0], list(self.cmd), {**os.environ, **self.environment_vars}) if storage['arguments'].get('debug'): - log(f"Executing: {self.cmd}", level=logging.DEBUG) + debug(f"Executing: {self.cmd}") except FileNotFoundError: - log(f"{self.cmd[0]} does not exist.", level=logging.ERROR, fg="red") + error(f"{self.cmd[0]} does not exist.") self.exit_code = 1 return False else: @@ -455,7 +420,7 @@ class SysCommand: # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager if len(args) >= 2 and args[1]: - log(args[1], level=logging.ERROR, fg='red') + error(args[1]) def __iter__(self, *args :List[Any], **kwargs :Dict[str, Any]) -> Iterator[bytes]: if self.session: @@ -535,22 +500,7 @@ class SysCommand: return None -def prerequisite_check() -> bool: - """ - This function is used as a safety check before - continuing with an installation. - - Could be anything from checking that /boot is big enough - to check if nvidia hardware exists when nvidia driver was chosen. - """ - - return True - -def reboot(): - SysCommand("/usr/bin/reboot") - - -def pid_exists(pid: int) -> bool: +def _pid_exists(pid: int) -> bool: try: return any(subprocess.check_output(['/usr/bin/ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip()) except subprocess.CalledProcessError: @@ -559,7 +509,7 @@ def pid_exists(pid: int) -> bool: def run_custom_user_commands(commands :List[str], installation :Installer) -> None: for index, command in enumerate(commands): - log(f'Executing custom command "{command}" ...', level=logging.INFO) + info(f'Executing custom command "{command}" ...') with open(f"{installation.target}/var/tmp/user-command.{index}.sh", "w") as temp_script: temp_script.write(command) @@ -568,6 +518,7 @@ def run_custom_user_commands(commands :List[str], installation :Installer) -> No os.unlink(f"{installation.target}/var/tmp/user-command.{index}.sh") + def json_stream_to_structure(configuration_identifier : str, stream :str, target :dict) -> bool : """ Function to load a stream (file (as name) or valid JSON string into an existing dictionary @@ -582,16 +533,16 @@ def json_stream_to_structure(configuration_identifier : str, stream :str, target try: with urllib.request.urlopen(urllib.request.Request(stream, headers={'User-Agent': 'ArchInstall'})) as response: target.update(json.loads(response.read())) - except urllib.error.HTTPError as error: - log(f"Could not load {configuration_identifier} via {parsed_url} due to: {error}", level=logging.ERROR, fg="red") + except urllib.error.HTTPError as err: + error(f"Could not load {configuration_identifier} via {parsed_url} due to: {err}") return False else: if pathlib.Path(stream).exists(): try: with pathlib.Path(stream).open() as fh: target.update(json.load(fh)) - except Exception as error: - log(f"{configuration_identifier} = {stream} does not contain a valid JSON format: {error}", level=logging.ERROR, fg="red") + except Exception as err: + error(f"{configuration_identifier} = {stream} does not contain a valid JSON format: {err}") return False else: # NOTE: This is a rudimentary check if what we're trying parse is a dict structure. @@ -600,14 +551,15 @@ def json_stream_to_structure(configuration_identifier : str, stream :str, target try: target.update(json.loads(stream)) except Exception as e: - log(f" {configuration_identifier} Contains an invalid JSON format : {e}",level=logging.ERROR, fg="red") + error(f"{configuration_identifier} Contains an invalid JSON format: {e}") return False else: - log(f" {configuration_identifier} is neither a file nor is a JSON string:",level=logging.ERROR, fg="red") + error(f"{configuration_identifier} is neither a file nor is a JSON string") return False return True + def secret(x :str): """ return * with len equal to to the input string """ return '*' * len(x) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index a969d93f..13595132 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -11,24 +11,24 @@ from .models.users import User from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration from .storage import storage -from .user_interaction import add_number_of_parrallel_downloads -from .user_interaction import ask_additional_packages_to_install -from .user_interaction import ask_for_additional_users -from .user_interaction import ask_for_audio_selection -from .user_interaction import ask_for_bootloader -from .user_interaction import ask_for_swap -from .user_interaction import ask_hostname -from .user_interaction import ask_ntp -from .user_interaction import ask_to_configure_network -from .user_interaction import get_password, ask_for_a_timezone -from .user_interaction import select_additional_repositories -from .user_interaction import select_kernel -from .user_interaction import select_language -from .user_interaction import select_locale_enc -from .user_interaction import select_locale_lang -from .user_interaction import select_mirror_regions -from .user_interaction.disk_conf import select_disk_config -from .user_interaction.save_conf import save_config +from .configuration import save_config +from .interactions import add_number_of_parrallel_downloads +from .interactions import ask_additional_packages_to_install +from .interactions import ask_for_additional_users +from .interactions import ask_for_audio_selection +from .interactions import ask_for_bootloader +from .interactions import ask_for_swap +from .interactions import ask_hostname +from .interactions import ask_to_configure_network +from .interactions import get_password, ask_for_a_timezone +from .interactions import select_additional_repositories +from .interactions import select_kernel +from .interactions import select_language +from .interactions import select_locale_enc +from .interactions import select_locale_lang +from .interactions import select_mirror_regions +from .interactions import ask_ntp +from .interactions.disk_conf import select_disk_config if TYPE_CHECKING: _: Any diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 3759725f..b95301f9 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -1,27 +1,12 @@ import os -import logging -from functools import partial +from functools import cached_property from pathlib import Path -from typing import Iterator, Optional, Dict +from typing import Optional, Dict from .general import SysCommand from .networking import list_interfaces, enrich_iface_types from .exceptions import SysCallError -from .output import log - -__packages__ = [ - "mesa", - "xf86-video-amdgpu", - "xf86-video-ati", - "xf86-video-nouveau", - "xf86-video-vmware", - "libva-mesa-driver", - "libva-intel-driver", - "intel-media-driver", - "vulkan-radeon", - "vulkan-intel", - "nvidia", -] +from .output import debug AVAILABLE_GFX_DRIVERS = { # Sub-dicts are layer-2 options to be selected @@ -62,136 +47,125 @@ AVAILABLE_GFX_DRIVERS = { } -def cpuinfo() -> Iterator[dict[str, str]]: - """ - Yields information about the CPUs of the system - """ - cpu_info_path = Path("/proc/cpuinfo") - cpu: Dict[str, str] = {} +class _SysInfo: + def __init__(self): + pass - with cpu_info_path.open() as file: - for line in file: - if not (line := line.strip()): - yield cpu - cpu = {} - continue - - key, value = line.split(":", maxsplit=1) - cpu[key.strip()] = value.strip() - - -def all_meminfo() -> Dict[str, int]: - """ - Returns a dict with memory info if called with no args - or the value of the given key of said dict. - """ - mem_info_path = Path("/proc/meminfo") - mem_info: Dict[str, int] = {} - - with mem_info_path.open() as file: - for line in file: - key, value = line.strip().split(':') - num = value.split()[0] - mem_info[key] = int(num) - - return mem_info - - -def meminfo_for_key(key: str) -> int: - info = all_meminfo() - return info[key] - - -def has_wifi() -> bool: - ifaces = list(list_interfaces().values()) - return 'WIRELESS' in enrich_iface_types(ifaces).values() - - -def has_cpu_vendor(vendor_id: str) -> bool: - return any(cpu.get("vendor_id") == vendor_id for cpu in cpuinfo()) - - -has_amd_cpu = partial(has_cpu_vendor, "AuthenticAMD") - - -has_intel_cpu = partial(has_cpu_vendor, "GenuineIntel") - - -def has_uefi() -> bool: - return os.path.isdir('/sys/firmware/efi') - - -def graphics_devices() -> dict: - cards = {} - for line in SysCommand("lspci"): - if b' VGA ' in line or b' 3D ' in line: - _, identifier = line.split(b': ', 1) - cards[identifier.strip().decode('UTF-8')] = line - return cards - - -def has_nvidia_graphics() -> bool: - return any('nvidia' in x.lower() for x in graphics_devices()) - - -def has_amd_graphics() -> bool: - return any('amd' in x.lower() for x in graphics_devices()) - - -def has_intel_graphics() -> bool: - return any('intel' in x.lower() for x in graphics_devices()) - - -def cpu_vendor() -> Optional[str]: - for cpu in cpuinfo(): - return cpu.get("vendor_id") - - return None - - -def cpu_model() -> Optional[str]: - for cpu in cpuinfo(): - return cpu.get("model name") - - return None - - -def sys_vendor() -> Optional[str]: - with open(f"/sys/devices/virtual/dmi/id/sys_vendor") as vendor: - return vendor.read().strip() - - -def product_name() -> Optional[str]: - with open(f"/sys/devices/virtual/dmi/id/product_name") as product: - return product.read().strip() - - -def mem_available() -> Optional[int]: - return meminfo_for_key('MemAvailable') - - -def mem_free() -> Optional[int]: - return meminfo_for_key('MemFree') - - -def mem_total() -> Optional[int]: - return meminfo_for_key('MemTotal') - - -def virtualization() -> Optional[str]: - try: - return str(SysCommand("systemd-detect-virt")).strip('\r\n') - except SysCallError as error: - log(f"Could not detect virtual system: {error}", level=logging.DEBUG) - - return None - - -def is_vm() -> bool: - try: - result = SysCommand("systemd-detect-virt") - return b"none" not in b"".join(result).lower() - except SysCallError as error: - log(f"System is not running in a VM: {error}", level=logging.DEBUG) - - return False + @cached_property + def cpu_info(self) -> Dict[str, str]: + """ + Returns system cpu information + """ + cpu_info_path = Path("/proc/cpuinfo") + cpu: Dict[str, str] = {} + + with cpu_info_path.open() as file: + for line in file: + if line := line.strip(): + key, value = line.split(":", maxsplit=1) + cpu[key.strip()] = value.strip() + + return cpu + + @cached_property + def mem_info(self) -> Dict[str, int]: + """ + Returns system memory information + """ + mem_info_path = Path("/proc/meminfo") + mem_info: Dict[str, int] = {} + + with mem_info_path.open() as file: + for line in file: + key, value = line.strip().split(':') + num = value.split()[0] + mem_info[key] = int(num) + + return mem_info + + def mem_info_by_key(self, key: str) -> int: + return self.mem_info[key] + + +_sys_info = _SysInfo() + + +class SysInfo: + @staticmethod + def has_wifi() -> bool: + ifaces = list(list_interfaces().values()) + return 'WIRELESS' in enrich_iface_types(ifaces).values() + + @staticmethod + def has_uefi() -> bool: + return os.path.isdir('/sys/firmware/efi') + + @staticmethod + def _graphics_devices() -> Dict[str, str]: + cards: Dict[str, str] = {} + for line in SysCommand("lspci"): + if b' VGA ' in line or b' 3D ' in line: + _, identifier = line.split(b': ', 1) + cards[identifier.strip().decode('UTF-8')] = str(line) + return cards + + @staticmethod + def has_nvidia_graphics() -> bool: + return any('nvidia' in x.lower() for x in SysInfo._graphics_devices()) + + @staticmethod + def has_amd_graphics() -> bool: + return any('amd' in x.lower() for x in SysInfo._graphics_devices()) + + @staticmethod + def has_intel_graphics() -> bool: + return any('intel' in x.lower() for x in SysInfo._graphics_devices()) + + @staticmethod + def cpu_vendor() -> Optional[str]: + return _sys_info.cpu_info.get('vendor_id', None) + + @staticmethod + def cpu_model() -> Optional[str]: + return _sys_info.cpu_info.get('model name', None) + + @staticmethod + def sys_vendor() -> str: + with open(f"/sys/devices/virtual/dmi/id/sys_vendor") as vendor: + return vendor.read().strip() + + @staticmethod + def product_name() -> str: + with open(f"/sys/devices/virtual/dmi/id/product_name") as product: + return product.read().strip() + + @staticmethod + def mem_available() -> int: + return _sys_info.mem_info_by_key('MemAvailable') + + @staticmethod + def mem_free() -> int: + return _sys_info.mem_info_by_key('MemFree') + + @staticmethod + def mem_total() -> int: + return _sys_info.mem_info_by_key('MemTotal') + + @staticmethod + def virtualization() -> Optional[str]: + try: + return str(SysCommand("systemd-detect-virt")).strip('\r\n') + except SysCallError as err: + debug(f"Could not detect virtual system: {err}") + + return None + + @staticmethod + def is_vm() -> bool: + try: + result = SysCommand("systemd-detect-virt") + return b"none" not in b"".join(result).lower() + except SysCallError as err: + debug(f"System is not running in a VM: {err}") + + return False diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 726ff3d0..3c427ab2 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1,5 +1,4 @@ import glob -import logging import os import re import shlex @@ -12,17 +11,16 @@ from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable, It from . import disk from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .general import SysCommand -from .hardware import has_uefi, is_vm, cpu_vendor -from .locale_helpers import verify_keyboard_layout, verify_x11_keyboard_layout +from .hardware import SysInfo +from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .luks import Luks2 from .mirrors import use_mirrors from .models.bootloader import Bootloader from .models.network_configuration import NetworkConfiguration from .models.users import User -from .output import log +from .output import log, error, info, warn, debug from .pacman import run_pacman from .plugins import plugins -from .services import service_state from .storage import storage if TYPE_CHECKING: @@ -41,28 +39,6 @@ def accessibility_tools_in_use() -> bool: class Installer: - """ - `Installer()` is the wrapper for most basic installation steps. - It also wraps :py:func:`~archinstall.Installer.pacstrap` among other things. - - :param partition: Requires a partition as the first argument, this is - so that the installer can mount to `mountpoint` and strap packages there. - :type partition: class:`archinstall.Partition` - - :param boot_partition: There's two reasons for needing a boot partition argument, - The first being so that `mkinitcpio` can place the `vmlinuz` kernel at the right place - during the `pacstrap` or `linux` and the base packages for a minimal installation. - The second being when :py:func:`~archinstall.Installer.add_bootloader` is called, - A `boot_partition` must be known to the installer before this is called. - :type boot_partition: class:`archinstall.Partition` - - :param profile: A profile to install, this is optional and can be called later manually. - This just simplifies the process by not having to call :py:func:`~archinstall.Installer.install_profile` later on. - :type profile: str, optional - - :param hostname: The given /etc/hostname for the machine. - :type hostname: str, optional - """ def __init__( self, target: Path, @@ -71,6 +47,10 @@ class Installer: base_packages: List[str] = [], kernels: Optional[List[str]] = None ): + """ + `Installer()` is the wrapper for most basic installation steps. + It also wraps :py:func:`~archinstall.Installer.pacstrap` among other things. + """ if not base_packages: base_packages = __packages__[:3] @@ -126,7 +106,7 @@ class Installer: def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: - log(exc_val, fg='red', level=logging.ERROR) + error(exc_val) self.sync_log_to_install_medium() @@ -137,48 +117,41 @@ class Installer: raise exc_val if not (missing_steps := self.post_install_check()): - self.log('Installation completed without any errors. You may now reboot.', fg='green', level=logging.INFO) + log('Installation completed without any errors. You may now reboot.', fg='green') self.sync_log_to_install_medium() return True else: - self.log('Some required steps were not successfully installed/configured before leaving the installer:', fg='red', level=logging.WARNING) + warn('Some required steps were not successfully installed/configured before leaving the installer:') for step in missing_steps: - self.log(f' - {step}', fg='red', level=logging.WARNING) + warn(f' - {step}') - self.log(f"Detailed error logs can be found at: {storage['LOG_PATH']}", level=logging.WARNING) - self.log("Submit this zip file as an issue to https://github.com/archlinux/archinstall/issues", level=logging.WARNING) + warn(f"Detailed error logs can be found at: {storage['LOG_PATH']}") + warn("Submit this zip file as an issue to https://github.com/archlinux/archinstall/issues") self.sync_log_to_install_medium() return False - def log(self, *args :str, level :int = logging.DEBUG, **kwargs :str): - """ - installer.log() wraps output.log() mainly to set a default log-level for this install session. - Any manual override can be done per log() call. - """ - log(*args, level=level, **kwargs) - def _verify_service_stop(self): """ Certain services might be running that affects the system during installation. One such service is "reflector.service" which updates /etc/pacman.d/mirrorlist We need to wait for it before we continue since we opted in to use a custom mirror/region. """ - log('Waiting for time sync (systemd-timesyncd.service) to complete.', level=logging.INFO) + info('Waiting for time sync (systemd-timesyncd.service) to complete.') while SysCommand('timedatectl show --property=NTPSynchronized --value').decode().rstrip() != 'yes': time.sleep(1) - log('Waiting for automatic mirror selection (reflector) to complete.', level=logging.INFO) - while service_state('reflector') not in ('dead', 'failed', 'exited'): + info('Waiting for automatic mirror selection (reflector) to complete.') + while self._service_state('reflector') not in ('dead', 'failed', 'exited'): time.sleep(1) - log('Waiting pacman-init.service to complete.', level=logging.INFO) - while service_state('pacman-init') not in ('dead', 'failed', 'exited'): + info('Waiting pacman-init.service to complete.') + while self._service_state('pacman-init') not in ('dead', 'failed', 'exited'): time.sleep(1) - log('Waiting Arch Linux keyring sync (archlinux-keyring-wkd-sync) to complete.', level=logging.INFO) - while service_state('archlinux-keyring-wkd-sync') not in ('dead', 'failed', 'exited'): + info('Waiting Arch Linux keyring sync (archlinux-keyring-wkd-sync) to complete.') + while self._service_state('archlinux-keyring-wkd-sync') not in ('dead', 'failed', 'exited'): time.sleep(1) def _verify_boot_part(self): @@ -204,7 +177,7 @@ class Installer: self._verify_service_stop() def mount_ordered_layout(self): - log('Mounting partitions in order', level=logging.INFO) + info('Mounting partitions in order') for mod in self._disk_config.device_modifications: # partitions have to mounted in the right order on btrfs the mountpoint will @@ -275,7 +248,7 @@ class Installer: ) if gen_enc_file and not part_mod.is_root(): - log(f'Creating key-file: {part_mod.dev_path}', level=logging.INFO) + info(f'Creating key-file: {part_mod.dev_path}') luks_handler.create_keyfile(self.target) if part_mod.is_root() and not gen_enc_file: @@ -384,25 +357,25 @@ class Installer: if (result := plugin.on_pacstrap(packages)): packages = result - self.log(f'Installing packages: {packages}', level=logging.INFO) + info(f'Installing packages: {packages}') # TODO: We technically only need to run the -Syy once. try: run_pacman('-Syy', default_cmd='/usr/bin/pacman') - except SysCallError as error: - self.log(f'Could not sync a new package database: {error}', level=logging.ERROR, fg="red") + except SysCallError as err: + error(f'Could not sync a new package database: {err}') if storage['arguments'].get('silent', False) is False: if input('Would you like to re-try this download? (Y/n): ').lower().strip() in ('', 'y'): return self._pacstrap(packages) - raise RequirementError(f'Could not sync mirrors: {error}') + raise RequirementError(f'Could not sync mirrors: {err}') try: SysCommand(f'/usr/bin/pacstrap -C /etc/pacman.conf -K {self.target} {" ".join(packages)} --noconfirm', peek_output=True) return True - except SysCallError as error: - self.log(f'Could not strap in packages: {error}', level=logging.ERROR, fg="red") + except SysCallError as err: + error(f'Could not strap in packages: {err}') if storage['arguments'].get('silent', False) is False: if input('Would you like to re-try this download? (Y/n): ').lower().strip() in ('', 'y'): @@ -420,12 +393,12 @@ class Installer: use_mirrors(mirrors, destination=destination) def genfstab(self, flags :str = '-pU'): - self.log(f"Updating {self.target}/etc/fstab", level=logging.INFO) + info(f"Updating {self.target}/etc/fstab") try: gen_fstab = SysCommand(f'/usr/bin/genfstab {flags} {self.target}').decode() - except SysCallError as error: - raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {error}') + except SysCallError as err: + raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {err}') if not gen_fstab: raise RequirementError(f'Genrating fstab returned empty value') @@ -530,24 +503,20 @@ class Installer: return True else: - self.log( - f"Time zone {zone} does not exist, continuing with system default.", - level=logging.WARNING, - fg='red' - ) + warn(f'Time zone {zone} does not exist, continuing with system default') return False def activate_time_syncronization(self) -> None: - self.log('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers.', level=logging.INFO) + info('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers') self.enable_service('systemd-timesyncd') def enable_espeakup(self) -> None: - self.log('Enabling espeakup.service for speech synthesis (accessibility).', level=logging.INFO) + info('Enabling espeakup.service for speech synthesis (accessibility)') self.enable_service('espeakup') def enable_periodic_trim(self) -> None: - self.log("Enabling periodic TRIM") + info("Enabling periodic TRIM") # fstrim is owned by util-linux, a dependency of both base and systemd. self.enable_service("fstrim.timer") @@ -556,12 +525,12 @@ class Installer: services = [services] for service in services: - self.log(f'Enabling service {service}', level=logging.INFO) + info(f'Enabling service {service}') try: self.arch_chroot(f'systemctl enable {service}') - except SysCallError as error: - raise ServiceException(f"Unable to start service {service}: {error}") + except SysCallError as err: + raise ServiceException(f"Unable to start service {service}: {err}") for plugin in plugins.values(): if hasattr(plugin, 'on_service'): @@ -713,11 +682,11 @@ class Installer: if 'encrypt' not in self._hooks: self._hooks.insert(self._hooks.index('filesystems'), 'encrypt') - if not has_uefi(): + if not SysInfo.has_uefi(): self.base_packages.append('grub') - if not is_vm(): - vendor = cpu_vendor() + if not SysInfo.is_vm(): + vendor = SysInfo.cpu_vendor() if vendor == "AuthenticAMD": self.base_packages.append("amd-ucode") if (ucode := Path(f"{self.target}/boot/amd-ucode.img")).exists(): @@ -727,21 +696,21 @@ class Installer: if (ucode := Path(f"{self.target}/boot/intel-ucode.img")).exists(): ucode.unlink() else: - self.log(f"Unknown CPU vendor '{vendor}' detected. Archinstall won't install any ucode.", level=logging.DEBUG) + debug(f"Unknown CPU vendor '{vendor}' detected. Archinstall won't install any ucode") # Determine whether to enable multilib/testing repositories before running pacstrap if testing flag is set. # This action takes place on the host system as pacstrap copies over package repository lists. if multilib: - self.log("The multilib flag is set. This system will be installed with the multilib repository enabled.") + info("The multilib flag is set. This system will be installed with the multilib repository enabled.") self.enable_multilib_repository() else: - self.log("The multilib flag is not set. This system will be installed without multilib repositories enabled.") + info("The multilib flag is not set. This system will be installed without multilib repositories enabled.") if testing: - self.log("The testing flag is set. This system will be installed with testing repositories enabled.") + info("The testing flag is set. This system will be installed with testing repositories enabled.") self.enable_testing_repositories(multilib) else: - self.log("The testing flag is not set. This system will be installed without testing repositories enabled.") + info("The testing flag is not set. This system will be installed without testing repositories enabled.") self._pacstrap(self.base_packages) self.helper_flags['base-strapped'] = True @@ -773,7 +742,7 @@ class Installer: # Run registered post-install hooks for function in self.post_base_install: - self.log(f"Running post-installation hook: {function}", level=logging.INFO) + info(f"Running post-installation hook: {function}") function(self) for plugin in plugins.values(): @@ -782,7 +751,7 @@ class Installer: def setup_swap(self, kind :str = 'zram'): if kind == 'zram': - self.log(f"Setting up swap on zram") + info(f"Setting up swap on zram") self._pacstrap('zram-generator') # We could use the default example below, but maybe not the best idea: https://github.com/archlinux/archinstall/pull/678#issuecomment-962124813 @@ -812,7 +781,7 @@ class Installer: def _add_systemd_bootloader(self, root_partition: disk.PartitionModification): self._pacstrap('efibootmgr') - if not has_uefi(): + if not SysInfo.has_uefi(): raise HardwareIncompatibilityError # TODO: Ideally we would want to check if another config @@ -862,16 +831,18 @@ class Installer: entry.write(f'# Created on: {self.init_time}\n') entry.write(f'title Arch Linux ({kernel}{variant})\n') entry.write(f"linux /vmlinuz-{kernel}\n") - if not is_vm(): - vendor = cpu_vendor() + if not SysInfo.is_vm(): + vendor = SysInfo.cpu_vendor() if vendor == "AuthenticAMD": entry.write("initrd /amd-ucode.img\n") elif vendor == "GenuineIntel": entry.write("initrd /intel-ucode.img\n") else: - self.log( - f"Unknown CPU vendor '{vendor}' detected. Archinstall won't add any ucode to systemd-boot config.", - level=logging.DEBUG) + debug( + f"Unknown CPU vendor '{vendor}' detected.", + "Archinstall won't add any ucode to systemd-boot config.", + ) + entry.write(f"initrd /initramfs-{kernel}{variant}.img\n") # blkid doesn't trigger on loopback devices really well, # so we'll use the old manual method until we get that sorted out. @@ -890,7 +861,7 @@ class Installer: if root_partition.fs_type.is_crypto(): # TODO: We need to detect if the encrypted device is a whole disk encryption, # or simply a partition encryption. Right now we assume it's a partition (and we always have) - log('Root partition is an encrypted device, identifying by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + debug('Root partition is an encrypted device, identifying by PARTUUID: {root_partition.partuuid}') kernel_options = f"options" @@ -905,7 +876,7 @@ class Installer: entry.write(f'{kernel_options} root=/dev/mapper/luksdev {options_entry}') else: - log(f'Identifying root partition by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + debug(f'Identifying root partition by PARTUUID: {root_partition.partuuid}') entry.write(f'options root=PARTUUID={root_partition.partuuid} {options_entry}') self.helper_flags['bootloader'] = 'systemd' @@ -920,7 +891,7 @@ class Installer: _file = "/etc/default/grub" if root_partition.fs_type.is_crypto(): - log(f"Using UUID {root_partition.uuid} as encrypted root identifier", level=logging.DEBUG) + debug(f"Using UUID {root_partition.uuid} as encrypted root identifier") cmd_line_linux = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"cryptdevice=UUID={root_partition.uuid}:cryptlvm rootfstype={root_partition.fs_type.value}\"/'" enable_cryptdisk = "sed -i 's/#GRUB_ENABLE_CRYPTODISK=y/GRUB_ENABLE_CRYPTODISK=y/'" @@ -931,9 +902,9 @@ class Installer: SysCommand(f"/usr/bin/arch-chroot {self.target} {cmd_line_linux} {_file}") - log(f"GRUB boot partition: {boot_partition.dev_path}", level=logging.INFO) + info(f"GRUB boot partition: {boot_partition.dev_path}") - if has_uefi(): + if SysInfo.has_uefi(): self._pacstrap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? try: @@ -941,8 +912,8 @@ class Installer: except SysCallError: try: SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable', peek_output=True) - except SysCallError as error: - raise DiskError(f"Could not install GRUB to {self.target}/boot: {error}") + except SysCallError as err: + raise DiskError(f"Could not install GRUB to {self.target}/boot: {err}") else: device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) @@ -958,13 +929,13 @@ class Installer: f' --recheck {device.device_info.path}' SysCommand(cmd, peek_output=True) - except SysCallError as error: - raise DiskError(f"Failed to install GRUB boot on {boot_partition.dev_path}: {error}") + except SysCallError as err: + raise DiskError(f"Failed to install GRUB boot on {boot_partition.dev_path}: {err}") try: SysCommand(f'/usr/bin/arch-chroot {self.target} grub-mkconfig -o /boot/grub/grub.cfg') - except SysCallError as error: - raise DiskError(f"Could not configure GRUB: {error}") + except SysCallError as err: + raise DiskError(f"Could not configure GRUB: {err}") self.helper_flags['bootloader'] = "grub" @@ -975,7 +946,7 @@ class Installer: ): self._pacstrap('efibootmgr') - if not has_uefi(): + if not SysInfo.has_uefi(): raise HardwareIncompatibilityError # TODO: Ideally we would want to check if another config @@ -989,14 +960,14 @@ class Installer: kernel_parameters = [] - if not is_vm(): - vendor = cpu_vendor() + if not SysInfo.is_vm(): + vendor = SysInfo.cpu_vendor() if vendor == "AuthenticAMD": kernel_parameters.append("initrd=\\amd-ucode.img") elif vendor == "GenuineIntel": kernel_parameters.append("initrd=\\intel-ucode.img") else: - self.log(f"Unknown CPU vendor '{vendor}' detected. Archinstall won't add any ucode to firmware boot entry.", level=logging.DEBUG) + debug(f"Unknown CPU vendor '{vendor}' detected. Archinstall won't add any ucode to firmware boot entry.") kernel_parameters.append(f"initrd=\\initramfs-{kernel}.img") @@ -1006,10 +977,10 @@ class Installer: if root_partition.fs_type.is_crypto(): # TODO: We need to detect if the encrypted device is a whole disk encryption, # or simply a partition encryption. Right now we assume it's a partition (and we always have) - log(f'Identifying root partition by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + debug(f'Identifying root partition by PARTUUID: {root_partition.partuuid}') kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}') else: - log(f'Root partition is an encrypted device identifying by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + debug(f'Root partition is an encrypted device identifying by PARTUUID: {root_partition.partuuid}') kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}') device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) @@ -1060,7 +1031,7 @@ class Installer: if root_partition is None: raise ValueError(f'Could not detect root at mountpoint {self.target}') - self.log(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}', level=logging.INFO) + info(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}') match bootloader: case Bootloader.Systemd: @@ -1078,7 +1049,7 @@ class Installer: self.arch_chroot(f'systemctl enable --user {service}', run_as=user.username) def enable_sudo(self, entity: str, group :bool = False): - self.log(f'Enabling sudo permissions for {entity}.', level=logging.INFO) + info(f'Enabling sudo permissions for {entity}') sudoers_dir = f"{self.target}/etc/sudoers.d" @@ -1127,11 +1098,11 @@ class Installer: handled_by_plugin = result if not handled_by_plugin: - self.log(f'Creating user {user}', level=logging.INFO) + info(f'Creating user {user}') try: SysCommand(f'/usr/bin/arch-chroot {self.target} useradd -m -G wheel {user}') - except SysCallError as error: - raise SystemError(f"Could not create user inside installation: {error}") + except SysCallError as err: + raise SystemError(f"Could not create user inside installation: {err}") for plugin in plugins.values(): if hasattr(plugin, 'on_user_created'): @@ -1149,7 +1120,7 @@ class Installer: self.helper_flags['user'] = True def user_set_pw(self, user :str, password :str) -> bool: - self.log(f'Setting password for {user}', level=logging.INFO) + info(f'Setting password for {user}') if user == 'root': # This means the root account isn't locked/disabled with * in /etc/passwd @@ -1166,7 +1137,7 @@ class Installer: return False def user_set_shell(self, user :str, shell :str) -> bool: - self.log(f'Setting shell for {user} to {shell}', level=logging.INFO) + info(f'Setting shell for {user} to {shell}') try: SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c \"chsh -s {shell} {user}\"") @@ -1183,49 +1154,59 @@ class Installer: return False def set_keyboard_language(self, language: str) -> bool: - log(f"Setting keyboard language to {language}", level=logging.INFO) + info(f"Setting keyboard language to {language}") + if len(language.strip()): if not verify_keyboard_layout(language): - self.log(f"Invalid keyboard language specified: {language}", fg="red", level=logging.ERROR) + error(f"Invalid keyboard language specified: {language}") return False # In accordance with https://github.com/archlinux/archinstall/issues/107#issuecomment-841701968 # Setting an empty keymap first, allows the subsequent call to set layout for both console and x11. - from .systemd import Boot + from .boot import Boot with Boot(self) as session: os.system('/usr/bin/systemd-run --machine=archinstall --pty localectl set-keymap ""') try: session.SysCommand(["localectl", "set-keymap", language]) - except SysCallError as error: - raise ServiceException(f"Unable to set locale '{language}' for console: {error}") + except SysCallError as err: + raise ServiceException(f"Unable to set locale '{language}' for console: {err}") - self.log(f"Keyboard language for this installation is now set to: {language}") + info(f"Keyboard language for this installation is now set to: {language}") else: - self.log('Keyboard language was not changed from default (no language specified).', fg="yellow", level=logging.INFO) + info('Keyboard language was not changed from default (no language specified)') return True def set_x11_keyboard_language(self, language: str) -> bool: - log(f"Setting x11 keyboard language to {language}", level=logging.INFO) """ A fallback function to set x11 layout specifically and separately from console layout. This isn't strictly necessary since .set_keyboard_language() does this as well. """ + info(f"Setting x11 keyboard language to {language}") + if len(language.strip()): if not verify_x11_keyboard_layout(language): - self.log(f"Invalid x11-keyboard language specified: {language}", fg="red", level=logging.ERROR) + error(f"Invalid x11-keyboard language specified: {language}") return False - from .systemd import Boot + from .boot import Boot with Boot(self) as session: session.SysCommand(["localectl", "set-x11-keymap", '""']) try: session.SysCommand(["localectl", "set-x11-keymap", language]) - except SysCallError as error: - raise ServiceException(f"Unable to set locale '{language}' for X11: {error}") + except SysCallError as err: + raise ServiceException(f"Unable to set locale '{language}' for X11: {err}") else: - self.log(f'X11-Keyboard language was not changed from default (no language specified).', fg="yellow", level=logging.INFO) + info(f'X11-Keyboard language was not changed from default (no language specified)') return True + + def _service_state(self, service_name: str) -> str: + if os.path.splitext(service_name)[1] != '.service': + service_name += '.service' # Just to be safe + + state = b''.join(SysCommand(f'systemctl show --no-pager -p SubState --value {service_name}', environment_vars={'SYSTEMD_COLORS': '0'})) + + return state.strip().decode('UTF-8') diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py new file mode 100644 index 00000000..b5691a10 --- /dev/null +++ b/archinstall/lib/interactions/__init__.py @@ -0,0 +1,20 @@ +from .locale_conf import select_locale_lang, select_locale_enc +from .manage_users_conf import UserList, ask_for_additional_users +from .network_conf import ManualNetworkConfig, ask_to_configure_network +from .utils import get_password + +from .disk_conf import ( + select_devices, select_disk_config, get_default_partition_layout, + select_main_filesystem_format, suggest_single_disk_layout, + suggest_multi_disk_layout +) + +from .general_conf import ( + ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_language, + select_mirror_regions, select_archinstall_language, ask_additional_packages_to_install, + add_number_of_parrallel_downloads, select_additional_repositories +) + +from .system_conf import ( + select_kernel, ask_for_bootloader, select_driver, ask_for_swap +) diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py new file mode 100644 index 00000000..78e4cff4 --- /dev/null +++ b/archinstall/lib/interactions/disk_conf.py @@ -0,0 +1,393 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, TYPE_CHECKING +from typing import Optional, List, Tuple + +from .. import disk +from ..hardware import SysInfo +from ..menu import Menu +from ..menu import TableMenu +from ..menu.menu import MenuSelectionType +from ..output import FormattedOutput, debug +from ..utils.util import prompt_dir + +if TYPE_CHECKING: + _: Any + + +def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]: + """ + Asks the user to select one or multiple devices + + :return: List of selected devices + :rtype: list + """ + + def _preview_device_selection(selection: disk._DeviceInfo) -> Optional[str]: + dev = disk.device_handler.get_device(selection.path) + if dev and dev.partition_infos: + return FormattedOutput.as_table(dev.partition_infos) + return None + + if preset is None: + preset = [] + + title = str(_('Select one or more devices to use and configure')) + warning = str(_('If you reset the device selection this will also reset the current disk layout. Are you sure?')) + + devices = disk.device_handler.devices + options = [d.device_info for d in devices] + preset_value = [p.device_info for p in preset] + + choice = TableMenu( + title, + data=options, + multi=True, + preset=preset_value, + preview_command=_preview_device_selection, + preview_title=str(_('Existing Partitions')), + preview_size=0.2, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: + selected_device_info: List[disk._DeviceInfo] = choice.value # type: ignore + selected_devices = [] + + for device in devices: + if device.device_info in selected_device_info: + selected_devices.append(device) + + return selected_devices + + +def get_default_partition_layout( + devices: List[disk.BDevice], + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_option: bool = False +) -> List[disk.DeviceModification]: + + if len(devices) == 1: + device_modification = suggest_single_disk_layout( + devices[0], + filesystem_type=filesystem_type, + advanced_options=advanced_option + ) + return [device_modification] + else: + return suggest_multi_disk_layout( + devices, + filesystem_type=filesystem_type, + advanced_options=advanced_option + ) + + +def _manual_partitioning( + preset: List[disk.DeviceModification], + devices: List[disk.BDevice] +) -> List[disk.DeviceModification]: + modifications = [] + for device in devices: + mod = next(filter(lambda x: x.device == device, preset), None) + if not mod: + mod = disk.DeviceModification(device, wipe=False) + + if partitions := disk.manual_partitioning(device, preset=mod.partitions): + mod.partitions = partitions + modifications.append(mod) + + return modifications + + +def select_disk_config( + preset: Optional[disk.DiskLayoutConfiguration] = None, + advanced_option: bool = False +) -> Optional[disk.DiskLayoutConfiguration]: + default_layout = disk.DiskLayoutType.Default.display_msg() + manual_mode = disk.DiskLayoutType.Manual.display_msg() + pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg() + + options = [default_layout, manual_mode, pre_mount_mode] + preset_value = preset.config_type.display_msg() if preset else None + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select a partitioning option'), + options, + allow_reset=True, + allow_reset_warning_msg=warning, + sort=False, + preview_size=0.2, + preset_values=preset_value + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + case MenuSelectionType.Selection: + if choice.single_value == pre_mount_mode: + output = "You will use whatever drive-setup is mounted at the specified directory\n" + output += "WARNING: Archinstall won't check the suitability of this setup\n" + + path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output) + mods = disk.device_handler.detect_pre_mounted_mods(path) + + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Pre_mount, + relative_mountpoint=path, + device_modifications=mods + ) + + preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] + + devices = select_devices(preset_devices) + + if not devices: + return None + + if choice.value == default_layout: + modifications = get_default_partition_layout(devices, advanced_option=advanced_option) + if modifications: + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Default, + device_modifications=modifications + ) + elif choice.value == manual_mode: + preset_mods = preset.device_modifications if preset else [] + modifications = _manual_partitioning(preset_mods, devices) + + if modifications: + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Manual, + device_modifications=modifications + ) + + return None + + +def _boot_partition() -> disk.PartitionModification: + if SysInfo.has_uefi(): + start = disk.Size(1, disk.Unit.MiB) + size = disk.Size(512, disk.Unit.MiB) + else: + start = disk.Size(3, disk.Unit.MiB) + size = disk.Size(203, disk.Unit.MiB) + + # boot partition + return disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=start, + length=size, + mountpoint=Path('/boot'), + fs_type=disk.FilesystemType.Fat32, + flags=[disk.PartitionFlag.Boot] + ) + + +def select_main_filesystem_format(advanced_options=False) -> disk.FilesystemType: + options = { + 'btrfs': disk.FilesystemType.Btrfs, + 'ext4': disk.FilesystemType.Ext4, + 'xfs': disk.FilesystemType.Xfs, + 'f2fs': disk.FilesystemType.F2fs + } + + if advanced_options: + options.update({'ntfs': disk.FilesystemType.Ntfs}) + + prompt = _('Select which filesystem your main partition should use') + choice = Menu(prompt, options, skip=False, sort=False).run() + return options[choice.single_value] + + +def suggest_single_disk_layout( + device: disk.BDevice, + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_options: bool = False, + separate_home: Optional[bool] = None +) -> disk.DeviceModification: + if not filesystem_type: + filesystem_type = select_main_filesystem_format(advanced_options) + + min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB) + root_partition_size = disk.Size(20, disk.Unit.GiB) + using_subvolumes = False + using_home_partition = False + compression = False + device_size_gib = device.device_info.total_size + + if filesystem_type == disk.FilesystemType.Btrfs: + prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + using_subvolumes = choice.value == Menu.yes() + + prompt = str(_('Would you like to use BTRFS compression?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + compression = choice.value == Menu.yes() + + device_modification = disk.DeviceModification(device, wipe=True) + + # Used for reference: https://wiki.archlinux.org/title/partitioning + # 2 MiB is unallocated for GRUB on BIOS. Potentially unneeded for other bootloaders? + + # TODO: On BIOS, /boot partition is only needed if the drive will + # be encrypted, otherwise it is not recommended. We should probably + # add a check for whether the drive will be encrypted or not. + + # Increase the UEFI partition if UEFI is detected. + # Also re-align the start to 1MiB since we don't need the first sectors + # like we do in MBR layouts where the boot loader is installed traditionally. + + boot_partition = _boot_partition() + device_modification.add_partition(boot_partition) + + if not using_subvolumes: + if device_size_gib >= min_size_to_allow_home_part: + if separate_home is None: + prompt = str(_('Would you like to create a separate partition for /home?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + using_home_partition = choice.value == Menu.yes() + elif separate_home is True: + using_home_partition = True + else: + using_home_partition = False + + # root partition + start = disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB) + + # Set a size for / (/root) + if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: + length = disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size) + else: + length = min(device.device_info.total_size, root_partition_size) + + root_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=start, + length=length, + mountpoint=Path('/') if not using_subvolumes else None, + fs_type=filesystem_type, + mount_options=['compress=zstd'] if compression else [], + ) + device_modification.add_partition(root_partition) + + if using_subvolumes: + # https://btrfs.wiki.kernel.org/index.php/FAQ + # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash + # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh + subvolumes = [ + disk.SubvolumeModification(Path('@'), Path('/')), + disk.SubvolumeModification(Path('@home'), Path('/home')), + disk.SubvolumeModification(Path('@log'), Path('/var/log')), + disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), + disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')) + ] + root_partition.btrfs_subvols = subvolumes + elif using_home_partition: + # If we don't want to use subvolumes, + # But we want to be able to re-use data between re-installs.. + # A second partition for /home would be nice if we have the space for it + home_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=root_partition.length, + length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + mountpoint=Path('/home'), + fs_type=filesystem_type, + mount_options=['compress=zstd'] if compression else [] + ) + device_modification.add_partition(home_partition) + + return device_modification + + +def suggest_multi_disk_layout( + devices: List[disk.BDevice], + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_options: bool = False +) -> List[disk.DeviceModification]: + if not devices: + return [] + + # Not really a rock solid foundation of information to stand on, but it's a start: + # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ + # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ + min_home_partition_size = disk.Size(40, disk.Unit.GiB) + # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? + desired_root_partition_size = disk.Size(20, disk.Unit.GiB) + compression = False + + if not filesystem_type: + filesystem_type = select_main_filesystem_format(advanced_options) + + # find proper disk for /home + possible_devices = list(filter(lambda x: x.device_info.total_size >= min_home_partition_size, devices)) + home_device = max(possible_devices, key=lambda d: d.device_info.total_size) if possible_devices else None + + # find proper device for /root + devices_delta = {} + for device in devices: + if device is not home_device: + delta = device.device_info.total_size - desired_root_partition_size + devices_delta[device] = delta + + sorted_delta: List[Tuple[disk.BDevice, Any]] = sorted(devices_delta.items(), key=lambda x: x[1]) + root_device: Optional[disk.BDevice] = sorted_delta[0][0] + + if home_device is None or root_device is None: + text = _('The selected drives do not have the minimum capacity required for an automatic suggestion\n') + text += _('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB)) + text += _('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB)) + Menu(str(text), [str(_('Continue'))], skip=False).run() + return [] + + if filesystem_type == disk.FilesystemType.Btrfs: + prompt = str(_('Would you like to use BTRFS compression?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + compression = choice.value == Menu.yes() + + device_paths = ', '.join([str(d.device_info.path) for d in devices]) + + debug(f"Suggesting multi-disk-layout for devices: {device_paths}") + debug(f"/root: {root_device.device_info.path}") + debug(f"/home: {home_device.device_info.path}") + + root_device_modification = disk.DeviceModification(root_device, wipe=True) + home_device_modification = disk.DeviceModification(home_device, wipe=True) + + # add boot partition to the root device + boot_partition = _boot_partition() + root_device_modification.add_partition(boot_partition) + + # add root partition to the root device + root_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB), + length=disk.Size(100, disk.Unit.Percent, total_size=root_device.device_info.total_size), + mountpoint=Path('/'), + mount_options=['compress=zstd'] if compression else [], + fs_type=filesystem_type + ) + root_device_modification.add_partition(root_partition) + + # add home partition to home device + home_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=disk.Size(1, disk.Unit.MiB), + length=disk.Size(100, disk.Unit.Percent, total_size=home_device.device_info.total_size), + mountpoint=Path('/home'), + mount_options=['compress=zstd'] if compression else [], + fs_type=filesystem_type, + ) + home_device_modification.add_partition(home_partition) + + return [root_device_modification, home_device_modification] diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py new file mode 100644 index 00000000..5fcfa633 --- /dev/null +++ b/archinstall/lib/interactions/general_conf.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +import pathlib +from typing import List, Any, Optional, Dict, TYPE_CHECKING + +from ..locale import list_keyboard_languages, list_timezones +from ..menu import MenuSelectionType, Menu, TextInput +from ..mirrors import list_mirrors +from ..output import warn +from ..packages.packages import validate_package_list +from ..storage import storage +from ..translationhandler import Language + +if TYPE_CHECKING: + _: Any + + +def ask_ntp(preset: bool = True) -> bool: + prompt = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n')) + prompt += str(_('Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki')) + if preset: + preset_val = Menu.yes() + else: + preset_val = Menu.no() + choice = Menu(prompt, Menu.yes_no(), skip=False, preset_values=preset_val, default_option=Menu.yes()).run() + + return False if choice.value == Menu.no() else True + + +def ask_hostname(preset: str = '') -> str: + while True: + hostname = TextInput( + str(_('Desired hostname for the installation: ')), + preset + ).run().strip() + + if hostname: + return hostname + + +def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: + timezones = list_timezones() + default = 'UTC' + + choice = Menu( + _('Select a timezone'), + list(timezones), + preset_values=preset, + default_option=default + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None + + +def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]: + no_audio = str(_('No audio server')) + choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] + default = 'pipewire' if desktop else no_audio + + choice = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=default).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None + + +def select_language(preset: Optional[str] = None) -> Optional[str]: + """ + Asks the user to select a language + Usually this is combined with :ref:`archinstall.list_keyboard_languages`. + + :return: The language/dictionary key of the selected language + :rtype: str + """ + kb_lang = list_keyboard_languages() + # sort alphabetically and then by length + sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) + + choice = Menu( + _('Select keyboard layout'), + sorted_kb_lang, + preset_values=preset, + sort=False + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None + + +def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: + """ + Asks the user to select a mirror or region + Usually this is combined with :ref:`archinstall.list_mirrors`. + + :return: The dictionary information about a mirror/region. + :rtype: dict + """ + if preset_values is None: + preselected = None + else: + preselected = list(preset_values.keys()) + + mirrors = list_mirrors() + + choice = Menu( + _('Select one of the regions to download packages from'), + list(mirrors.keys()), + preset_values=preselected, + multi=True, + allow_reset=True + ).run() + + match choice.type_: + case MenuSelectionType.Reset: + return {} + case MenuSelectionType.Skip: + return preset_values + case MenuSelectionType.Selection: + return {selected: mirrors[selected] for selected in choice.multi_value} + + return {} + + +def select_archinstall_language(languages: List[Language], preset: Language) -> Language: + # these are the displayed language names which can either be + # the english name of a language or, if present, the + # name of the language in its own language + options = {lang.display_name: lang for lang in languages} + + title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n' + title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n' + title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n' + + choice = Menu( + title, + list(options.keys()), + default_option=preset.display_name, + preview_size=0.5 + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return options[choice.single_value] + + raise ValueError('Language selection not handled') + + +def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: + # Additional packages (with some light weight error handling for invalid package names) + print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) + print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) + + def read_packages(already_defined: list = []) -> list: + display = ' '.join(already_defined) + input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip() + return input_packages.split() if input_packages else [] + + pre_set_packages = pre_set_packages if pre_set_packages else [] + packages = read_packages(pre_set_packages) + + if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']: + while True: + if len(packages): + # Verify packages that were given + print(_("Verifying that additional packages exist (this might take a few seconds)")) + valid, invalid = validate_package_list(packages) + + if invalid: + warn(f"Some packages could not be found in the repository: {invalid}") + packages = read_packages(valid) + continue + break + + return packages + + +def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Optional[int]: + max_downloads = 5 + print(_(f"This option enables the number of parallel downloads that can occur during installation")) + print(_(f"Enter the number of parallel downloads to be enabled.\n (Enter a value between 1 to {max_downloads})\nNote:")) + print(_(f" - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )")) + print(_(f" - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )")) + print(_(f" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )")) + + while True: + try: + input_number = int(TextInput(_("[Default value: 0] > ")).run().strip() or 0) + if input_number <= 0: + input_number = 0 + elif input_number > max_downloads: + input_number = max_downloads + break + except: + print(_(f"Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]")) + + pacman_conf_path = pathlib.Path("/etc/pacman.conf") + with pacman_conf_path.open() as f: + pacman_conf = f.read().split("\n") + + with pacman_conf_path.open("w") as fwrite: + for line in pacman_conf: + if "ParallelDownloads" in line: + fwrite.write(f"ParallelDownloads = {input_number+1}\n") if not input_number == 0 else fwrite.write("#ParallelDownloads = 0\n") + else: + fwrite.write(f"{line}\n") + + return input_number + + +def select_additional_repositories(preset: List[str]) -> List[str]: + """ + Allows the user to select additional repositories (multilib, and testing) if desired. + + :return: The string as a selected repository + :rtype: string + """ + + repositories = ["multilib", "testing"] + + choice = Menu( + _('Choose which optional additional repositories to enable'), + repositories, + sort=False, + multi=True, + preset_values=preset, + allow_reset=True + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Selection: return choice.single_value + + return [] diff --git a/archinstall/lib/interactions/locale_conf.py b/archinstall/lib/interactions/locale_conf.py new file mode 100644 index 00000000..de115202 --- /dev/null +++ b/archinstall/lib/interactions/locale_conf.py @@ -0,0 +1,43 @@ +from typing import Any, TYPE_CHECKING, Optional + +from ..locale import list_locales +from ..menu import Menu, MenuSelectionType + +if TYPE_CHECKING: + _: Any + + +def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: + locales = list_locales() + locale_lang = set([locale.split()[0] for locale in locales]) + + choice = Menu( + _('Choose which locale language to use'), + list(locale_lang), + sort=True, + preset_values=preset + ).run() + + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return preset + + return None + + +def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: + locales = list_locales() + locale_enc = set([locale.split()[1] for locale in locales]) + + choice = Menu( + _('Choose which locale encoding to use'), + list(locale_enc), + sort=True, + preset_values=preset + ).run() + + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return preset + + return None diff --git a/archinstall/lib/interactions/manage_users_conf.py b/archinstall/lib/interactions/manage_users_conf.py new file mode 100644 index 00000000..879578da --- /dev/null +++ b/archinstall/lib/interactions/manage_users_conf.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import re +from typing import Any, Dict, TYPE_CHECKING, List, Optional + +from .utils import get_password +from ..menu import Menu, ListManager +from ..models.users import User +from ..output import FormattedOutput + +if TYPE_CHECKING: + _: Any + + +class UserList(ListManager): + """ + subclass of ListManager for the managing of user accounts + """ + + def __init__(self, prompt: str, lusers: List[User]): + self._actions = [ + str(_('Add a user')), + str(_('Change password')), + str(_('Promote/Demote user')), + str(_('Delete User')) + ] + super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[User]) -> Dict[str, Any]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[User]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, user in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = user + + return display_data + + def selected_action_display(self, user: User) -> str: + return user.username + + def handle_action(self, action: str, entry: Optional[User], data: List[User]) -> List[User]: + if action == self._actions[0]: # add + new_user = self._add_user() + if new_user is not None: + # in case a user with the same username as an existing user + # was created we'll replace the existing one + data = [d for d in data if d.username != new_user.username] + data += [new_user] + elif action == self._actions[1] and entry: # change password + prompt = str(_('Password for user "{}": ').format(entry.username)) + new_password = get_password(prompt=prompt) + if new_password: + user = next(filter(lambda x: x == entry, data)) + user.password = new_password + elif action == self._actions[2] and entry: # promote/demote + user = next(filter(lambda x: x == entry, data)) + user.sudo = False if user.sudo else True + elif action == self._actions[3] and entry: # delete + data = [d for d in data if d != entry] + + return data + + def _check_for_correct_username(self, username: str) -> bool: + if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: + return True + return False + + def _add_user(self) -> Optional[User]: + prompt = '\n\n' + str(_('Enter username (leave blank to skip): ')) + + while True: + username = input(prompt).strip(' ') + if not username: + return None + if not self._check_for_correct_username(username): + error_prompt = str(_("The username you entered is invalid. Try again")) + print(error_prompt) + else: + break + + password = get_password(prompt=str(_('Password for user "{}": ').format(username))) + + if not password: + return None + + choice = Menu( + str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(), + skip=False, + default_option=Menu.yes(), + clear_screen=False, + show_search_hint=False + ).run() + + sudo = True if choice.value == Menu.yes() else False + return User(username, password, sudo) + + +def ask_for_additional_users(prompt: str = '', defined_users: List[User] = []) -> List[User]: + users = UserList(prompt, defined_users).run() + return users diff --git a/archinstall/lib/interactions/network_conf.py b/archinstall/lib/interactions/network_conf.py new file mode 100644 index 00000000..18a834a1 --- /dev/null +++ b/archinstall/lib/interactions/network_conf.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import ipaddress +from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict + +from ..menu import MenuSelectionType, TextInput +from ..models.network_configuration import NetworkConfiguration, NicType + +from ..networking import list_interfaces +from ..output import FormattedOutput, warn +from ..menu import ListManager, Menu + +if TYPE_CHECKING: + _: Any + + +class ManualNetworkConfig(ListManager): + """ + subclass of ListManager for the managing of network configurations + """ + + def __init__(self, prompt: str, ifaces: List[NetworkConfiguration]): + self._actions = [ + str(_('Add interface')), + str(_('Edit interface')), + str(_('Delete interface')) + ] + + super().__init__(prompt, ifaces, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[NetworkConfiguration]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, iface in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = iface + + return display_data + + def selected_action_display(self, iface: NetworkConfiguration) -> str: + return iface.iface if iface.iface else '' + + def handle_action(self, action: str, entry: Optional[NetworkConfiguration], data: List[NetworkConfiguration]): + if action == self._actions[0]: # add + iface_name = self._select_iface(data) + if iface_name: + iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) + iface = self._edit_iface(iface) + data += [iface] + elif entry: + if action == self._actions[1]: # edit interface + data = [d for d in data if d.iface != entry.iface] + data.append(self._edit_iface(entry)) + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] + + return data + + def _select_iface(self, data: List[NetworkConfiguration]) -> Optional[Any]: + all_ifaces = list_interfaces().values() + existing_ifaces = [d.iface for d in data] + available = set(all_ifaces) - set(existing_ifaces) + choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() + + if choice.type_ == MenuSelectionType.Skip: + return None + + return choice.value + + def _edit_iface(self, edit_iface: NetworkConfiguration): + iface_name = edit_iface.iface + modes = ['DHCP (auto detect)', 'IP (static)'] + default_mode = 'DHCP (auto detect)' + + prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) + mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() + + if mode.value == 'IP (static)': + while 1: + prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) + ip = TextInput(prompt, edit_iface.ip).run().strip() + # Implemented new check for correct IP/subnet input + try: + ipaddress.ip_interface(ip) + break + except ValueError: + warn("You need to enter a valid IP in IP-config mode") + + # Implemented new check for correct gateway IP address + gateway = None + + while 1: + gateway = TextInput( + _('Enter your gateway (router) IP address or leave blank for none: '), + edit_iface.gateway + ).run().strip() + try: + if len(gateway) > 0: + ipaddress.ip_address(gateway) + break + except ValueError: + warn("You need to enter a valid gateway (router) IP address") + + if edit_iface.dns: + display_dns = ' '.join(edit_iface.dns) + else: + display_dns = None + dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() + + dns = [] + if len(dns_input): + dns = dns_input.split(' ') + + return NetworkConfiguration(NicType.MANUAL, iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) + else: + # this will contain network iface names + return NetworkConfiguration(NicType.MANUAL, iface=iface_name) + + +def ask_to_configure_network( + preset: Union[NetworkConfiguration, List[NetworkConfiguration]] +) -> Optional[NetworkConfiguration | List[NetworkConfiguration]]: + """ + Configure the network on the newly installed system + """ + network_options = { + 'none': str(_('No network configuration')), + 'iso_config': str(_('Copy ISO network configuration to installation')), + 'network_manager': str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')), + 'manual': str(_('Manual configuration')) + } + # for this routine it's easier to set the cursor position rather than a preset value + cursor_idx = None + + if preset and not isinstance(preset, list): + if preset.type == 'iso_config': + cursor_idx = 0 + elif preset.type == 'network_manager': + cursor_idx = 1 + + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select one network interface to configure'), + list(network_options.values()), + cursor_index=cursor_idx, + sort=False, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + + if choice.value == network_options['none']: + return None + elif choice.value == network_options['iso_config']: + return NetworkConfiguration(NicType.ISO) + elif choice.value == network_options['network_manager']: + return NetworkConfiguration(NicType.NM) + elif choice.value == network_options['manual']: + preset_ifaces = preset if isinstance(preset, list) else [] + return ManualNetworkConfig('Configure interfaces', preset_ifaces).run() + + return preset diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py new file mode 100644 index 00000000..bbcb5b23 --- /dev/null +++ b/archinstall/lib/interactions/system_conf.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from typing import List, Any, Dict, TYPE_CHECKING, Optional + +from ..hardware import AVAILABLE_GFX_DRIVERS, SysInfo +from ..menu import MenuSelectionType, Menu +from ..models.bootloader import Bootloader + +if TYPE_CHECKING: + _: Any + + +def select_kernel(preset: List[str] = []) -> List[str]: + """ + Asks the user to select a kernel for system. + + :return: The string as a selected kernel + :rtype: string + """ + + kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] + default_kernel = "linux" + + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Choose which kernels to use or leave blank for default "{}"').format(default_kernel), + kernels, + sort=True, + multi=True, + preset_values=preset, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Selection: return choice.value # type: ignore + + +def ask_for_bootloader(preset: Bootloader) -> Bootloader: + # when the system only supports grub + if not SysInfo.has_uefi(): + options = [Bootloader.Grub.value] + default = Bootloader.Grub.value + else: + options = Bootloader.values() + default = Bootloader.Systemd.value + + preset_value = preset.value if preset else None + + choice = Menu( + _('Choose a bootloader'), + options, + preset_values=preset_value, + sort=False, + default_option=default + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return Bootloader(choice.value) + + return preset + + +def select_driver(options: Dict[str, Any] = {}, current_value: Optional[str] = None) -> Optional[str]: + """ + Some what convoluted function, whose job is simple. + Select a graphics driver from a pre-defined set of popular options. + + (The template xorg is for beginner users, not advanced, and should + there for appeal to the general public first and edge cases later) + """ + + if not options: + options = AVAILABLE_GFX_DRIVERS + + drivers = sorted(list(options.keys())) + + if drivers: + title = '' + if SysInfo.has_amd_graphics(): + title += str(_('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.')) + '\n' + if SysInfo.has_intel_graphics(): + title += str(_('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n')) + if SysInfo.has_nvidia_graphics(): + title += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')) + + title += str(_('\nSelect a graphics driver or leave blank to install all open-source drivers')) + + preset = current_value if current_value else None + choice = Menu(title, drivers, preset_values=preset).run() + + if choice.type_ != MenuSelectionType.Selection: + return None + + return choice.value # type: ignore + + return current_value + + +def ask_for_swap(preset: bool = True) -> bool: + if preset: + preset_val = Menu.yes() + else: + preset_val = Menu.no() + + prompt = _('Would you like to use swap on zram?') + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True + + return preset diff --git a/archinstall/lib/interactions/utils.py b/archinstall/lib/interactions/utils.py new file mode 100644 index 00000000..f6b5b2d3 --- /dev/null +++ b/archinstall/lib/interactions/utils.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import getpass +from typing import Any, Optional, TYPE_CHECKING + +from ..models import PasswordStrength +from ..output import log, error + +if TYPE_CHECKING: + _: Any + +# used for signal handler +SIG_TRIGGER = None + + +def get_password(prompt: str = '') -> Optional[str]: + if not prompt: + prompt = _("Enter a password: ") + + while password := getpass.getpass(prompt): + if len(password.strip()) <= 0: + break + + strength = PasswordStrength.strength(password) + log(f'Password strength: {strength.value}', fg=strength.color()) + + passwd_verification = getpass.getpass(prompt=_('And one more time for verification: ')) + if password != passwd_verification: + error(' * Passwords did not match * ') + continue + + return password + + return None diff --git a/archinstall/lib/locale.py b/archinstall/lib/locale.py new file mode 100644 index 00000000..0a36c072 --- /dev/null +++ b/archinstall/lib/locale.py @@ -0,0 +1,68 @@ +from typing import Iterator, List + +from .exceptions import ServiceException, SysCallError +from .general import SysCommand +from .output import error + + +def list_keyboard_languages() -> Iterator[str]: + for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() + + +def list_locales() -> List[str]: + with open('/etc/locale.gen', 'r') as fp: + locales = [] + # before the list of locales begins there's an empty line with a '#' in front + # so we'll collect the localels from bottom up and halt when we're donw + entries = fp.readlines() + entries.reverse() + + for entry in entries: + text = entry.replace('#', '').strip() + if text == '': + break + locales.append(text) + + locales.reverse() + return locales + + +def list_x11_keyboard_languages() -> Iterator[str]: + for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() + + +def verify_keyboard_layout(layout :str) -> bool: + for language in list_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + + +def verify_x11_keyboard_layout(layout :str) -> bool: + for language in list_x11_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + + +def set_keyboard_language(locale :str) -> bool: + if len(locale.strip()): + if not verify_keyboard_layout(locale): + error(f"Invalid keyboard locale specified: {locale}") + return False + + try: + SysCommand(f'localectl set-keymap {locale}') + except SysCallError as err: + raise ServiceException(f"Unable to set locale '{locale}' for console: {err}") + + return True + + return False + + +def list_timezones() -> Iterator[str]: + for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() diff --git a/archinstall/lib/locale_helpers.py b/archinstall/lib/locale_helpers.py deleted file mode 100644 index efb0365f..00000000 --- a/archinstall/lib/locale_helpers.py +++ /dev/null @@ -1,176 +0,0 @@ -import logging -from typing import Iterator, List, Callable, Optional - -from .exceptions import ServiceException, SysCallError -from .general import SysCommand -from .output import log -from .storage import storage - - -def list_keyboard_languages() -> Iterator[str]: - for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() - - -def list_locales() -> List[str]: - with open('/etc/locale.gen', 'r') as fp: - locales = [] - # before the list of locales begins there's an empty line with a '#' in front - # so we'll collect the localels from bottom up and halt when we're donw - entries = fp.readlines() - entries.reverse() - - for entry in entries: - text = entry.replace('#', '').strip() - if text == '': - break - locales.append(text) - - locales.reverse() - return locales - -def get_locale_mode_text(mode): - if mode == 'LC_ALL': - mode_text = "general (LC_ALL)" - elif mode == "LC_CTYPE": - mode_text = "Character set" - elif mode == "LC_NUMERIC": - mode_text = "Numeric values" - elif mode == "LC_TIME": - mode_text = "Time Values" - elif mode == "LC_COLLATE": - mode_text = "sort order" - elif mode == "LC_MESSAGES": - mode_text = "text messages" - else: - mode_text = "Unassigned" - return mode_text - - -def reset_cmd_locale(): - """ sets the cmd_locale to its saved default """ - storage['CMD_LOCALE'] = storage.get('CMD_LOCALE_DEFAULT',{}) - - -def unset_cmd_locale(): - """ archinstall will use the execution environment default """ - storage['CMD_LOCALE'] = {} - - -def set_cmd_locale( - general: Optional[str] = None, - charset :str = 'C', - numbers :str = 'C', - time :str = 'C', - collate :str = 'C', - messages :str = 'C' -): - """ - Set the cmd locale. - If the parameter general is specified, it takes precedence over the rest (might as well not exist) - The rest define some specific settings above the installed default language. If anyone of this parameters is none means the installation default - """ - installed_locales = list_installed_locales() - result = {} - if general: - if general in installed_locales: - storage['CMD_LOCALE'] = {'LC_ALL':general} - else: - log(f"{get_locale_mode_text('LC_ALL')} {general} is not installed. Defaulting to C",fg="yellow",level=logging.WARNING) - return - - if numbers: - if numbers in installed_locales: - result["LC_NUMERIC"] = numbers - else: - log(f"{get_locale_mode_text('LC_NUMERIC')} {numbers} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - if charset: - if charset in installed_locales: - result["LC_CTYPE"] = charset - else: - log(f"{get_locale_mode_text('LC_CTYPE')} {charset} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - if time: - if time in installed_locales: - result["LC_TIME"] = time - else: - log(f"{get_locale_mode_text('LC_TIME')} {time} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - if collate: - if collate in installed_locales: - result["LC_COLLATE"] = collate - else: - log(f"{get_locale_mode_text('LC_COLLATE')} {collate} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - if messages: - if messages in installed_locales: - result["LC_MESSAGES"] = messages - else: - log(f"{get_locale_mode_text('LC_MESSAGES')} {messages} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - storage['CMD_LOCALE'] = result - -def host_locale_environ(func :Callable): - """ decorator when we want a function executing in the host's locale environment """ - def wrapper(*args, **kwargs): - unset_cmd_locale() - result = func(*args,**kwargs) - reset_cmd_locale() - return result - return wrapper - -def c_locale_environ(func :Callable): - """ decorator when we want a function executing in the C locale environment """ - def wrapper(*args, **kwargs): - set_cmd_locale(general='C') - result = func(*args,**kwargs) - reset_cmd_locale() - return result - return wrapper - -def list_installed_locales() -> List[str]: - lista = [] - for line in SysCommand('locale -a'): - lista.append(line.decode('UTF-8').strip()) - return lista - -def list_x11_keyboard_languages() -> Iterator[str]: - for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() - - -def verify_keyboard_layout(layout :str) -> bool: - for language in list_keyboard_languages(): - if layout.lower() == language.lower(): - return True - return False - - -def verify_x11_keyboard_layout(layout :str) -> bool: - for language in list_x11_keyboard_languages(): - if layout.lower() == language.lower(): - return True - return False - - -def search_keyboard_layout(layout :str) -> Iterator[str]: - for language in list_keyboard_languages(): - if layout.lower() in language.lower(): - yield language - - -def set_keyboard_language(locale :str) -> bool: - if len(locale.strip()): - if not verify_keyboard_layout(locale): - log(f"Invalid keyboard locale specified: {locale}", fg="red", level=logging.ERROR) - return False - - try: - SysCommand(f'localectl set-keymap {locale}') - except SysCallError as error: - raise ServiceException(f"Unable to set locale '{locale}' for console: {error}") - - return True - - return False - - -def list_timezones() -> Iterator[str]: - for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index 53a5e8d2..f9b09b53 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging import shlex import time from dataclasses import dataclass @@ -9,7 +8,7 @@ from typing import Optional, List from . import disk from .general import SysCommand, generate_password, SysCommandWorker -from .output import log +from .output import info, debug from .exceptions import SysCallError, DiskError from .storage import storage @@ -61,7 +60,7 @@ class Luks2: iter_time: int = 10000, key_file: Optional[Path] = None ) -> Path: - log(f'Luks2 encrypting: {self.luks_dev_path}', level=logging.INFO) + info(f'Luks2 encrypting: {self.luks_dev_path}') byte_password = self._password_bytes() @@ -95,21 +94,21 @@ class Luks2: try: SysCommand(cryptsetup_args) break - except SysCallError as error: + except SysCallError as err: time.sleep(storage['DISK_TIMEOUTS']) if retry_attempt != storage['DISK_RETRY_ATTEMPTS'] - 1: continue - if error.exit_code == 1: - log(f'luks2 partition currently in use: {self.luks_dev_path}') - log('Attempting to unmount, crypt-close and trying encryption again') + if err.exit_code == 1: + info(f'luks2 partition currently in use: {self.luks_dev_path}') + info('Attempting to unmount, crypt-close and trying encryption again') self.lock() # Then try again to set up the crypt-device SysCommand(cryptsetup_args) else: - raise DiskError(f'Could not encrypt volume "{self.luks_dev_path}": {error}') + raise DiskError(f'Could not encrypt volume "{self.luks_dev_path}": {err}') return key_file @@ -119,7 +118,7 @@ class Luks2: try: return SysCommand(command).decode().strip() # type: ignore except SysCallError as err: - log(f'Unable to get UUID for Luks device: {self.luks_dev_path}', level=logging.INFO) + info(f'Unable to get UUID for Luks device: {self.luks_dev_path}') raise err def is_unlocked(self) -> bool: @@ -133,7 +132,7 @@ class Luks2: :param key_file: An alternative key file :type key_file: Path """ - log(f'Unlocking luks2 device: {self.luks_dev_path}', level=logging.DEBUG) + debug(f'Unlocking luks2 device: {self.luks_dev_path}') if not self.mapper_name: raise ValueError('mapper name missing') @@ -170,11 +169,11 @@ class Luks2: for child in lsblk_info.children: # Unmount the child location for mountpoint in child.mountpoints: - log(f'Unmounting {mountpoint}', level=logging.DEBUG) + debug(f'Unmounting {mountpoint}') disk.device_handler.umount(mountpoint, recursive=True) # And close it if possible. - log(f"Closing crypt device {child.name}", level=logging.DEBUG) + debug(f"Closing crypt device {child.name}") SysCommand(f"cryptsetup close {child.name}") self._mapper_dev = None @@ -194,10 +193,10 @@ class Luks2: if key_file.exists(): if not override: - log(f'Key file {key_file} already exists, keeping existing') + info(f'Key file {key_file} already exists, keeping existing') return else: - log(f'Key file {key_file} already exists, overriding') + info(f'Key file {key_file} already exists, overriding') key_file_path.mkdir(parents=True, exist_ok=True) @@ -210,7 +209,7 @@ class Luks2: self._crypttab(crypttab_path, key_file, options=["luks", "key-slot=1"]) def _add_key(self, key_file: Path): - log(f'Adding additional key-file {key_file}', level=logging.INFO) + info(f'Adding additional key-file {key_file}') command = f'/usr/bin/cryptsetup -q -v luksAddKey {self.luks_dev_path} {key_file}' worker = SysCommandWorker(command, environment_vars={'LC_ALL': 'C'}) @@ -230,7 +229,7 @@ class Luks2: key_file: Path, options: List[str] ) -> None: - log(f'Adding crypttab entry for key {key_file}', level=logging.INFO) + info(f'Adding crypttab entry for key {key_file}') with open(crypttab_path, 'a') as crypttab: opt = ','.join(options) diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index e44d65a4..2bd56374 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -1,11 +1,10 @@ from __future__ import annotations -import logging from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING from .menu import Menu, MenuSelectionType -from ..locale_helpers import set_keyboard_language -from ..output import log +from ..locale import set_keyboard_language +from ..output import error from ..translationhandler import TranslationHandler, Language if TYPE_CHECKING: @@ -211,7 +210,7 @@ class AbstractMenu: # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager # TODO: skip processing when it comes from a planified exit if len(args) >= 2 and args[1]: - log(args[1], level=logging.ERROR, fg='red') + error(args[1]) print(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues") raise args[1] @@ -483,7 +482,7 @@ class AbstractMenu: yield item def _select_archinstall_language(self, preset: Language) -> Language: - from ..user_interaction.general_conf import select_archinstall_language + from ..interactions.general_conf import select_archinstall_language language = select_archinstall_language(self.translation_handler.translated_languages, preset) self._translation_handler.activate(language) return language diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index f3fdb85f..768dfe55 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -6,11 +6,8 @@ from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional, Callable from simple_term_menu import TerminalMenu # type: ignore from ..exceptions import RequirementError -from ..output import log +from ..output import debug -from collections.abc import Iterable -import sys -import logging if TYPE_CHECKING: _: Any @@ -127,33 +124,15 @@ class Menu(TerminalMenu): :param extra_bottom_space: Add an extra empty line at the end of the menu :type extra_bottom_space: bool """ - # we guarantee the inmutability of the options outside the class. - # an unknown number of iterables (.keys(),.values(),generator,...) can't be directly copied, in this case - # we recourse to make them lists before, but thru an exceptions - # this is the old code, which is not maintenable with more types - # options = copy(list(p_options) if isinstance(p_options,(type({}.keys()),type({}.values()))) else p_options) - # We check that the options are iterable. If not we abort. Else we copy them to lists - # it options is a dictionary we use the values as entries of the list - # if options is a string object, each character becomes an entry - # if options is a list, we implictily build a copy to maintain immutability - if not isinstance(p_options,Iterable): - log(f"Objects of type {type(p_options)} is not iterable, and are not supported at Menu",fg="red") - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) - raise RequirementError("Menu() requires an iterable as option.") - - if isinstance(p_options,dict): + if isinstance(p_options, Dict): options = list(p_options.keys()) else: options = list(p_options) if not options: - log(" * Menu didn't find any options to choose from * ", fg='red') - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) raise RequirementError('Menu.__init__() requires at least one option to proceed.') if any([o for o in options if not isinstance(o, str)]): - log(" * Menu options must be of type string * ", fg='red') - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) raise RequirementError('Menu.__init__() requires the options to be of type string') if sort: @@ -343,7 +322,7 @@ class Menu(TerminalMenu): idx = self._menu_options.index(self._default_menu_value) indexes.append(idx) except (IndexError, ValueError): - log(f'Error finding index of {p}: {self._menu_options}', level=logging.DEBUG) + debug(f'Error finding index of {p}: {self._menu_options}') if len(indexes) == 0: indexes.append(0) diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index c6c5c8e4..62a0b081 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -1,4 +1,3 @@ -import logging import pathlib import urllib.error import urllib.request @@ -6,7 +5,7 @@ from typing import Union, Iterable, Dict, Any, List from dataclasses import dataclass from .general import SysCommand -from .output import log +from .output import info, warn from .exceptions import SysCallError from .storage import storage @@ -136,7 +135,7 @@ def use_mirrors( regions: Dict[str, Iterable[str]], destination: str = '/etc/pacman.d/mirrorlist' ): - log(f'A new package mirror-list has been created: {destination}', level=logging.INFO) + info(f'A new package mirror-list has been created: {destination}') with open(destination, 'w') as mirrorlist: for region, mirrors in regions.items(): for mirror in mirrors: @@ -170,7 +169,7 @@ def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]: try: response = urllib.request.urlopen(url) except urllib.error.URLError as err: - log(f'Could not fetch an active mirror-list: {err}', level=logging.WARNING, fg="orange") + warn(f'Could not fetch an active mirror-list: {err}') return regions mirrorlist = response.read() diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py index 38254c99..e21cda33 100644 --- a/archinstall/lib/models/bootloader.py +++ b/archinstall/lib/models/bootloader.py @@ -1,12 +1,11 @@ from __future__ import annotations -import logging import sys from enum import Enum from typing import List -from ..hardware import has_uefi -from ..output import log +from ..hardware import SysInfo +from ..output import warn class Bootloader(Enum): @@ -23,7 +22,7 @@ class Bootloader(Enum): @classmethod def get_default(cls) -> Bootloader: - if has_uefi(): + if SysInfo.has_uefi(): return Bootloader.Systemd else: return Bootloader.Grub @@ -35,6 +34,6 @@ class Bootloader(Enum): if bootloader not in cls.values(): values = ', '.join(cls.values()) - log(f'Invalid bootloader value "{bootloader}". Allowed values: {values}', level=logging.WARN) + warn(f'Invalid bootloader value "{bootloader}". Allowed values: {values}') sys.exit(1) return Bootloader(bootloader) diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index a8795fc1..93dd1c44 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -1,11 +1,10 @@ from __future__ import annotations -import logging from dataclasses import dataclass, field from enum import Enum from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING, Tuple -from ..output import log +from ..output import debug from ..profile import ProfileConfiguration if TYPE_CHECKING: @@ -138,8 +137,7 @@ class NetworkConfigurationHandler: iface = manual_config.get('iface', None) if iface is None: - log(_('No iface specified for manual configuration')) - exit(1) + raise ValueError('No iface specified for manual configuration') if manual_config.get('dhcp', False) or not any([manual_config.get(v, '') for v in ['ip', 'gateway', 'dns']]): configurations.append( @@ -148,8 +146,7 @@ class NetworkConfigurationHandler: else: ip = manual_config.get('ip', '') if not ip: - log(_('Manual nic configuration with no auto DHCP requires an IP address'), fg='red') - exit(1) + raise ValueError('Manual nic configuration with no auto DHCP requires an IP address') dns = manual_config.get('dns', []) if not isinstance(dns, list): @@ -173,8 +170,7 @@ class NetworkConfigurationHandler: return NicType(nic_type) except ValueError: options = [e.value for e in NicType] - log(_('Unknown nic type: {}. Possible values are {}').format(nic_type, options), fg='red') - exit(1) + raise ValueError(f'Unknown nic type: {nic_type}. Possible values are {options}') def parse_arguments(self, config: Any): if isinstance(config, list): # new data format @@ -187,4 +183,4 @@ class NetworkConfigurationHandler: else: # manual configuration settings self._configuration = self._parse_manual_config([config]) else: - log(f'Unable to parse network configuration: {config}', level=logging.DEBUG) + debug(f'Unable to parse network configuration: {config}') diff --git a/archinstall/lib/networking.py b/archinstall/lib/networking.py index b858daaf..6906c320 100644 --- a/archinstall/lib/networking.py +++ b/archinstall/lib/networking.py @@ -1,4 +1,3 @@ -import logging import os import socket import ssl @@ -8,18 +7,16 @@ from urllib.error import URLError from urllib.parse import urlencode from urllib.request import urlopen -from .exceptions import HardwareIncompatibilityError, SysCallError -from .general import SysCommand -from .output import log +from .exceptions import SysCallError +from .output import error, info, debug from .pacman import run_pacman -from .storage import storage def get_hw_addr(ifname :str) -> str: import fcntl s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', bytes(ifname, 'utf-8')[:15])) - return ':'.join('%02x' % b for b in info[18:24]) + ret = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', bytes(ifname, 'utf-8')[:15])) + return ':'.join('%02x' % b for b in ret[18:24]) def list_interfaces(skip_loopback :bool = True) -> Dict[str, str]: @@ -36,26 +33,26 @@ def list_interfaces(skip_loopback :bool = True) -> Dict[str, str]: def check_mirror_reachable() -> bool: - log("Testing connectivity to the Arch Linux mirrors ...", level=logging.INFO) + info("Testing connectivity to the Arch Linux mirrors...") try: run_pacman("-Sy") return True except SysCallError as err: if os.geteuid() != 0: - log("check_mirror_reachable() uses 'pacman -Sy' which requires root.", level=logging.ERROR, fg="red") - log(f'exit_code: {err.exit_code}, Error: {err.message}', level=logging.DEBUG) + error("check_mirror_reachable() uses 'pacman -Sy' which requires root.") + debug(f'exit_code: {err.exit_code}, Error: {err.message}') return False def update_keyring() -> bool: - log("Updating archlinux-keyring ...", level=logging.INFO) + info("Updating archlinux-keyring ...") try: run_pacman("-Sy --noconfirm archlinux-keyring") return True except SysCallError: if os.geteuid() != 0: - log("update_keyring() uses 'pacman -Sy archlinux-keyring' which requires root.", level=logging.ERROR, fg="red") + error("update_keyring() uses 'pacman -Sy archlinux-keyring' which requires root.") return False @@ -80,38 +77,6 @@ def enrich_iface_types(interfaces: Union[Dict[str, Any], List[str]]) -> Dict[str return result -def wireless_scan(interface :str) -> None: - interfaces = enrich_iface_types(list(list_interfaces().values())) - if interfaces[interface] != 'WIRELESS': - raise HardwareIncompatibilityError(f"Interface {interface} is not a wireless interface: {interfaces}") - - try: - SysCommand(f"iwctl station {interface} scan") - except SysCallError as error: - raise SystemError(f"Could not scan for wireless networks: {error}") - - if '_WIFI' not in storage: - storage['_WIFI'] = {} - if interface not in storage['_WIFI']: - storage['_WIFI'][interface] = {} - - storage['_WIFI'][interface]['scanning'] = True - - -# TODO: Full WiFi experience might get evolved in the future, pausing for now 2021-01-25 -def get_wireless_networks(interface :str) -> None: - # TODO: Make this oneliner pritter to check if the interface is scanning or not. - # TODO: Rename this to list_wireless_networks() as it doesn't return anything - if '_WIFI' not in storage or interface not in storage['_WIFI'] or storage['_WIFI'][interface].get('scanning', False) is False: - import time - - wireless_scan(interface) - time.sleep(5) - - for line in SysCommand(f"iwctl station {interface} get-networks"): - print(line) - - def fetch_data_from_url(url: str, params: Optional[Dict] = None) -> str: ssl_context = ssl.create_default_context() ssl_context.check_hostname = False diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index d65f835f..bd31b5b3 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -1,15 +1,16 @@ import logging import os import sys +from enum import Enum + from pathlib import Path from typing import Dict, Union, List, Any, Callable, Optional +from dataclasses import asdict, is_dataclass from .storage import storage -from dataclasses import asdict, is_dataclass class FormattedOutput: - @classmethod def values( cls, @@ -118,7 +119,7 @@ class FormattedOutput: class Journald: @staticmethod - def log(message :str, level :int = logging.DEBUG) -> None: + def log(message: str, level: int = logging.DEBUG) -> None: try: import systemd.journal # type: ignore except ModuleNotFoundError: @@ -134,16 +135,37 @@ class Journald: log_adapter.log(level, message) -# TODO: Replace log() for session based logging. -class SessionLogging: - def __init__(self): - pass +def check_log_permissions(): + filename = storage.get('LOG_FILE', None) + + if not filename: + return + + log_dir = storage.get('LOG_PATH', Path('./')) + absolute_logfile = log_dir / filename + + try: + log_dir.mkdir(exist_ok=True, parents=True) + with absolute_logfile.open('a') as fp: + fp.write('') + except PermissionError: + # Fallback to creating the log file in the current folder + fallback_log_file = Path('./').absolute() / filename + absolute_logfile = fallback_log_file + absolute_logfile.mkdir(exist_ok=True, parents=True) + storage['LOG_PATH'] = Path('./').absolute() + err_string = f"Not enough permission to place log file at {absolute_logfile}, creating it in {fallback_log_file} instead." + warn(err_string) -# Found first reference here: https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python -# And re-used this: https://github.com/django/django/blob/master/django/core/management/color.py#L12 -def supports_color() -> bool: + +def _supports_color() -> bool: """ + Found first reference here: + https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python + And re-used this: + https://github.com/django/django/blob/master/django/core/management/color.py#L12 + Return True if the running system's terminal supports color, and False otherwise. """ @@ -154,13 +176,30 @@ def supports_color() -> bool: return supported_platform and is_a_tty -# Heavily influenced by: https://github.com/django/django/blob/ae8338daf34fd746771e0678081999b656177bae/django/utils/termcolors.py#L13 -# Color options here: https://askubuntu.com/questions/528928/how-to-do-underline-bold-italic-strikethrough-color-background-and-size-i -def stylize_output(text: str, *opts :str, **kwargs) -> str: +class Font(Enum): + bold = '1' + italic = '3' + underscore = '4' + blink = '5' + reverse = '7' + conceal = '8' + + +def _stylize_output( + text: str, + fg: str, + bg: Optional[str], + reset: bool, + font: List[Font] = [], +) -> str: """ + Heavily influenced by: + https://github.com/django/django/blob/ae8338daf34fd746771e0678081999b656177bae/django/utils/termcolors.py#L13 + Color options here: + https://askubuntu.com/questions/528928/how-to-do-underline-bold-italic-strikethrough-color-background-and-size-i + Adds styling to a text given a set of color arguments. """ - opt_dict = {'bold': '1', 'italic': '3', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'} colors = { 'black' : '0', 'red' : '1', @@ -178,65 +217,72 @@ def stylize_output(text: str, *opts :str, **kwargs) -> str: 'darkgray' : '8;5;240', 'lightgray' : '8;5;256' } + foreground = {key: f'3{colors[key]}' for key in colors} background = {key: f'4{colors[key]}' for key in colors} - reset = '0' - code_list = [] - if text == '' and len(opts) == 1 and opts[0] == 'reset': - return '\x1b[%sm' % reset - for k, v in kwargs.items(): - if k == 'fg': - code_list.append(foreground[str(v)]) - elif k == 'bg': - code_list.append(background[str(v)]) + if text == '' and reset: + return '\x1b[%sm' % '0' + + code_list.append(foreground[str(fg)]) + + if bg: + code_list.append(background[str(bg)]) + + for o in font: + code_list.append(o.value) + + ansi = ';'.join(code_list) + + return f'\033[{ansi}m{text}\033[0m' + - for o in opts: - if o in opt_dict: - code_list.append(opt_dict[o]) +def info(*msgs: str): + log(*msgs, level=logging.INFO) - if 'noreset' not in opts: - text = '%s\x1b[%sm' % (text or '', reset) - return '%s%s' % (('\x1b[%sm' % ';'.join(code_list)), text or '') +def debug(*msgs: str): + log(*msgs, level=logging.DEBUG) -def log(*args :str, **kwargs :Union[str, int, Dict[str, Union[str, int]]]) -> None: - string = orig_string = ' '.join([str(x) for x in args]) +def error(*msgs: str): + log(*msgs, level=logging.ERROR, fg='red') + + +def warn(*msgs: str): + log(*msgs, level=logging.WARNING, fg='yellow') + + +def log( + *msgs: str, + level: int = logging.INFO, + fg: str = 'white', + bg: Optional[str] = None, + reset: bool = False, + font: List[Font] = [] +): + text = orig_string = ' '.join([str(x) for x in msgs]) # Attempt to colorize the output if supported # Insert default colors and override with **kwargs - if supports_color(): - kwargs = {'fg': 'white', **kwargs} - string = stylize_output(string, **kwargs) + if _supports_color(): + text = _stylize_output(text, fg, bg, reset, font) # If a logfile is defined in storage, # we use that one to output everything if filename := storage.get('LOG_FILE', None): - absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename) + log_dir = storage.get('LOG_PATH', Path('./')) + absolute_logfile = log_dir / filename - try: - Path(absolute_logfile).parents[0].mkdir(exist_ok=True, parents=True) - with open(absolute_logfile, 'a') as log_file: - log_file.write("") - except PermissionError: - # Fallback to creating the log file in the current folder - err_string = f"Not enough permission to place log file at {absolute_logfile}, creating it in {Path('./').absolute() / filename} instead." - absolute_logfile = Path('./').absolute() / filename - absolute_logfile.parents[0].mkdir(exist_ok=True) - absolute_logfile = str(absolute_logfile) - storage['LOG_PATH'] = './' - log(err_string, fg="red") - - with open(absolute_logfile, 'a') as log_file: - log_file.write(f"{orig_string}\n") - - Journald.log(string, level=int(str(kwargs.get('level', logging.INFO)))) + with open(absolute_logfile, 'a') as fp: + fp.write(f"{orig_string}\n") + + Journald.log(text, level=level) # Finally, print the log unless we skipped it based on level. # We use sys.stdout.write()+flush() instead of print() to try and # fix issue #94 - if kwargs.get('level', logging.INFO) != logging.DEBUG or storage.get('arguments', {}).get('verbose', False): - sys.stdout.write(f"{string}\n") + if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False): + sys.stdout.write(f"{text}\n") sys.stdout.flush() diff --git a/archinstall/lib/pacman.py b/archinstall/lib/pacman.py index 0dfd5afa..f5514f05 100644 --- a/archinstall/lib/pacman.py +++ b/archinstall/lib/pacman.py @@ -1,10 +1,9 @@ -import logging import pathlib import time from typing import TYPE_CHECKING, Any from .general import SysCommand -from .output import log +from .output import warn, error if TYPE_CHECKING: _: Any @@ -19,14 +18,14 @@ def run_pacman(args :str, default_cmd :str = 'pacman') -> SysCommand: pacman_db_lock = pathlib.Path('/var/lib/pacman/db.lck') if pacman_db_lock.exists(): - log(_('Pacman is already running, waiting maximum 10 minutes for it to terminate.'), level=logging.WARNING, fg="red") + warn(_('Pacman is already running, waiting maximum 10 minutes for it to terminate.')) started = time.time() while pacman_db_lock.exists(): time.sleep(0.25) if time.time() - started > (60 * 10): - log(_('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.'), level=logging.WARNING, fg="red") + error(_('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.')) exit(1) return SysCommand(f'{default_cmd} {args}') diff --git a/archinstall/lib/plugins.py b/archinstall/lib/plugins.py index b1ece04f..4ccb0666 100644 --- a/archinstall/lib/plugins.py +++ b/archinstall/lib/plugins.py @@ -1,6 +1,5 @@ import hashlib import importlib -import logging import os import sys import urllib.parse @@ -9,7 +8,7 @@ from importlib import metadata from pathlib import Path from typing import Optional, List -from .output import log +from .output import error, info, warn from .storage import storage plugins = {} @@ -24,11 +23,13 @@ for plugin_definition in metadata.entry_points().select(group='archinstall.plugi try: plugins[plugin_definition.name] = plugin_entrypoint() except Exception as err: - log(f'Error: {err}', level=logging.ERROR) - log(f"The above error was detected when loading the plugin: {plugin_definition}", fg="red", level=logging.ERROR) + error( + f'Error: {err}', + f"The above error was detected when loading the plugin: {plugin_definition}" + ) -def localize_path(path: Path) -> Path: +def _localize_path(path: Path) -> Path: """ Support structures for load_plugin() """ @@ -45,7 +46,7 @@ def localize_path(path: Path) -> Path: return path -def import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str]: +def _import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str]: if not namespace: namespace = os.path.basename(path) @@ -61,8 +62,10 @@ def import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str return namespace except Exception as err: - log(f'Error: {err}', level=logging.ERROR) - log(f"The above error was detected when loading the plugin: {path}", fg="red", level=logging.ERROR) + error( + f'Error: {err}', + f"The above error was detected when loading the plugin: {path}" + ) try: del sys.modules[namespace] @@ -72,7 +75,7 @@ def import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str return namespace -def find_nth(haystack: List[str], needle: str, n: int) -> Optional[int]: +def _find_nth(haystack: List[str], needle: str, n: int) -> Optional[int]: indices = [idx for idx, elem in enumerate(haystack) if elem == needle] if n <= len(indices): return indices[n - 1] @@ -82,34 +85,36 @@ def find_nth(haystack: List[str], needle: str, n: int) -> Optional[int]: def load_plugin(path: Path): namespace: Optional[str] = None parsed_url = urllib.parse.urlparse(str(path)) - log(f"Loading plugin from url {parsed_url}.", level=logging.INFO) + info(f"Loading plugin from url {parsed_url}") # The Profile was not a direct match on a remote URL if not parsed_url.scheme: # Path was not found in any known examples, check if it's an absolute path if os.path.isfile(path): - namespace = import_via_path(path) + namespace = _import_via_path(path) elif parsed_url.scheme in ('https', 'http'): - localized = localize_path(path) - namespace = import_via_path(localized) + localized = _localize_path(path) + namespace = _import_via_path(localized) if namespace and namespace in sys.modules: # Version dependency via __archinstall__version__ variable (if present) in the plugin # Any errors in version inconsistency will be handled through normal error handling if not defined. if hasattr(sys.modules[namespace], '__archinstall__version__'): - archinstall_major_and_minor_version = float(storage['__version__'][:find_nth(storage['__version__'], '.', 2)]) + archinstall_major_and_minor_version = float(storage['__version__'][:_find_nth(storage['__version__'], '.', 2)]) if sys.modules[namespace].__archinstall__version__ < archinstall_major_and_minor_version: - log(f"Plugin {sys.modules[namespace]} does not support the current Archinstall version.", fg="red", level=logging.ERROR) + error(f"Plugin {sys.modules[namespace]} does not support the current Archinstall version.") # Locate the plugin entry-point called Plugin() # This in accordance with the entry_points() from setup.cfg above if hasattr(sys.modules[namespace], 'Plugin'): try: plugins[namespace] = sys.modules[namespace].Plugin() - log(f"Plugin {plugins[namespace]} has been loaded.", fg="gray", level=logging.INFO) + info(f"Plugin {plugins[namespace]} has been loaded.") except Exception as err: - log(f'Error: {err}', level=logging.ERROR) - log(f"The above error was detected when initiating the plugin: {path}", fg="red", level=logging.ERROR) + error( + f'Error: {err}', + f"The above error was detected when initiating the plugin: {path}" + ) else: - log(f"Plugin '{path}' is missing a valid entry-point or is corrupt.", fg="yellow", level=logging.WARNING) + warn(f"Plugin '{path}' is missing a valid entry-point or is corrupt.") diff --git a/archinstall/lib/profile/profile_menu.py b/archinstall/lib/profile/profile_menu.py index 6462685a..213466a6 100644 --- a/archinstall/lib/profile/profile_menu.py +++ b/archinstall/lib/profile/profile_menu.py @@ -6,7 +6,7 @@ from archinstall.default_profiles.profile import Profile, GreeterType from .profile_model import ProfileConfiguration from ..hardware import AVAILABLE_GFX_DRIVERS from ..menu import Menu, MenuSelectionType, AbstractSubMenu, Selector -from ..user_interaction.system_conf import select_driver +from ..interactions.system_conf import select_driver if TYPE_CHECKING: _: Any diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 6ed95f8e..16fef251 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -1,7 +1,6 @@ from __future__ import annotations import importlib.util -import logging import sys from collections import Counter from functools import cached_property @@ -15,7 +14,7 @@ from .profile_model import ProfileConfiguration from ..hardware import AVAILABLE_GFX_DRIVERS from ..menu import MenuSelectionType, Menu, MenuSelection from ..networking import list_interfaces, fetch_data_from_url -from ..output import log +from ..output import error, debug, info, warn from ..storage import storage if TYPE_CHECKING: @@ -106,7 +105,7 @@ class ProfileHandler: invalid = ', '.join([k for k, v in resolved.items() if v is None]) if invalid: - log(f'No profile definition found: {invalid}') + info(f'No profile definition found: {invalid}') custom_settings = profile_config.get('custom_settings', {}) for profile in valid: @@ -216,7 +215,7 @@ class ProfileHandler: install_session.add_additional_packages(additional_pkg) except Exception as err: - log(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}", level=logging.WARNING, fg="yellow") + warn(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}") # Prep didn't run, so there's no driver to install install_session.add_additional_packages(['xorg-server', 'xorg-xinit']) @@ -250,7 +249,7 @@ class ProfileHandler: self.add_custom_profiles(profiles) except ValueError: err = str(_('Unable to fetch profile from specified url: {}')).format(url) - log(err, level=logging.ERROR, fg="red") + error(err) def _load_profile_class(self, module: ModuleType) -> List[Profile]: """ @@ -264,7 +263,7 @@ class ProfileHandler: if isinstance(cls_, Profile): profiles.append(cls_) except Exception: - log(f'Cannot import {module}, it does not appear to be a Profile class', level=logging.DEBUG) + debug(f'Cannot import {module}, it does not appear to be a Profile class') return profiles @@ -278,7 +277,7 @@ class ProfileHandler: if len(duplicates) > 0: err = str(_('Profiles must have unique name, but profile definitions with duplicate name found: {}')).format(duplicates[0][0]) - log(err, level=logging.ERROR, fg="red") + error(err) sys.exit(1) def _is_legacy(self, file: Path) -> bool: @@ -297,15 +296,15 @@ class ProfileHandler: Process a file for profile definitions """ if self._is_legacy(file): - log(f'Cannot import {file} because it is no longer supported, please use the new profile format') + info(f'Cannot import {file} because it is no longer supported, please use the new profile format') return [] if not file.is_file(): - log(f'Cannot find profile file {file}') + info(f'Cannot find profile file {file}') return [] name = file.name.removesuffix(file.suffix) - log(f'Importing profile: {file}', level=logging.DEBUG) + debug(f'Importing profile: {file}') try: spec = importlib.util.spec_from_file_location(name, file) @@ -315,7 +314,7 @@ class ProfileHandler: spec.loader.exec_module(imported) return self._load_profile_class(imported) except Exception as e: - log(f'Unable to parse file {file}: {e}', level=logging.ERROR) + error(f'Unable to parse file {file}: {e}') return [] diff --git a/archinstall/lib/services.py b/archinstall/lib/services.py deleted file mode 100644 index b177052b..00000000 --- a/archinstall/lib/services.py +++ /dev/null @@ -1,11 +0,0 @@ -import os -from .general import SysCommand - - -def service_state(service_name: str) -> str: - if os.path.splitext(service_name)[1] != '.service': - service_name += '.service' # Just to be safe - - state = b''.join(SysCommand(f'systemctl show --no-pager -p SubState --value {service_name}', environment_vars={'SYSTEMD_COLORS': '0'})) - - return state.strip().decode('UTF-8') diff --git a/archinstall/lib/storage.py b/archinstall/lib/storage.py index 5a54d816..2f256e5d 100644 --- a/archinstall/lib/storage.py +++ b/archinstall/lib/storage.py @@ -11,8 +11,8 @@ from pathlib import Path storage: Dict[str, Any] = { 'PROFILE': Path(__file__).parent.parent.joinpath('default_profiles'), - 'LOG_PATH': '/var/log/archinstall', - 'LOG_FILE': 'install.log', + 'LOG_PATH': Path('/var/log/archinstall'), + 'LOG_FILE': Path('install.log'), 'MOUNT_POINT': Path('/mnt/archinstall'), 'ENC_IDENTIFIER': 'ainst', 'DISK_TIMEOUTS' : 1, # seconds diff --git a/archinstall/lib/systemd.py b/archinstall/lib/systemd.py deleted file mode 100644 index 6ccbc5f6..00000000 --- a/archinstall/lib/systemd.py +++ /dev/null @@ -1,110 +0,0 @@ -import logging -import time -from typing import Iterator, Optional -from .exceptions import SysCallError -from .general import SysCommand, SysCommandWorker, locate_binary -from .installer import Installer -from .output import log -from .storage import storage - - -class Boot: - def __init__(self, installation: Installer): - self.instance = installation - self.container_name = 'archinstall' - self.session: Optional[SysCommandWorker] = None - self.ready = False - - def __enter__(self) -> 'Boot': - if (existing_session := storage.get('active_boot', None)) and existing_session.instance != self.instance: - raise KeyError("Archinstall only supports booting up one instance, and a active session is already active and it is not this one.") - - if existing_session: - self.session = existing_session.session - self.ready = existing_session.ready - else: - # '-P' or --console=pipe could help us not having to do a bunch - # of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual. - self.session = SysCommandWorker([ - '/usr/bin/systemd-nspawn', - '-D', str(self.instance.target), - '--timezone=off', - '-b', - '--no-pager', - '--machine', self.container_name - ]) - - if not self.ready and self.session: - while self.session.is_alive(): - if b' login:' in self.session: - self.ready = True - break - - storage['active_boot'] = self - return self - - def __exit__(self, *args :str, **kwargs :str) -> None: - # b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync. - # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager - - if len(args) >= 2 and args[1]: - log(args[1], level=logging.ERROR, fg='red') - log(f"The error above occurred in a temporary boot-up of the installation {self.instance}", level=logging.ERROR, fg="red") - - shutdown = None - shutdown_exit_code: Optional[int] = -1 - - try: - shutdown = SysCommand(f'systemd-run --machine={self.container_name} --pty shutdown now') - except SysCallError as error: - shutdown_exit_code = error.exit_code - - if self.session: - while self.session.is_alive(): - time.sleep(0.25) - - if shutdown and shutdown.exit_code: - shutdown_exit_code = shutdown.exit_code - - if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0): - storage['active_boot'] = None - else: - session_exit_code = self.session.exit_code if self.session else -1 - - raise SysCallError( - f"Could not shut down temporary boot of {self.instance}: {session_exit_code}/{shutdown_exit_code}", - exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])) - ) - - def __iter__(self) -> Iterator[bytes]: - if self.session: - for value in self.session: - yield value - - def __contains__(self, key: bytes) -> bool: - if self.session is None: - return False - - return key in self.session - - def is_alive(self) -> bool: - if self.session is None: - return False - - return self.session.is_alive() - - def SysCommand(self, cmd: list, *args, **kwargs) -> SysCommand: - if cmd[0][0] != '/' and cmd[0][:2] != './': - # This check is also done in SysCommand & SysCommandWorker. - # However, that check is done for `machinectl` and not for our chroot command. - # So this wrapper for SysCommand will do this additionally. - - cmd[0] = locate_binary(cmd[0]) - - return SysCommand(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) - - def SysCommandWorker(self, cmd: list, *args, **kwargs) -> SysCommandWorker: - if cmd[0][0] != '/' and cmd[0][:2] != './': - cmd[0] = locate_binary(cmd[0]) - - return SysCommandWorker(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) diff --git a/archinstall/lib/translationhandler.py b/archinstall/lib/translationhandler.py index 0d74f974..5f0f0695 100644 --- a/archinstall/lib/translationhandler.py +++ b/archinstall/lib/translationhandler.py @@ -1,14 +1,14 @@ from __future__ import annotations import json -import logging import os import gettext from dataclasses import dataclass from pathlib import Path from typing import List, Dict, Any, TYPE_CHECKING, Optional -from .exceptions import TranslationError + +from .output import error, debug if TYPE_CHECKING: _: Any @@ -80,8 +80,8 @@ class TranslationHandler: language = Language(abbr, lang, translation, percent, translated_lang) languages.append(language) - except FileNotFoundError as error: - raise TranslationError(f"Could not locate language file for '{lang}': {error}") + except FileNotFoundError as err: + raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}") return languages @@ -89,12 +89,12 @@ class TranslationHandler: """ Set the provided font as the new terminal font """ - from .general import SysCommand, log + from .general import SysCommand try: - log(f'Setting font: {font}', level=logging.DEBUG) + debug(f'Setting font: {font}') SysCommand(f'setfont {font}') except Exception: - log(f'Unable to set font {font}', level=logging.ERROR) + error(f'Unable to set font {font}') def _load_language_mappings(self) -> List[Dict[str, Any]]: """ diff --git a/archinstall/lib/user_interaction/__init__.py b/archinstall/lib/user_interaction/__init__.py deleted file mode 100644 index 5ee89de0..00000000 --- a/archinstall/lib/user_interaction/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .manage_users_conf import ask_for_additional_users -from .locale_conf import select_locale_lang, select_locale_enc -from .system_conf import select_kernel, select_driver, ask_for_bootloader, ask_for_swap -from .network_conf import ask_to_configure_network -from .general_conf import ( - ask_ntp, ask_for_a_timezone, ask_for_audio_selection, select_language, select_mirror_regions, - select_archinstall_language, ask_additional_packages_to_install, - select_additional_repositories, ask_hostname, add_number_of_parrallel_downloads -) -from .utils import get_password diff --git a/archinstall/lib/user_interaction/disk_conf.py b/archinstall/lib/user_interaction/disk_conf.py deleted file mode 100644 index a77e950a..00000000 --- a/archinstall/lib/user_interaction/disk_conf.py +++ /dev/null @@ -1,391 +0,0 @@ -from __future__ import annotations - -import logging -from pathlib import Path -from typing import Any, TYPE_CHECKING, Optional, List, Tuple - -from .. import disk -from ..hardware import has_uefi -from ..menu import Menu, MenuSelectionType, TableMenu -from ..output import FormattedOutput -from ..output import log -from ..utils.util import prompt_dir - -if TYPE_CHECKING: - _: Any - - -def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]: - """ - Asks the user to select one or multiple devices - - :return: List of selected devices - :rtype: list - """ - - def _preview_device_selection(selection: disk._DeviceInfo) -> Optional[str]: - dev = disk.device_handler.get_device(selection.path) - if dev and dev.partition_infos: - return FormattedOutput.as_table(dev.partition_infos) - return None - - if preset is None: - preset = [] - - title = str(_('Select one or more devices to use and configure')) - warning = str(_('If you reset the device selection this will also reset the current disk layout. Are you sure?')) - - devices = disk.device_handler.devices - options = [d.device_info for d in devices] - preset_value = [p.device_info for p in preset] - - choice = TableMenu( - title, - data=options, - multi=True, - preset=preset_value, - preview_command=_preview_device_selection, - preview_title=str(_('Existing Partitions')), - preview_size=0.2, - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match choice.type_: - case MenuSelectionType.Reset: return [] - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: - selected_device_info: List[disk._DeviceInfo] = choice.value # type: ignore - selected_devices = [] - - for device in devices: - if device.device_info in selected_device_info: - selected_devices.append(device) - - return selected_devices - - -def get_default_partition_layout( - devices: List[disk.BDevice], - filesystem_type: Optional[disk.FilesystemType] = None, - advanced_option: bool = False -) -> List[disk.DeviceModification]: - - if len(devices) == 1: - device_modification = suggest_single_disk_layout( - devices[0], - filesystem_type=filesystem_type, - advanced_options=advanced_option - ) - return [device_modification] - else: - return suggest_multi_disk_layout( - devices, - filesystem_type=filesystem_type, - advanced_options=advanced_option - ) - - -def _manual_partitioning( - preset: List[disk.DeviceModification], - devices: List[disk.BDevice] -) -> List[disk.DeviceModification]: - modifications = [] - for device in devices: - mod = next(filter(lambda x: x.device == device, preset), None) - if not mod: - mod = disk.DeviceModification(device, wipe=False) - - if partitions := disk.manual_partitioning(device, preset=mod.partitions): - mod.partitions = partitions - modifications.append(mod) - - return modifications - - -def select_disk_config( - preset: Optional[disk.DiskLayoutConfiguration] = None, - advanced_option: bool = False -) -> Optional[disk.DiskLayoutConfiguration]: - default_layout = disk.DiskLayoutType.Default.display_msg() - manual_mode = disk.DiskLayoutType.Manual.display_msg() - pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg() - - options = [default_layout, manual_mode, pre_mount_mode] - preset_value = preset.config_type.display_msg() if preset else None - warning = str(_('Are you sure you want to reset this setting?')) - - choice = Menu( - _('Select a partitioning option'), - options, - allow_reset=True, - allow_reset_warning_msg=warning, - sort=False, - preview_size=0.2, - preset_values=preset_value - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return None - case MenuSelectionType.Selection: - if choice.single_value == pre_mount_mode: - output = "You will use whatever drive-setup is mounted at the specified directory\n" - output += "WARNING: Archinstall won't check the suitability of this setup\n" - - path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output) - mods = disk.device_handler.detect_pre_mounted_mods(path) - - return disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Pre_mount, - relative_mountpoint=path, - device_modifications=mods - ) - - preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] - - devices = select_devices(preset_devices) - - if not devices: - return None - - if choice.value == default_layout: - modifications = get_default_partition_layout(devices, advanced_option=advanced_option) - if modifications: - return disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Default, - device_modifications=modifications - ) - elif choice.value == manual_mode: - preset_mods = preset.device_modifications if preset else [] - modifications = _manual_partitioning(preset_mods, devices) - - if modifications: - return disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Manual, - device_modifications=modifications - ) - - return None - - -def _boot_partition() -> disk.PartitionModification: - if has_uefi(): - start = disk.Size(1, disk.Unit.MiB) - size = disk.Size(512, disk.Unit.MiB) - else: - start = disk.Size(3, disk.Unit.MiB) - size = disk.Size(203, disk.Unit.MiB) - - # boot partition - return disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=start, - length=size, - mountpoint=Path('/boot'), - fs_type=disk.FilesystemType.Fat32, - flags=[disk.PartitionFlag.Boot] - ) - - -def ask_for_main_filesystem_format(advanced_options=False) -> disk.FilesystemType: - options = { - 'btrfs': disk.FilesystemType.Btrfs, - 'ext4': disk.FilesystemType.Ext4, - 'xfs': disk.FilesystemType.Xfs, - 'f2fs': disk.FilesystemType.F2fs - } - - if advanced_options: - options.update({'ntfs': disk.FilesystemType.Ntfs}) - - prompt = _('Select which filesystem your main partition should use') - choice = Menu(prompt, options, skip=False, sort=False).run() - return options[choice.single_value] - - -def suggest_single_disk_layout( - device: disk.BDevice, - filesystem_type: Optional[disk.FilesystemType] = None, - advanced_options: bool = False, - separate_home: Optional[bool] = None -) -> disk.DeviceModification: - if not filesystem_type: - filesystem_type = ask_for_main_filesystem_format(advanced_options) - - min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB) - root_partition_size = disk.Size(20, disk.Unit.GiB) - using_subvolumes = False - using_home_partition = False - compression = False - device_size_gib = device.device_info.total_size - - if filesystem_type == disk.FilesystemType.Btrfs: - prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - using_subvolumes = choice.value == Menu.yes() - - prompt = str(_('Would you like to use BTRFS compression?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - compression = choice.value == Menu.yes() - - device_modification = disk.DeviceModification(device, wipe=True) - - # Used for reference: https://wiki.archlinux.org/title/partitioning - # 2 MiB is unallocated for GRUB on BIOS. Potentially unneeded for other bootloaders? - - # TODO: On BIOS, /boot partition is only needed if the drive will - # be encrypted, otherwise it is not recommended. We should probably - # add a check for whether the drive will be encrypted or not. - - # Increase the UEFI partition if UEFI is detected. - # Also re-align the start to 1MiB since we don't need the first sectors - # like we do in MBR layouts where the boot loader is installed traditionally. - - boot_partition = _boot_partition() - device_modification.add_partition(boot_partition) - - if not using_subvolumes: - if device_size_gib >= min_size_to_allow_home_part: - if separate_home is None: - prompt = str(_('Would you like to create a separate partition for /home?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - using_home_partition = choice.value == Menu.yes() - elif separate_home is True: - using_home_partition = True - else: - using_home_partition = False - - # root partition - start = disk.Size(513, disk.Unit.MiB) if has_uefi() else disk.Size(206, disk.Unit.MiB) - - # Set a size for / (/root) - if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: - length = disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size) - else: - length = min(device.device_info.total_size, root_partition_size) - - root_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=start, - length=length, - mountpoint=Path('/') if not using_subvolumes else None, - fs_type=filesystem_type, - mount_options=['compress=zstd'] if compression else [], - ) - device_modification.add_partition(root_partition) - - if using_subvolumes: - # https://btrfs.wiki.kernel.org/index.php/FAQ - # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash - # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh - subvolumes = [ - disk.SubvolumeModification(Path('@'), Path('/')), - disk.SubvolumeModification(Path('@home'), Path('/home')), - disk.SubvolumeModification(Path('@log'), Path('/var/log')), - disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), - disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')) - ] - root_partition.btrfs_subvols = subvolumes - elif using_home_partition: - # If we don't want to use subvolumes, - # But we want to be able to re-use data between re-installs.. - # A second partition for /home would be nice if we have the space for it - home_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=root_partition.length, - length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), - mountpoint=Path('/home'), - fs_type=filesystem_type, - mount_options=['compress=zstd'] if compression else [] - ) - device_modification.add_partition(home_partition) - - return device_modification - - -def suggest_multi_disk_layout( - devices: List[disk.BDevice], - filesystem_type: Optional[disk.FilesystemType] = None, - advanced_options: bool = False -) -> List[disk.DeviceModification]: - if not devices: - return [] - - # Not really a rock solid foundation of information to stand on, but it's a start: - # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ - # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ - min_home_partition_size = disk.Size(40, disk.Unit.GiB) - # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? - desired_root_partition_size = disk.Size(20, disk.Unit.GiB) - compression = False - - if not filesystem_type: - filesystem_type = ask_for_main_filesystem_format(advanced_options) - - # find proper disk for /home - possible_devices = list(filter(lambda x: x.device_info.total_size >= min_home_partition_size, devices)) - home_device = max(possible_devices, key=lambda d: d.device_info.total_size) if possible_devices else None - - # find proper device for /root - devices_delta = {} - for device in devices: - if device is not home_device: - delta = device.device_info.total_size - desired_root_partition_size - devices_delta[device] = delta - - sorted_delta: List[Tuple[disk.BDevice, Any]] = sorted(devices_delta.items(), key=lambda x: x[1]) - root_device: Optional[disk.BDevice] = sorted_delta[0][0] - - if home_device is None or root_device is None: - text = _('The selected drives do not have the minimum capacity required for an automatic suggestion\n') - text += _('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB)) - text += _('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB)) - Menu(str(text), [str(_('Continue'))], skip=False).run() - return [] - - if filesystem_type == disk.FilesystemType.Btrfs: - prompt = str(_('Would you like to use BTRFS compression?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - compression = choice.value == Menu.yes() - - device_paths = ', '.join([str(d.device_info.path) for d in devices]) - log(f"Suggesting multi-disk-layout for devices: {device_paths}", level=logging.DEBUG) - log(f"/root: {root_device.device_info.path}", level=logging.DEBUG) - log(f"/home: {home_device.device_info.path}", level=logging.DEBUG) - - root_device_modification = disk.DeviceModification(root_device, wipe=True) - home_device_modification = disk.DeviceModification(home_device, wipe=True) - - # add boot partition to the root device - boot_partition = _boot_partition() - root_device_modification.add_partition(boot_partition) - - # add root partition to the root device - root_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=disk.Size(513, disk.Unit.MiB) if has_uefi() else disk.Size(206, disk.Unit.MiB), - length=disk.Size(100, disk.Unit.Percent, total_size=root_device.device_info.total_size), - mountpoint=Path('/'), - mount_options=['compress=zstd'] if compression else [], - fs_type=filesystem_type - ) - root_device_modification.add_partition(root_partition) - - # add home partition to home device - home_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=disk.Size(1, disk.Unit.MiB), - length=disk.Size(100, disk.Unit.Percent, total_size=home_device.device_info.total_size), - mountpoint=Path('/home'), - mount_options=['compress=zstd'] if compression else [], - fs_type=filesystem_type, - ) - home_device_modification.add_partition(home_partition) - - return [root_device_modification, home_device_modification] diff --git a/archinstall/lib/user_interaction/general_conf.py b/archinstall/lib/user_interaction/general_conf.py deleted file mode 100644 index 9722dc4d..00000000 --- a/archinstall/lib/user_interaction/general_conf.py +++ /dev/null @@ -1,244 +0,0 @@ -from __future__ import annotations - -import logging -import pathlib -from typing import List, Any, Optional, Dict, TYPE_CHECKING - -from ..locale_helpers import list_keyboard_languages, list_timezones -from ..menu import MenuSelectionType, Menu, TextInput -from ..mirrors import list_mirrors -from ..output import log -from ..packages.packages import validate_package_list -from ..storage import storage -from ..translationhandler import Language - -if TYPE_CHECKING: - _: Any - - -def ask_ntp(preset: bool = True) -> bool: - prompt = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n')) - prompt += str(_('Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki')) - if preset: - preset_val = Menu.yes() - else: - preset_val = Menu.no() - choice = Menu(prompt, Menu.yes_no(), skip=False, preset_values=preset_val, default_option=Menu.yes()).run() - - return False if choice.value == Menu.no() else True - - -def ask_hostname(preset: str = '') -> str: - while True: - hostname = TextInput( - str(_('Desired hostname for the installation: ')), - preset - ).run().strip() - - if hostname: - return hostname - - -def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: - timezones = list_timezones() - default = 'UTC' - - choice = Menu( - _('Select a timezone'), - list(timezones), - preset_values=preset, - default_option=default - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.single_value - - return None - - -def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]: - no_audio = str(_('No audio server')) - choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] - default = 'pipewire' if desktop else no_audio - - choice = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=default).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.single_value - - return None - - -def select_language(preset: Optional[str] = None) -> Optional[str]: - """ - Asks the user to select a language - Usually this is combined with :ref:`archinstall.list_keyboard_languages`. - - :return: The language/dictionary key of the selected language - :rtype: str - """ - kb_lang = list_keyboard_languages() - # sort alphabetically and then by length - sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) - - choice = Menu( - _('Select keyboard layout'), - sorted_kb_lang, - preset_values=preset, - sort=False - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.single_value - - return None - - -def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: - """ - Asks the user to select a mirror or region - Usually this is combined with :ref:`archinstall.list_mirrors`. - - :return: The dictionary information about a mirror/region. - :rtype: dict - """ - if preset_values is None: - preselected = None - else: - preselected = list(preset_values.keys()) - - mirrors = list_mirrors() - - choice = Menu( - _('Select one of the regions to download packages from'), - list(mirrors.keys()), - preset_values=preselected, - multi=True, - allow_reset=True - ).run() - - match choice.type_: - case MenuSelectionType.Reset: - return {} - case MenuSelectionType.Skip: - return preset_values - case MenuSelectionType.Selection: - return {selected: mirrors[selected] for selected in choice.multi_value} - - return {} - - -def select_archinstall_language(languages: List[Language], preset: Language) -> Language: - # these are the displayed language names which can either be - # the english name of a language or, if present, the - # name of the language in its own language - options = {lang.display_name: lang for lang in languages} - - title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n' - title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n' - title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n' - - choice = Menu( - title, - list(options.keys()), - default_option=preset.display_name, - preview_size=0.5 - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return options[choice.single_value] - - raise ValueError('Language selection not handled') - - -def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: - # Additional packages (with some light weight error handling for invalid package names) - print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) - print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) - - def read_packages(already_defined: list = []) -> list: - display = ' '.join(already_defined) - input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip() - return input_packages.split() if input_packages else [] - - pre_set_packages = pre_set_packages if pre_set_packages else [] - packages = read_packages(pre_set_packages) - - if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']: - while True: - if len(packages): - # Verify packages that were given - print(_("Verifying that additional packages exist (this might take a few seconds)")) - valid, invalid = validate_package_list(packages) - - if invalid: - log(f"Some packages could not be found in the repository: {invalid}", level=logging.WARNING, fg='red') - packages = read_packages(valid) - continue - break - - return packages - - -def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Optional[int]: - max_downloads = 5 - print(_(f"This option enables the number of parallel downloads that can occur during installation")) - print(_(f"Enter the number of parallel downloads to be enabled.\n (Enter a value between 1 to {max_downloads})\nNote:")) - print(_(f" - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )")) - print(_(f" - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )")) - print(_(f" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )")) - - while True: - try: - input_number = int(TextInput(_("[Default value: 0] > ")).run().strip() or 0) - if input_number <= 0: - input_number = 0 - elif input_number > max_downloads: - input_number = max_downloads - break - except: - print(_(f"Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]")) - - pacman_conf_path = pathlib.Path("/etc/pacman.conf") - with pacman_conf_path.open() as f: - pacman_conf = f.read().split("\n") - - with pacman_conf_path.open("w") as fwrite: - for line in pacman_conf: - if "ParallelDownloads" in line: - fwrite.write(f"ParallelDownloads = {input_number+1}\n") if not input_number == 0 else fwrite.write("#ParallelDownloads = 0\n") - else: - fwrite.write(f"{line}\n") - - return input_number - - -def select_additional_repositories(preset: List[str]) -> List[str]: - """ - Allows the user to select additional repositories (multilib, and testing) if desired. - - :return: The string as a selected repository - :rtype: string - """ - - repositories = ["multilib", "testing"] - - choice = Menu( - _('Choose which optional additional repositories to enable'), - repositories, - sort=False, - multi=True, - preset_values=preset, - allow_reset=True - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return [] - case MenuSelectionType.Selection: return choice.single_value - - return [] diff --git a/archinstall/lib/user_interaction/locale_conf.py b/archinstall/lib/user_interaction/locale_conf.py deleted file mode 100644 index cdc3423a..00000000 --- a/archinstall/lib/user_interaction/locale_conf.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -from typing import Any, TYPE_CHECKING, Optional - -from ..locale_helpers import list_locales -from ..menu import Menu, MenuSelectionType - -if TYPE_CHECKING: - _: Any - - -def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: - locales = list_locales() - locale_lang = set([locale.split()[0] for locale in locales]) - - choice = Menu( - _('Choose which locale language to use'), - list(locale_lang), - sort=True, - preset_values=preset - ).run() - - match choice.type_: - case MenuSelectionType.Selection: return choice.single_value - case MenuSelectionType.Skip: return preset - - return None - - -def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: - locales = list_locales() - locale_enc = set([locale.split()[1] for locale in locales]) - - choice = Menu( - _('Choose which locale encoding to use'), - list(locale_enc), - sort=True, - preset_values=preset - ).run() - - match choice.type_: - case MenuSelectionType.Selection: return choice.single_value - case MenuSelectionType.Skip: return preset - - return None diff --git a/archinstall/lib/user_interaction/manage_users_conf.py b/archinstall/lib/user_interaction/manage_users_conf.py deleted file mode 100644 index 879578da..00000000 --- a/archinstall/lib/user_interaction/manage_users_conf.py +++ /dev/null @@ -1,106 +0,0 @@ -from __future__ import annotations - -import re -from typing import Any, Dict, TYPE_CHECKING, List, Optional - -from .utils import get_password -from ..menu import Menu, ListManager -from ..models.users import User -from ..output import FormattedOutput - -if TYPE_CHECKING: - _: Any - - -class UserList(ListManager): - """ - subclass of ListManager for the managing of user accounts - """ - - def __init__(self, prompt: str, lusers: List[User]): - self._actions = [ - str(_('Add a user')), - str(_('Change password')), - str(_('Promote/Demote user')), - str(_('Delete User')) - ] - super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) - - def reformat(self, data: List[User]) -> Dict[str, Any]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[User]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, user in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = user - - return display_data - - def selected_action_display(self, user: User) -> str: - return user.username - - def handle_action(self, action: str, entry: Optional[User], data: List[User]) -> List[User]: - if action == self._actions[0]: # add - new_user = self._add_user() - if new_user is not None: - # in case a user with the same username as an existing user - # was created we'll replace the existing one - data = [d for d in data if d.username != new_user.username] - data += [new_user] - elif action == self._actions[1] and entry: # change password - prompt = str(_('Password for user "{}": ').format(entry.username)) - new_password = get_password(prompt=prompt) - if new_password: - user = next(filter(lambda x: x == entry, data)) - user.password = new_password - elif action == self._actions[2] and entry: # promote/demote - user = next(filter(lambda x: x == entry, data)) - user.sudo = False if user.sudo else True - elif action == self._actions[3] and entry: # delete - data = [d for d in data if d != entry] - - return data - - def _check_for_correct_username(self, username: str) -> bool: - if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: - return True - return False - - def _add_user(self) -> Optional[User]: - prompt = '\n\n' + str(_('Enter username (leave blank to skip): ')) - - while True: - username = input(prompt).strip(' ') - if not username: - return None - if not self._check_for_correct_username(username): - error_prompt = str(_("The username you entered is invalid. Try again")) - print(error_prompt) - else: - break - - password = get_password(prompt=str(_('Password for user "{}": ').format(username))) - - if not password: - return None - - choice = Menu( - str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(), - skip=False, - default_option=Menu.yes(), - clear_screen=False, - show_search_hint=False - ).run() - - sudo = True if choice.value == Menu.yes() else False - return User(username, password, sudo) - - -def ask_for_additional_users(prompt: str = '', defined_users: List[User] = []) -> List[User]: - users = UserList(prompt, defined_users).run() - return users diff --git a/archinstall/lib/user_interaction/network_conf.py b/archinstall/lib/user_interaction/network_conf.py deleted file mode 100644 index b682c1d2..00000000 --- a/archinstall/lib/user_interaction/network_conf.py +++ /dev/null @@ -1,173 +0,0 @@ -from __future__ import annotations - -import ipaddress -import logging -from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict - -from ..menu import MenuSelectionType, TextInput -from ..models.network_configuration import NetworkConfiguration, NicType - -from ..networking import list_interfaces -from ..output import log, FormattedOutput -from ..menu import ListManager, Menu - -if TYPE_CHECKING: - _: Any - - -class ManualNetworkConfig(ListManager): - """ - subclass of ListManager for the managing of network configurations - """ - - def __init__(self, prompt: str, ifaces: List[NetworkConfiguration]): - self._actions = [ - str(_('Add interface')), - str(_('Edit interface')), - str(_('Delete interface')) - ] - - super().__init__(prompt, ifaces, [self._actions[0]], self._actions[1:]) - - def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[NetworkConfiguration]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, iface in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = iface - - return display_data - - def selected_action_display(self, iface: NetworkConfiguration) -> str: - return iface.iface if iface.iface else '' - - def handle_action(self, action: str, entry: Optional[NetworkConfiguration], data: List[NetworkConfiguration]): - if action == self._actions[0]: # add - iface_name = self._select_iface(data) - if iface_name: - iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) - iface = self._edit_iface(iface) - data += [iface] - elif entry: - if action == self._actions[1]: # edit interface - data = [d for d in data if d.iface != entry.iface] - data.append(self._edit_iface(entry)) - elif action == self._actions[2]: # delete - data = [d for d in data if d != entry] - - return data - - def _select_iface(self, data: List[NetworkConfiguration]) -> Optional[Any]: - all_ifaces = list_interfaces().values() - existing_ifaces = [d.iface for d in data] - available = set(all_ifaces) - set(existing_ifaces) - choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() - - if choice.type_ == MenuSelectionType.Skip: - return None - - return choice.value - - def _edit_iface(self, edit_iface: NetworkConfiguration): - iface_name = edit_iface.iface - modes = ['DHCP (auto detect)', 'IP (static)'] - default_mode = 'DHCP (auto detect)' - - prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) - mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() - - if mode.value == 'IP (static)': - while 1: - prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) - ip = TextInput(prompt, edit_iface.ip).run().strip() - # Implemented new check for correct IP/subnet input - try: - ipaddress.ip_interface(ip) - break - except ValueError: - log("You need to enter a valid IP in IP-config mode.", level=logging.WARNING, fg='red') - - # Implemented new check for correct gateway IP address - gateway = None - - while 1: - gateway = TextInput( - _('Enter your gateway (router) IP address or leave blank for none: '), - edit_iface.gateway - ).run().strip() - try: - if len(gateway) > 0: - ipaddress.ip_address(gateway) - break - except ValueError: - log("You need to enter a valid gateway (router) IP address.", level=logging.WARNING, fg='red') - - if edit_iface.dns: - display_dns = ' '.join(edit_iface.dns) - else: - display_dns = None - dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() - - dns = [] - if len(dns_input): - dns = dns_input.split(' ') - - return NetworkConfiguration(NicType.MANUAL, iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) - else: - # this will contain network iface names - return NetworkConfiguration(NicType.MANUAL, iface=iface_name) - - -def ask_to_configure_network( - preset: Union[NetworkConfiguration, List[NetworkConfiguration]] -) -> Optional[NetworkConfiguration | List[NetworkConfiguration]]: - """ - Configure the network on the newly installed system - """ - network_options = { - 'none': str(_('No network configuration')), - 'iso_config': str(_('Copy ISO network configuration to installation')), - 'network_manager': str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')), - 'manual': str(_('Manual configuration')) - } - # for this routine it's easier to set the cursor position rather than a preset value - cursor_idx = None - - if preset and not isinstance(preset, list): - if preset.type == 'iso_config': - cursor_idx = 0 - elif preset.type == 'network_manager': - cursor_idx = 1 - - warning = str(_('Are you sure you want to reset this setting?')) - - choice = Menu( - _('Select one network interface to configure'), - list(network_options.values()), - cursor_index=cursor_idx, - sort=False, - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return None - - if choice.value == network_options['none']: - return None - elif choice.value == network_options['iso_config']: - return NetworkConfiguration(NicType.ISO) - elif choice.value == network_options['network_manager']: - return NetworkConfiguration(NicType.NM) - elif choice.value == network_options['manual']: - preset_ifaces = preset if isinstance(preset, list) else [] - return ManualNetworkConfig('Configure interfaces', preset_ifaces).run() - - return preset diff --git a/archinstall/lib/user_interaction/save_conf.py b/archinstall/lib/user_interaction/save_conf.py deleted file mode 100644 index e05b9afe..00000000 --- a/archinstall/lib/user_interaction/save_conf.py +++ /dev/null @@ -1,113 +0,0 @@ -from __future__ import annotations - -import logging - -from pathlib import Path -from typing import Any, Dict, TYPE_CHECKING - -from ..general import SysCommand -from ..menu import Menu -from ..menu.menu import MenuSelectionType -from ..output import log -from ..configuration import ConfigurationOutput - -if TYPE_CHECKING: - _: Any - - -def save_config(config: Dict): - def preview(selection: str): - if options['user_config'] == selection: - serialized = config_output.user_config_to_json() - return f'{config_output.user_configuration_file}\n{serialized}' - elif options['user_creds'] == selection: - if maybe_serial := config_output.user_credentials_to_json(): - return f'{config_output.user_credentials_file}\n{maybe_serial}' - else: - return str(_('No configuration')) - elif options['all'] == selection: - output = f'{config_output.user_configuration_file}\n' - if config_output.user_credentials_to_json(): - output += f'{config_output.user_credentials_file}\n' - return output[:-1] - return None - - config_output = ConfigurationOutput(config) - - options = { - 'user_config': str(_('Save user configuration')), - 'user_creds': str(_('Save user credentials')), - 'disk_layout': str(_('Save disk layout')), - 'all': str(_('Save all')) - } - - choice = Menu( - _('Choose which configuration to save'), - list(options.values()), - sort=False, - skip=True, - preview_size=0.75, - preview_command=preview - ).run() - - if choice.type_ == MenuSelectionType.Skip: - return - - save_config_value = choice.single_value - saving_key = [k for k, v in options.items() if v == save_config_value][0] - - dirs_to_exclude = [ - '/bin', - '/dev', - '/lib', - '/lib64', - '/lost+found', - '/opt', - '/proc', - '/run', - '/sbin', - '/srv', - '/sys', - '/usr', - '/var', - ] - - log('Ignore configuration option folders: ' + ','.join(dirs_to_exclude), level=logging.DEBUG) - log(_('Finding possible directories to save configuration files ...'), level=logging.INFO) - - find_exclude = '-path ' + ' -prune -o -path '.join(dirs_to_exclude) + ' -prune ' - file_picker_command = f'find / {find_exclude} -o -type d -print0' - - directories = SysCommand(file_picker_command).decode() - - if directories is None: - raise ValueError('Failed to retrieve possible configuration directories') - - possible_save_dirs = list(filter(None, directories.split('\x00'))) - - selection = Menu( - _('Select directory (or directories) for saving configuration files'), - possible_save_dirs, - multi=True, - skip=True, - allow_reset=False, - ).run() - - match selection.type_: - case MenuSelectionType.Skip: - return - - save_dirs = selection.multi_value - - log(f'Saving {saving_key} configuration files to {save_dirs}', level=logging.DEBUG) - - if save_dirs is not None: - for save_dir_str in save_dirs: - save_dir = Path(save_dir_str) - if options['user_config'] == save_config_value: - config_output.save_user_config(save_dir) - elif options['user_creds'] == save_config_value: - config_output.save_user_creds(save_dir) - elif options['all'] == save_config_value: - config_output.save_user_config(save_dir) - config_output.save_user_creds(save_dir) diff --git a/archinstall/lib/user_interaction/system_conf.py b/archinstall/lib/user_interaction/system_conf.py deleted file mode 100644 index 3f57d0e7..00000000 --- a/archinstall/lib/user_interaction/system_conf.py +++ /dev/null @@ -1,117 +0,0 @@ -from __future__ import annotations - -from typing import List, Any, Dict, TYPE_CHECKING, Optional - -from ..hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics -from ..menu import MenuSelectionType, Menu -from ..models.bootloader import Bootloader - -if TYPE_CHECKING: - _: Any - - -def select_kernel(preset: List[str] = []) -> List[str]: - """ - Asks the user to select a kernel for system. - - :return: The string as a selected kernel - :rtype: string - """ - - kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] - default_kernel = "linux" - - warning = str(_('Are you sure you want to reset this setting?')) - - choice = Menu( - _('Choose which kernels to use or leave blank for default "{}"').format(default_kernel), - kernels, - sort=True, - multi=True, - preset_values=preset, - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return [] - case MenuSelectionType.Selection: return choice.value # type: ignore - - -def ask_for_bootloader(preset: Bootloader) -> Bootloader: - # when the system only supports grub - if not has_uefi(): - options = [Bootloader.Grub.value] - default = Bootloader.Grub.value - else: - options = Bootloader.values() - default = Bootloader.Systemd.value - - preset_value = preset.value if preset else None - - choice = Menu( - _('Choose a bootloader'), - options, - preset_values=preset_value, - sort=False, - default_option=default - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return Bootloader(choice.value) - - return preset - - -def select_driver(options: Dict[str, Any] = {}, current_value: Optional[str] = None) -> Optional[str]: - """ - Some what convoluted function, whose job is simple. - Select a graphics driver from a pre-defined set of popular options. - - (The template xorg is for beginner users, not advanced, and should - there for appeal to the general public first and edge cases later) - """ - - if not options: - options = AVAILABLE_GFX_DRIVERS - - drivers = sorted(list(options.keys())) - - if drivers: - title = '' - if has_amd_graphics(): - title += str(_('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.')) + '\n' - if has_intel_graphics(): - title += str(_('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n')) - if has_nvidia_graphics(): - title += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')) - - title += str(_('\nSelect a graphics driver or leave blank to install all open-source drivers')) - - preset = current_value if current_value else None - choice = Menu(title, drivers, preset_values=preset).run() - - if choice.type_ != MenuSelectionType.Selection: - return None - - return choice.value # type: ignore - - return current_value - - -def ask_for_swap(preset: bool = True) -> bool: - if preset: - preset_val = Menu.yes() - else: - preset_val = Menu.no() - - prompt = _('Would you like to use swap on zram?') - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True - - return preset diff --git a/archinstall/lib/user_interaction/utils.py b/archinstall/lib/user_interaction/utils.py deleted file mode 100644 index 918945c0..00000000 --- a/archinstall/lib/user_interaction/utils.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import annotations - -import getpass -from typing import Any, Optional, TYPE_CHECKING - -from ..models import PasswordStrength -from ..output import log - -if TYPE_CHECKING: - _: Any - -# used for signal handler -SIG_TRIGGER = None - - -def get_password(prompt: str = '') -> Optional[str]: - if not prompt: - prompt = _("Enter a password: ") - - while password := getpass.getpass(prompt): - if len(password.strip()) <= 0: - break - - strength = PasswordStrength.strength(password) - log(f'Password strength: {strength.value}', fg=strength.color()) - - passwd_verification = getpass.getpass(prompt=_('And one more time for verification: ')) - if password != passwd_verification: - log(' * Passwords did not match * ', fg='red') - continue - - return password - - return None diff --git a/archinstall/lib/utils/util.py b/archinstall/lib/utils/util.py index ded480ae..34716f4a 100644 --- a/archinstall/lib/utils/util.py +++ b/archinstall/lib/utils/util.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Any, TYPE_CHECKING, Optional -from ..output import log +from ..output import info if TYPE_CHECKING: _: Any @@ -16,7 +16,7 @@ def prompt_dir(text: str, header: Optional[str] = None) -> Path: dest_path = Path(path) if dest_path.exists() and dest_path.is_dir(): return dest_path - log(_('Not a valid directory: {}').format(dest_path), fg='red') + info(_('Not a valid directory: {}').format(dest_path)) def is_subpath(first: Path, second: Path): diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 9906e0a9..37cc1cad 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -1,9 +1,10 @@ -import logging import os from pathlib import Path from typing import Any, TYPE_CHECKING import archinstall +from archinstall import info, debug +from archinstall import SysInfo from archinstall.lib import disk from archinstall.lib.global_menu import GlobalMenu from archinstall.default_profiles.applications.pipewire import PipewireProfile @@ -13,7 +14,7 @@ from archinstall.lib.menu import Menu from archinstall.lib.mirrors import use_mirrors from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.network_configuration import NetworkConfigurationHandler -from archinstall.lib.output import log +from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler if TYPE_CHECKING: @@ -24,20 +25,6 @@ if archinstall.arguments.get('help'): print("See `man archinstall` for help.") exit(0) -if os.getuid() != 0: - print(_("Archinstall requires root privileges to run. See --help for more.")) - exit(1) - -# Log various information about hardware before starting the installation. This might assist in troubleshooting -archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) -archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) -archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) -archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) -archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) - -# For support reasons, we'll log the disk layout pre installation to match against post-installation layout -archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) - def ask_user_questions(): """ @@ -121,7 +108,7 @@ def perform_installation(mountpoint: Path): Only requirement is that the block devices are formatted and setup prior to entering this function. """ - log('Starting installation', level=logging.INFO) + info('Starting installation') disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] # Retrieve list of additional repositories and set boolean values appropriately @@ -167,7 +154,7 @@ def perform_installation(mountpoint: Path): if archinstall.arguments.get('swap'): installation.setup_swap('zram') - if archinstall.arguments.get("bootloader") == Bootloader.Grub and archinstall.has_uefi(): + if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages("grub") installation.add_bootloader(archinstall.arguments["bootloader"]) @@ -190,13 +177,13 @@ def perform_installation(mountpoint: Path): installation.create_users(users) if audio := archinstall.arguments.get('audio', None): - log(f'Installing audio server: {audio}', level=logging.INFO) + info(f'Installing audio server: {audio}') if audio == 'pipewire': PipewireProfile().install(installation) elif audio == 'pulseaudio': installation.add_additional_packages("pulseaudio") else: - installation.log("No audio server will be installed.", level=logging.INFO) + info("No audio server will be installed") if profile_config := archinstall.arguments.get('profile_config', None): profile_handler.install_profile_config(installation, profile_config) @@ -231,7 +218,7 @@ def perform_installation(mountpoint: Path): installation.genfstab() - installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") + info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation") if not archinstall.arguments.get('silent'): prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) @@ -242,12 +229,12 @@ def perform_installation(mountpoint: Path): except: pass - archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + debug(f"Disk states after installing: {disk.disk_layouts()}") -if archinstall.arguments.get('skip-mirror-check', False) is False and archinstall.check_mirror_reachable() is False: +if archinstall.arguments.get('skip-mirror-check', False) is False and check_mirror_reachable() is False: log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + info(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.") exit(1) if not archinstall.arguments.get('silent'): diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py index 0cdbdcef..704759fc 100644 --- a/archinstall/scripts/minimal.py +++ b/archinstall/scripts/minimal.py @@ -2,23 +2,25 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, List import archinstall -from archinstall import ConfigurationOutput, Installer, ProfileConfiguration, profile_handler +from archinstall import info +from archinstall import Installer, ConfigurationOutput from archinstall.default_profiles.minimal import MinimalProfile -from archinstall import disk -from archinstall import models -from archinstall.lib.user_interaction.disk_conf import select_devices, suggest_single_disk_layout +from archinstall.lib.interactions import suggest_single_disk_layout, select_devices +from archinstall.lib.models import Bootloader, User +from archinstall.lib.profile import ProfileConfiguration, profile_handler +from archinstall.lib import disk if TYPE_CHECKING: _: Any -archinstall.log("Minimal only supports:") -archinstall.log(" * Being installed to a single disk") +info("Minimal only supports:") +info(" * Being installed to a single disk") if archinstall.arguments.get('help', None): - archinstall.log(" - Optional disk encryption via --!encryption-password=") - archinstall.log(" - Optional filesystem type via --filesystem=") - archinstall.log(" - Optional systemd network via --network") + info(" - Optional disk encryption via --!encryption-password=") + info(" - Optional filesystem type via --filesystem=") + info(" - Optional systemd network via --network") def perform_installation(mountpoint: Path): @@ -35,7 +37,7 @@ def perform_installation(mountpoint: Path): # some other minor details as specified by this profile and user. if installation.minimal_installation(): installation.set_hostname('minimal-arch') - installation.add_bootloader(models.Bootloader.Systemd) + installation.add_bootloader(Bootloader.Systemd) # Optionally enable networking: if archinstall.arguments.get('network', None): @@ -46,14 +48,14 @@ def perform_installation(mountpoint: Path): profile_config = ProfileConfiguration(MinimalProfile()) profile_handler.install_profile_config(installation, profile_config) - user = models.User('devel', 'devel', False) + user = User('devel', 'devel', False) installation.create_users(user) # Once this is done, we output some useful information to the user # And the installation is complete. - archinstall.log("There are two new accounts in your installation after reboot:") - archinstall.log(" * root (password: airoot)") - archinstall.log(" * devel (password: devel)") + info("There are two new accounts in your installation after reboot:") + info(" * root (password: airoot)") + info(" * devel (password: devel)") def prompt_disk_layout(): diff --git a/archinstall/scripts/only_hd.py b/archinstall/scripts/only_hd.py index a903c5fe..d0ee1e39 100644 --- a/archinstall/scripts/only_hd.py +++ b/archinstall/scripts/only_hd.py @@ -1,22 +1,18 @@ -import logging import os from pathlib import Path import archinstall -from archinstall import Installer +from archinstall import info, debug +from archinstall.lib.installer import Installer from archinstall.lib.configuration import ConfigurationOutput -from archinstall import disk +from archinstall.lib import disk +from archinstall.lib.networking import check_mirror_reachable if archinstall.arguments.get('help'): print("See `man archinstall` for help.") exit(0) -if os.getuid() != 0: - print("Archinstall requires root privileges to run. See --help for more.") - exit(1) - - def ask_user_questions(): global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments) @@ -59,23 +55,12 @@ def perform_installation(mountpoint: Path): target.parent.mkdir(parents=True) # For support reasons, we'll log the disk layout post installation (crash or no crash) - archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) - - -# Log various information about hardware before starting the installation. This might assist in troubleshooting -archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) -archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) -archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) -archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) -archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) - -# For support reasons, we'll log the disk layout pre installation to match against post-installation layout -archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) + debug(f"Disk states after installing: {disk.disk_layouts()}") -if archinstall.arguments.get('skip-mirror-check', False) is False and archinstall.check_mirror_reachable() is False: +if archinstall.arguments.get('skip-mirror-check', False) is False and check_mirror_reachable() is False: log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + info(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'") exit(1) if not archinstall.arguments.get('silent'): diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 3bf847b1..a49f568d 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -1,18 +1,18 @@ -import logging import os from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Any, Dict import archinstall +from archinstall import SysInfo, info, debug from archinstall.lib.mirrors import use_mirrors -from archinstall import models -from archinstall import disk +from archinstall.lib import models +from archinstall.lib import disk +from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler -from archinstall import menu +from archinstall.lib import menu from archinstall.lib.global_menu import GlobalMenu -from archinstall.lib.output import log -from archinstall import Installer +from archinstall.lib.installer import Installer from archinstall.lib.configuration import ConfigurationOutput from archinstall.default_profiles.applications.pipewire import PipewireProfile @@ -25,11 +25,6 @@ if archinstall.arguments.get('help'): exit(0) -if os.getuid() != 0: - print("Archinstall requires root privileges to run. See --help for more.") - exit(1) - - class ExecutionMode(Enum): Full = 'full' Lineal = 'lineal' @@ -76,7 +71,7 @@ class SetupMenu(GlobalMenu): def exit_callback(self): if self._data_store.get('mode', None): archinstall.arguments['mode'] = self._data_store['mode'] - log(f"Archinstall will execute under {archinstall.arguments['mode']} mode") + info(f"Archinstall will execute under {archinstall.arguments['mode']} mode") class SwissMainMenu(GlobalMenu): @@ -124,7 +119,7 @@ class SwissMainMenu(GlobalMenu): case ExecutionMode.Minimal: pass case _: - archinstall.log(f' Execution mode {self._execution_mode} not supported') + info(f' Execution mode {self._execution_mode} not supported') exit(1) if self._execution_mode != ExecutionMode.Lineal: @@ -219,7 +214,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): if archinstall.arguments.get('swap'): installation.setup_swap('zram') - if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and archinstall.has_uefi(): + if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages("grub") installation.add_bootloader(archinstall.arguments["bootloader"]) @@ -242,13 +237,13 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): installation.create_users(users) if audio := archinstall.arguments.get('audio', None): - log(f'Installing audio server: {audio}', level=logging.INFO) + info(f'Installing audio server: {audio}') if audio == 'pipewire': PipewireProfile().install(installation) elif audio == 'pulseaudio': installation.add_additional_packages("pulseaudio") else: - installation.log("No audio server will be installed.", level=logging.INFO) + info("No audio server will be installed.") if profile_config := archinstall.arguments.get('profile_config', None): profile_handler.install_profile_config(installation, profile_config) @@ -283,9 +278,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): installation.genfstab() - installation.log( - "For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", - fg="yellow") + info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation") if not archinstall.arguments.get('silent'): prompt = str( @@ -297,23 +290,12 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): except: pass - archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) - - -# Log various information about hardware before starting the installation. This might assist in troubleshooting -archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) -archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) -archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) -archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) -archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) - -# For support reasons, we'll log the disk layout pre installation to match against post-installation layout -archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) + debug(f"Disk states after installing: {disk.disk_layouts()}") -if not archinstall.check_mirror_reachable(): +if not check_mirror_reachable(): log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + info(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'") exit(1) param_mode = archinstall.arguments.get('mode', ExecutionMode.Full.value).lower() @@ -321,7 +303,7 @@ param_mode = archinstall.arguments.get('mode', ExecutionMode.Full.value).lower() try: mode = ExecutionMode(param_mode) except KeyError: - log(f'Mode "{param_mode}" is not supported') + info(f'Mode "{param_mode}" is not supported') exit(1) if not archinstall.arguments.get('silent'): diff --git a/archinstall/scripts/unattended.py b/archinstall/scripts/unattended.py index 0a1c5160..5ae4ae3d 100644 --- a/archinstall/scripts/unattended.py +++ b/archinstall/scripts/unattended.py @@ -1,13 +1,14 @@ import time import archinstall -from archinstall.lib.profile.profiles_handler import profile_handler +from archinstall import info +from archinstall import profile -for profile in profile_handler.get_mac_addr_profiles(): +for p in profile.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). - archinstall.log(f'Found a tailored profile for this machine called: "{profile.name}"') + info(f'Found a tailored profile for this machine called: "{p.name}"') print('Starting install in:') for i in range(10, 0, -1): @@ -15,4 +16,4 @@ for profile in profile_handler.get_mac_addr_profiles(): time.sleep(1) install_session = archinstall.storage['installation_session'] - profile.install(install_session) + p.install(install_session) diff --git a/examples/full_automated_installation.py b/examples/full_automated_installation.py index a169dd50..dcef731a 100644 --- a/examples/full_automated_installation.py +++ b/examples/full_automated_installation.py @@ -1,9 +1,10 @@ from pathlib import Path -from archinstall import Installer, ProfileConfiguration, profile_handler +from archinstall import Installer +from archinstall import profile from archinstall.default_profiles.minimal import MinimalProfile from archinstall import disk -from archinstall.lib.models import User +from archinstall import models # we're creating a new ext4 filesystem installation fs_type = disk.FilesystemType('ext4') @@ -88,8 +89,8 @@ with Installer( # Optionally, install a profile of choice. # In this case, we install a minimal profile that is empty -profile_config = ProfileConfiguration(MinimalProfile()) -profile_handler.install_profile_config(installation, profile_config) +profile_config = profile.ProfileConfiguration(MinimalProfile()) +profile.profile_handler.install_profile_config(installation, profile_config) -user = User('archinstall', 'password', True) +user = models.User('archinstall', 'password', True) installation.create_users(user) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index a27ec0f9..72595048 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -1,13 +1,16 @@ -import logging from pathlib import Path from typing import TYPE_CHECKING, Any import archinstall -from archinstall import log, Installer, use_mirrors, profile_handler +from archinstall import Installer +from archinstall import profile +from archinstall import SysInfo +from archinstall import mirrors from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall import disk from archinstall import menu -from archinstall.lib.models import Bootloader, NetworkConfigurationHandler +from archinstall import models +from archinstall import info, debug if TYPE_CHECKING: _: Any @@ -84,7 +87,7 @@ def perform_installation(mountpoint: Path): Only requirement is that the block devices are formatted and setup prior to entering this function. """ - log('Starting installation', level=logging.INFO) + info('Starting installation') disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] # Retrieve list of additional repositories and set boolean values appropriately @@ -114,7 +117,7 @@ def perform_installation(mountpoint: Path): # Set mirrors used by pacstrap (outside of installation) if archinstall.arguments.get('mirror-region', None): - use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + mirrors.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium installation.minimal_installation( testing=enable_testing, @@ -130,7 +133,7 @@ def perform_installation(mountpoint: Path): if archinstall.arguments.get('swap'): installation.setup_swap('zram') - if archinstall.arguments.get("bootloader") == Bootloader.Grub and archinstall.has_uefi(): + if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages("grub") installation.add_bootloader(archinstall.arguments["bootloader"]) @@ -140,7 +143,7 @@ def perform_installation(mountpoint: Path): network_config = archinstall.arguments.get('nic', None) if network_config: - handler = NetworkConfigurationHandler(network_config) + handler = models.NetworkConfigurationHandler(network_config) handler.config_installer( installation, archinstall.arguments.get('profile_config', None) @@ -153,16 +156,16 @@ def perform_installation(mountpoint: Path): installation.create_users(users) if audio := archinstall.arguments.get('audio', None): - log(f'Installing audio server: {audio}', level=logging.INFO) + info(f'Installing audio server: {audio}') if audio == 'pipewire': PipewireProfile().install(installation) elif audio == 'pulseaudio': installation.add_additional_packages("pulseaudio") else: - installation.log("No audio server will be installed.", level=logging.INFO) + info("No audio server will be installed.") if profile_config := archinstall.arguments.get('profile_config', None): - profile_handler.install_profile_config(installation, profile_config) + profile.profile_handler.install_profile_config(installation, profile_config) if timezone := archinstall.arguments.get('timezone', None): installation.set_timezone(timezone) @@ -194,7 +197,7 @@ def perform_installation(mountpoint: Path): installation.genfstab() - installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") + info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation") if not archinstall.arguments.get('silent'): prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) @@ -202,10 +205,10 @@ def perform_installation(mountpoint: Path): if choice.value == menu.Menu.yes(): try: installation.drop_to_shell() - except: + except Exception: pass - archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + debug(f"Disk states after installing: {disk.disk_layouts()}") ask_user_questions() diff --git a/examples/mac_address_installation.py b/examples/mac_address_installation.py index 0a1c5160..74a123c7 100644 --- a/examples/mac_address_installation.py +++ b/examples/mac_address_installation.py @@ -1,13 +1,13 @@ import time import archinstall -from archinstall.lib.profile.profiles_handler import profile_handler +from archinstall import profile, info -for profile in profile_handler.get_mac_addr_profiles(): +for _profile in profile.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). - archinstall.log(f'Found a tailored profile for this machine called: "{profile.name}"') + info(f'Found a tailored profile for this machine called: "{_profile.name}"') print('Starting install in:') for i in range(10, 0, -1): @@ -15,4 +15,4 @@ for profile in profile_handler.get_mac_addr_profiles(): time.sleep(1) install_session = archinstall.storage['installation_session'] - profile.install(install_session) + _profile.install(install_session) diff --git a/examples/minimal_installation.py b/examples/minimal_installation.py index 8bd6fd55..e31adea4 100644 --- a/examples/minimal_installation.py +++ b/examples/minimal_installation.py @@ -2,11 +2,12 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, List import archinstall -from archinstall.lib import disk -from archinstall import Installer, ProfileConfiguration, profile_handler +from archinstall import disk +from archinstall import Installer +from archinstall import profile +from archinstall import models +from archinstall import interactions from archinstall.default_profiles.minimal import MinimalProfile -from archinstall.lib.models import Bootloader, User -from archinstall.lib.user_interaction.disk_conf import select_devices, suggest_single_disk_layout if TYPE_CHECKING: _: Any @@ -26,7 +27,7 @@ def perform_installation(mountpoint: Path): # some other minor details as specified by this profile and user. if installation.minimal_installation(): installation.set_hostname('minimal-arch') - installation.add_bootloader(Bootloader.Systemd) + installation.add_bootloader(models.Bootloader.Systemd) # Optionally enable networking: if archinstall.arguments.get('network', None): @@ -34,10 +35,10 @@ def perform_installation(mountpoint: Path): installation.add_additional_packages(['nano', 'wget', 'git']) - profile_config = ProfileConfiguration(MinimalProfile()) - profile_handler.install_profile_config(installation, profile_config) + profile_config = profile.ProfileConfiguration(MinimalProfile()) + profile.profile_handler.install_profile_config(installation, profile_config) - user = User('devel', 'devel', False) + user = models.User('devel', 'devel', False) installation.create_users(user) @@ -46,8 +47,8 @@ def prompt_disk_layout(): 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) + devices = interactions.select_devices() + modifications = interactions.suggest_single_disk_layout(devices[0], filesystem_type=fs_type) archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration( config_type=disk.DiskLayoutType.Default, diff --git a/examples/only_hd_installation.py b/examples/only_hd_installation.py index 2fc74bf0..075bde20 100644 --- a/examples/only_hd_installation.py +++ b/examples/only_hd_installation.py @@ -1,9 +1,7 @@ -import logging from pathlib import Path import archinstall -from archinstall import Installer -from archinstall.lib import disk +from archinstall import Installer, disk, debug def ask_user_questions(): @@ -48,7 +46,7 @@ def perform_installation(mountpoint: Path): target.parent.mkdir(parents=True) # For support reasons, we'll log the disk layout post installation (crash or no crash) - archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + debug(f"Disk states after installing: {disk.disk_layouts()}") ask_user_questions() diff --git a/pyproject.toml b/pyproject.toml index f837ebdf..8b6ae4c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ Source = "https://github.com/archlinux/archinstall" [project.optional-dependencies] dev = [ "mypy==1.1.1", + "pre-commit==3.3.1", ] doc = ["sphinx"] -- cgit v1.2.3-54-g00ecf From 128db1cdf6698aba09915bb7c044404f713755ef Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Fri, 12 May 2023 02:24:14 -0400 Subject: Install the package `sof-firmware` if required (#1811) --- archinstall/lib/hardware.py | 21 ++++++++++++++++++++- archinstall/scripts/guided.py | 3 +++ examples/interactive_installation.py | 3 +++ 3 files changed, 26 insertions(+), 1 deletion(-) (limited to 'archinstall/scripts') diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index b95301f9..8d0fb74f 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -1,7 +1,7 @@ import os from functools import cached_property from pathlib import Path -from typing import Optional, Dict +from typing import Optional, Dict, List from .general import SysCommand from .networking import list_interfaces, enrich_iface_types @@ -169,3 +169,22 @@ class SysInfo: debug(f"System is not running in a VM: {err}") return False + + @staticmethod + def _loaded_modules() -> List[str]: + """ + Returns loaded kernel modules + """ + modules_path = Path('/proc/modules') + modules: List[str] = [] + + with modules_path.open() as file: + for line in file: + module = line.split(maxsplit=1)[0] + modules.append(module) + + return modules + + @staticmethod + def requires_sof() -> bool: + return 'snd_sof' in SysInfo._loaded_modules() diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 37cc1cad..9cb4ec2a 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -182,6 +182,9 @@ def perform_installation(mountpoint: Path): PipewireProfile().install(installation) elif audio == 'pulseaudio': installation.add_additional_packages("pulseaudio") + + if SysInfo.requires_sof(): + installation.add_additional_packages('sof-firmware') else: info("No audio server will be installed") diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 72595048..5738a9cc 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -161,6 +161,9 @@ def perform_installation(mountpoint: Path): PipewireProfile().install(installation) elif audio == 'pulseaudio': installation.add_additional_packages("pulseaudio") + + if SysInfo.requires_sof(): + installation.add_additional_packages('sof-firmware') else: info("No audio server will be installed.") -- cgit v1.2.3-54-g00ecf From 06f35fd289a06c5d142d65466820ff132e838365 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Fri, 12 May 2023 08:01:24 -0400 Subject: Install the package `alsa-firmware` if required (#1812) --- archinstall/lib/hardware.py | 34 +++++++++++++++++++--------------- archinstall/scripts/guided.py | 5 ++++- examples/interactive_installation.py | 5 ++++- 3 files changed, 27 insertions(+), 17 deletions(-) (limited to 'archinstall/scripts') diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 8d0fb74f..220d3d37 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -86,6 +86,21 @@ class _SysInfo: def mem_info_by_key(self, key: str) -> int: return self.mem_info[key] + @cached_property + def loaded_modules(self) -> List[str]: + """ + Returns loaded kernel modules + """ + modules_path = Path('/proc/modules') + modules: List[str] = [] + + with modules_path.open() as file: + for line in file: + module = line.split(maxsplit=1)[0] + modules.append(module) + + return modules + _sys_info = _SysInfo() @@ -171,20 +186,9 @@ class SysInfo: return False @staticmethod - def _loaded_modules() -> List[str]: - """ - Returns loaded kernel modules - """ - modules_path = Path('/proc/modules') - modules: List[str] = [] - - with modules_path.open() as file: - for line in file: - module = line.split(maxsplit=1)[0] - modules.append(module) - - return modules + def requires_sof_fw() -> bool: + return 'snd_sof' in _sys_info.loaded_modules @staticmethod - def requires_sof() -> bool: - return 'snd_sof' in SysInfo._loaded_modules() + def requires_alsa_fw() -> bool: + return 'snd_emu10k1' in _sys_info.loaded_modules diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 9cb4ec2a..1e19c9a3 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -183,8 +183,11 @@ def perform_installation(mountpoint: Path): elif audio == 'pulseaudio': installation.add_additional_packages("pulseaudio") - if SysInfo.requires_sof(): + if SysInfo.requires_sof_fw(): installation.add_additional_packages('sof-firmware') + + if SysInfo.requires_alsa_fw(): + installation.add_additional_packages('alsa-firmware') else: info("No audio server will be installed") diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 5738a9cc..487db4dd 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -162,8 +162,11 @@ def perform_installation(mountpoint: Path): elif audio == 'pulseaudio': installation.add_additional_packages("pulseaudio") - if SysInfo.requires_sof(): + if SysInfo.requires_sof_fw(): installation.add_additional_packages('sof-firmware') + + if SysInfo.requires_alsa_fw(): + installation.add_additional_packages('alsa-firmware') else: info("No audio server will be installed.") -- cgit v1.2.3-54-g00ecf From 8a292a163ea2e643a8ac5d4cfada8a27076de630 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 15 May 2023 17:16:18 +1000 Subject: Add custom mirror support (#1816) Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 8 +- archinstall/lib/global_menu.py | 34 ++- archinstall/lib/installer.py | 15 +- archinstall/lib/interactions/__init__.py | 2 +- archinstall/lib/interactions/general_conf.py | 37 +-- archinstall/lib/mirrors.py | 442 ++++++++++++++++++--------- archinstall/scripts/guided.py | 16 +- archinstall/scripts/swiss.py | 19 +- examples/interactive_installation.py | 14 +- 9 files changed, 358 insertions(+), 229 deletions(-) (limited to 'archinstall/scripts') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 992bd9fa..e6fcb267 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -225,12 +225,8 @@ def load_config(): if profile_config := arguments.get('profile_config', None): arguments['profile_config'] = profile.ProfileConfiguration.parse_arg(profile_config) - if arguments.get('mirror-region', None) is not None: - if type(arguments.get('mirror-region', None)) is dict: - arguments['mirror-region'] = arguments.get('mirror-region', None) - else: - selected_region = arguments.get('mirror-region', None) - arguments['mirror-region'] = {selected_region: mirrors.list_mirrors()[selected_region]} + if mirror_config := arguments.get('mirror_config', None): + arguments['mirror_config'] = mirrors.MirrorConfiguration.parse_args(mirror_config) if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 13595132..fc58a653 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -5,6 +5,7 @@ from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING from . import disk from .general import secret from .menu import Selector, AbstractMenu +from .mirrors import MirrorConfiguration, MirrorMenu from .models import NetworkConfiguration from .models.bootloader import Bootloader from .models.users import User @@ -26,7 +27,6 @@ from .interactions import select_kernel from .interactions import select_language from .interactions import select_locale_enc from .interactions import select_locale_lang -from .interactions import select_mirror_regions from .interactions import ask_ntp from .interactions.disk_conf import select_disk_config @@ -51,12 +51,13 @@ class GlobalMenu(AbstractMenu): _('Keyboard layout'), lambda preset: select_language(preset), default='us') - self._menu_options['mirror-region'] = \ + self._menu_options['mirror_config'] = \ Selector( - _('Mirror region'), - lambda preset: select_mirror_regions(preset), - display_func=lambda x: list(x.keys()) if x else '[]', - default={}) + _('Mirrors'), + lambda preset: self._mirror_configuration(preset), + display_func=lambda x: str(_('Defined')) if x else '', + preview_func=self._prev_mirror_config + ) self._menu_options['sys-language'] = \ Selector( _('Locale language'), @@ -354,3 +355,24 @@ class GlobalMenu(AbstractMenu): def _create_user_account(self, defined_users: List[User]) -> List[User]: users = ask_for_additional_users(defined_users=defined_users) return users + + def _mirror_configuration(self, preset: Optional[MirrorConfiguration] = None) -> Optional[MirrorConfiguration]: + data_store: Dict[str, Any] = {} + mirror_configuration = MirrorMenu(data_store, preset=preset).run() + return mirror_configuration + + def _prev_mirror_config(self) -> Optional[str]: + selector = self._menu_options['mirror_config'] + + if selector.has_selection(): + mirror_config: MirrorConfiguration = selector.current_selection # type: ignore + output = '' + if mirror_config.regions: + output += '{}: {}\n\n'.format(str(_('Mirror regions')), mirror_config.regions) + if mirror_config.custom_mirrors: + table = FormattedOutput.as_table(mirror_config.custom_mirrors) + output += '{}\n{}'.format(str(_('Custom mirrors')), table) + + return output.strip() + + return None diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 3c427ab2..30442774 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -6,7 +6,7 @@ import shutil import subprocess import time from pathlib import Path -from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable, Iterable +from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable from . import disk from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError @@ -14,7 +14,7 @@ from .general import SysCommand from .hardware import SysInfo from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .luks import Luks2 -from .mirrors import use_mirrors +from .mirrors import use_mirrors, MirrorConfiguration, add_custom_mirrors from .models.bootloader import Bootloader from .models.network_configuration import NetworkConfiguration from .models.users import User @@ -383,14 +383,17 @@ class Installer: raise RequirementError("Pacstrap failed. See /var/log/archinstall/install.log or above message for error details.") - def set_mirrors(self, mirrors: Dict[str, Iterable[str]]): + def set_mirrors(self, mirror_config: MirrorConfiguration): for plugin in plugins.values(): if hasattr(plugin, 'on_mirrors'): - if result := plugin.on_mirrors(mirrors): - mirrors = result + if result := plugin.on_mirrors(mirror_config): + mirror_config = result destination = f'{self.target}/etc/pacman.d/mirrorlist' - use_mirrors(mirrors, destination=destination) + if mirror_config.mirror_regions: + use_mirrors(mirror_config.mirror_regions, destination) + if mirror_config.custom_mirrors: + add_custom_mirrors(mirror_config.custom_mirrors) def genfstab(self, flags :str = '-pU'): info(f"Updating {self.target}/etc/fstab") diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index b5691a10..158750cc 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -11,7 +11,7 @@ from .disk_conf import ( from .general_conf import ( ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_language, - select_mirror_regions, select_archinstall_language, ask_additional_packages_to_install, + select_archinstall_language, ask_additional_packages_to_install, add_number_of_parrallel_downloads, select_additional_repositories ) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 5fcfa633..0338c61e 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -1,11 +1,10 @@ from __future__ import annotations import pathlib -from typing import List, Any, Optional, Dict, TYPE_CHECKING +from typing import List, Any, Optional, TYPE_CHECKING from ..locale import list_keyboard_languages, list_timezones from ..menu import MenuSelectionType, Menu, TextInput -from ..mirrors import list_mirrors from ..output import warn from ..packages.packages import validate_package_list from ..storage import storage @@ -96,40 +95,6 @@ def select_language(preset: Optional[str] = None) -> Optional[str]: return None -def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: - """ - Asks the user to select a mirror or region - Usually this is combined with :ref:`archinstall.list_mirrors`. - - :return: The dictionary information about a mirror/region. - :rtype: dict - """ - if preset_values is None: - preselected = None - else: - preselected = list(preset_values.keys()) - - mirrors = list_mirrors() - - choice = Menu( - _('Select one of the regions to download packages from'), - list(mirrors.keys()), - preset_values=preselected, - multi=True, - allow_reset=True - ).run() - - match choice.type_: - case MenuSelectionType.Reset: - return {} - case MenuSelectionType.Skip: - return preset_values - case MenuSelectionType.Selection: - return {selected: mirrors[selected] for selected in choice.multi_value} - - return {} - - def select_archinstall_language(languages: List[Language], preset: Language) -> Language: # these are the displayed language names which can either be # the english name of a language or, if present, the diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 62a0b081..521a8e5b 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -1,99 +1,279 @@ import pathlib -import urllib.error -import urllib.request -from typing import Union, Iterable, Dict, Any, List -from dataclasses import dataclass - -from .general import SysCommand -from .output import info, warn -from .exceptions import SysCallError +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, Any, List, Optional, TYPE_CHECKING + +from .menu import AbstractSubMenu, Selector, MenuSelectionType, Menu, ListManager, TextInput +from .networking import fetch_data_from_url +from .output import info, warn, FormattedOutput from .storage import storage +if TYPE_CHECKING: + _: Any -@dataclass -class CustomMirror: - url: str - signcheck: str - signoptions: str - name: str +class SignCheck(Enum): + Never = 'Never' + Optional = 'Optional' + Required = 'Required' -def sort_mirrorlist(raw_data :bytes, sort_order: List[str] = ['https', 'http']) -> bytes: - """ - This function can sort /etc/pacman.d/mirrorlist according to the - mirror's URL prefix. By default places HTTPS before HTTP but it also - preserves the country/rank-order. - This assumes /etc/pacman.d/mirrorlist looks like the following: +class SignOption(Enum): + TrustedOnly = 'TrustedOnly' + TrustAll = 'TrustAll' - ## Comment - Server = url - or +@dataclass +class CustomMirror: + name: str + url: str + sign_check: SignCheck + sign_option: SignOption + + def as_json(self) -> Dict[str, str]: + return { + 'Name': self.name, + 'Url': self.url, + 'Sign check': self.sign_check.value, + 'Sign options': self.sign_option.value + } + + def json(self) -> Dict[str, str]: + return { + 'name': self.name, + 'url': self.url, + 'sign_check': self.sign_check.value, + 'sign_option': self.sign_option.value + } + + @classmethod + def parse_args(cls, args: List[Dict[str, str]]) -> List['CustomMirror']: + configs = [] + for arg in args: + configs.append( + CustomMirror( + arg['name'], + arg['url'], + SignCheck(arg['sign_check']), + SignOption(arg['sign_option']) + ) + ) + + return configs - ## Comment - #Server = url - But the Comments need to start with double-hashmarks to be distringuished - from server url definitions (commented or uncommented). - """ - comments_and_whitespaces = b"" - sort_order += ['Unknown'] - categories: Dict[str, List] = {key: [] for key in sort_order} - - for line in raw_data.split(b"\n"): - if line[0:2] in (b'##', b''): - comments_and_whitespaces += line + b'\n' - elif line[:6].lower() == b'server' or line[:7].lower() == b'#server': - opening, url = line.split(b'=', 1) - opening, url = opening.strip(), url.strip() - if (category := url.split(b'://',1)[0].decode('UTF-8')) in categories: - categories[category].append(comments_and_whitespaces) - categories[category].append(opening + b' = ' + url + b'\n') - else: - categories["Unknown"].append(comments_and_whitespaces) - categories["Unknown"].append(opening + b' = ' + url + b'\n') - - comments_and_whitespaces = b"" - - new_raw_data = b'' - for category in sort_order + ["Unknown"]: - for line in categories[category]: - new_raw_data += line - - return new_raw_data - - -def filter_mirrors_by_region(regions :str, - destination :str = '/etc/pacman.d/mirrorlist', - sort_order :List[str] = ["https", "http"], - *args :str, - **kwargs :str -) -> Union[bool, bytes]: +@dataclass +class MirrorConfiguration: + mirror_regions: Dict[str, List[str]] = field(default_factory=dict) + custom_mirrors: List[CustomMirror] = field(default_factory=list) + + @property + def regions(self) -> str: + return ', '.join(self.mirror_regions.keys()) + + def json(self) -> Dict[str, Any]: + return { + 'mirror_regions': self.mirror_regions, + 'custom_mirrors': [c.json() for c in self.custom_mirrors] + } + + @classmethod + def parse_args(cls, args: Dict[str, Any]) -> 'MirrorConfiguration': + config = MirrorConfiguration() + + if 'mirror_regions' in args: + config.mirror_regions = args['mirror_regions'] + + if 'custom_mirrors' in args: + config.custom_mirrors = CustomMirror.parse_args(args['custom_mirrors']) + + return config + + +class CustomMirrorList(ListManager): + def __init__(self, prompt: str, custom_mirrors: List[CustomMirror]): + self._actions = [ + str(_('Add a custom mirror')), + str(_('Change custom mirror')), + str(_('Delete custom mirror')) + ] + super().__init__(prompt, custom_mirrors, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[CustomMirror]) -> Dict[str, Any]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[CustomMirror]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, user in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = user + + return display_data + + def selected_action_display(self, mirror: CustomMirror) -> str: + return mirror.name + + def handle_action( + self, + action: str, + entry: Optional[CustomMirror], + data: List[CustomMirror] + ) -> List[CustomMirror]: + if action == self._actions[0]: # add + new_mirror = self._add_custom_mirror() + if new_mirror is not None: + data = [d for d in data if d.name != new_mirror.name] + data += [new_mirror] + elif action == self._actions[1] and entry: # modify mirror + new_mirror = self._add_custom_mirror(entry) + if new_mirror is not None: + data = [d for d in data if d.name != entry.name] + data += [new_mirror] + elif action == self._actions[2] and entry: # delete + data = [d for d in data if d != entry] + + return data + + def _add_custom_mirror(self, mirror: Optional[CustomMirror] = None) -> Optional[CustomMirror]: + prompt = '\n\n' + str(_('Enter name (leave blank to skip): ')) + existing_name = mirror.name if mirror else '' + + while True: + name = TextInput(prompt, existing_name).run() + if not name: + return mirror + break + + prompt = '\n' + str(_('Enter url (leave blank to skip): ')) + existing_url = mirror.url if mirror else '' + + while True: + url = TextInput(prompt, existing_url).run() + if not url: + return mirror + break + + sign_check_choice = Menu( + str(_('Select signature check option')), + [s.value for s in SignCheck], + skip=False, + clear_screen=False, + preset_values=mirror.sign_check.value if mirror else None + ).run() + + sign_option_choice = Menu( + str(_('Select signature option')), + [s.value for s in SignOption], + skip=False, + clear_screen=False, + preset_values=mirror.sign_option.value if mirror else None + ).run() + + return CustomMirror( + name, + url, + SignCheck(sign_check_choice.single_value), + SignOption(sign_option_choice.single_value) + ) + + +class MirrorMenu(AbstractSubMenu): + def __init__( + self, + data_store: Dict[str, Any], + preset: Optional[MirrorConfiguration] = None + ): + if preset: + self._preset = preset + else: + self._preset = MirrorConfiguration() + + super().__init__(data_store=data_store) + + def setup_selection_menu_options(self): + self._menu_options['mirror_regions'] = \ + Selector( + _('Mirror region'), + lambda preset: select_mirror_regions(preset), + display_func=lambda x: ', '.join(x.keys()) if x else '', + default=self._preset.mirror_regions, + enabled=True) + self._menu_options['custom_mirrors'] = \ + Selector( + _('Custom mirrors'), + lambda preset: select_custom_mirror(preset=preset), + display_func=lambda x: str(_('Defined')) if x else '', + preview_func=self._prev_custom_mirror, + default=self._preset.custom_mirrors, + enabled=True + ) + + def _prev_custom_mirror(self) -> Optional[str]: + selector = self._menu_options['custom_mirrors'] + + if selector.has_selection(): + custom_mirrors: List[CustomMirror] = selector.current_selection # type: ignore + output = FormattedOutput.as_table(custom_mirrors) + return output.strip() + + return None + + def run(self, allow_reset: bool = True) -> Optional[MirrorConfiguration]: + super().run(allow_reset=allow_reset) + + if self._data_store.get('mirror_regions', None) or self._data_store.get('custom_mirrors', None): + return MirrorConfiguration( + mirror_regions=self._data_store['mirror_regions'], + custom_mirrors=self._data_store['custom_mirrors'], + ) + + return None + + +def select_mirror_regions(preset_values: Dict[str, List[str]] = {}) -> Dict[str, List[str]]: """ - This function will change the active mirrors on the live medium by - filtering which regions are active based on `regions`. + Asks the user to select a mirror or region + Usually this is combined with :ref:`archinstall.list_mirrors`. - :param regions: A series of country codes separated by `,`. For instance `SE,US` for sweden and United States. - :type regions: str + :return: The dictionary information about a mirror/region. + :rtype: dict """ - region_list = [f'country={region}' for region in regions.split(',')] - response = urllib.request.urlopen(urllib.request.Request(f"https://archlinux.org/mirrorlist/?{'&'.join(region_list)}&protocol=https&protocol=http&ip_version=4&ip_version=6&use_mirror_status=on'", headers={'User-Agent': 'ArchInstall'})) - new_list = response.read().replace(b"#Server", b"Server") + if preset_values is None: + preselected = None + else: + preselected = list(preset_values.keys()) - if sort_order: - new_list = sort_mirrorlist(new_list, sort_order=sort_order) + mirrors = list_mirrors() - if destination: - with open(destination, "wb") as mirrorlist: - mirrorlist.write(new_list) + choice = Menu( + _('Select one of the regions to download packages from'), + list(mirrors.keys()), + preset_values=preselected, + multi=True, + allow_reset=True + ).run() - return True - else: - return new_list.decode('UTF-8') + match choice.type_: + case MenuSelectionType.Reset: + return {} + case MenuSelectionType.Skip: + return preset_values + case MenuSelectionType.Selection: + return {selected: mirrors[selected] for selected in choice.multi_value} + + return {} -def add_custom_mirrors(mirrors: List[CustomMirror]) -> bool: +def select_custom_mirror(prompt: str = '', preset: List[CustomMirror] = []): + custom_mirrors = CustomMirrorList(prompt, preset).run() + return custom_mirrors + + +def add_custom_mirrors(mirrors: List[CustomMirror]): """ This will append custom mirror definitions in pacman.conf @@ -102,99 +282,57 @@ def add_custom_mirrors(mirrors: List[CustomMirror]) -> bool: """ with open('/etc/pacman.conf', 'a') as pacman: for mirror in mirrors: - pacman.write(f"[{mirror.name}]\n") - pacman.write(f"SigLevel = {mirror.signcheck} {mirror.signoptions}\n") + pacman.write(f"\n\n[{mirror.name}]\n") + pacman.write(f"SigLevel = {mirror.sign_check.value} {mirror.sign_option.value}\n") pacman.write(f"Server = {mirror.url}\n") - return True - - -def insert_mirrors(mirrors :Dict[str, Any], *args :str, **kwargs :str) -> bool: - """ - This function will insert a given mirror-list at the top of `/etc/pacman.d/mirrorlist`. - It will not flush any other mirrors, just insert new ones. - - :param mirrors: A dictionary of `{'url' : 'country', 'url2' : 'country'}` - :type mirrors: dict - """ - original_mirrorlist = '' - with open('/etc/pacman.d/mirrorlist', 'r') as original: - original_mirrorlist = original.read() - - with open('/etc/pacman.d/mirrorlist', 'w') as new_mirrorlist: - for mirror, country in mirrors.items(): - new_mirrorlist.write(f'## {country}\n') - new_mirrorlist.write(f'Server = {mirror}\n') - new_mirrorlist.write('\n') - new_mirrorlist.write(original_mirrorlist) - - return True - def use_mirrors( - regions: Dict[str, Iterable[str]], + regions: Dict[str, List[str]], destination: str = '/etc/pacman.d/mirrorlist' ): - info(f'A new package mirror-list has been created: {destination}') - with open(destination, 'w') as mirrorlist: + with open(destination, 'w') as fp: for region, mirrors in regions.items(): for mirror in mirrors: - mirrorlist.write(f'## {region}\n') - mirrorlist.write(f'Server = {mirror}\n') + fp.write(f'## {region}\n') + fp.write(f'Server = {mirror}\n') + + info(f'A new package mirror-list has been created: {destination}') + +def _parse_mirror_list(mirrorlist: str) -> Dict[str, List[str]]: + file_content = mirrorlist.split('\n') + file_content = list(filter(lambda x: x, file_content)) # filter out empty lines + first_srv_idx = [idx for idx, line in enumerate(file_content) if 'server' in line.lower()][0] + mirrors = file_content[first_srv_idx - 1:] -def re_rank_mirrors( - top: int = 10, - src: str = '/etc/pacman.d/mirrorlist', - dst: str = '/etc/pacman.d/mirrorlist', -) -> bool: - try: - cmd = SysCommand(f"/usr/bin/rankmirrors -n {top} {src}") - except SysCallError: - return False - with open(dst, 'w') as f: - f.write(str(cmd)) - return True + mirror_list: Dict[str, List[str]] = {} + for idx in range(0, len(mirrors), 2): + region = mirrors[idx].removeprefix('## ') + url = mirrors[idx + 1].removeprefix('#').removeprefix('Server = ') + mirror_list.setdefault(region, []).append(url) -def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]: - regions: Dict[str, Dict[str, Any]] = {} + return mirror_list + + +def list_mirrors() -> Dict[str, List[str]]: + regions: Dict[str, List[str]] = {} if storage['arguments']['offline']: - with pathlib.Path('/etc/pacman.d/mirrorlist').open('rb') as fh: - mirrorlist = fh.read() + with pathlib.Path('/etc/pacman.d/mirrorlist').open('r') as fp: + mirrorlist = fp.read() else: url = "https://archlinux.org/mirrorlist/?protocol=https&protocol=http&ip_version=4&ip_version=6&use_mirror_status=on" - try: - response = urllib.request.urlopen(url) - except urllib.error.URLError as err: + mirrorlist = fetch_data_from_url(url) + except ValueError as err: warn(f'Could not fetch an active mirror-list: {err}') return regions - mirrorlist = response.read() - - if sort_order: - mirrorlist = sort_mirrorlist(mirrorlist, sort_order=sort_order) - - region = 'Unknown region' - for line in mirrorlist.split(b'\n'): - if len(line.strip()) == 0: - continue - - clean_line = line.decode('UTF-8').strip('\n').strip('\r') - - if clean_line[:3] == '## ': - region = clean_line[3:] - elif clean_line[:10] == '#Server = ': - regions.setdefault(region, {}) - - url = clean_line.lstrip('#Server = ') - regions[region][url] = True - elif clean_line.startswith('Server = '): - regions.setdefault(region, {}) - - url = clean_line.lstrip('Server = ') - regions[region][url] = True + regions = _parse_mirror_list(mirrorlist) + sorted_regions = {} + for region, urls in regions.items(): + sorted_regions[region] = sorted(urls, reverse=True) - return regions + return sorted_regions diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 1e19c9a3..1aecc1cd 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -11,7 +11,7 @@ from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.installer import Installer from archinstall.lib.menu import Menu -from archinstall.lib.mirrors import use_mirrors +from archinstall.lib.mirrors import use_mirrors, add_custom_mirrors from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.network_configuration import NetworkConfigurationHandler from archinstall.lib.networking import check_mirror_reachable @@ -45,7 +45,7 @@ def ask_user_questions(): global_menu.enable('keyboard-layout') # Set which region to download packages from during the installation - global_menu.enable('mirror-region') + global_menu.enable('mirror_config') global_menu.enable('sys-language') @@ -137,8 +137,11 @@ def perform_installation(mountpoint: Path): installation.generate_key_files() # Set mirrors used by pacstrap (outside of installation) - if archinstall.arguments.get('mirror-region', None): - use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + if mirror_config.mirror_regions: + use_mirrors(mirror_config.mirror_regions) + if mirror_config.custom_mirrors: + add_custom_mirrors(mirror_config.custom_mirrors) installation.minimal_installation( testing=enable_testing, @@ -147,9 +150,8 @@ def perform_installation(mountpoint: Path): locales=[locale] ) - if archinstall.arguments.get('mirror-region') is not None: - if archinstall.arguments.get("mirrors", None) is not None: - installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + installation.set_mirrors(mirror_config) # Set the mirrors in the installation medium if archinstall.arguments.get('swap'): installation.setup_swap('zram') diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index a49f568d..1998f073 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any, Dict import archinstall from archinstall import SysInfo, info, debug -from archinstall.lib.mirrors import use_mirrors +from archinstall.lib import mirrors from archinstall.lib import models from archinstall.lib import disk from archinstall.lib.networking import check_mirror_reachable @@ -92,7 +92,7 @@ class SwissMainMenu(GlobalMenu): match self._execution_mode: case ExecutionMode.Full | ExecutionMode.Lineal: options_list = [ - 'keyboard-layout', 'mirror-region', 'disk_config', + 'keyboard-layout', 'mirror_config', 'disk_config', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' @@ -107,7 +107,7 @@ class SwissMainMenu(GlobalMenu): mandatory_list = ['disk_config'] case ExecutionMode.Only_OS: options_list = [ - 'keyboard-layout', 'mirror-region','bootloader', 'hostname', + 'keyboard-layout', 'mirror_config','bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' ] @@ -196,8 +196,11 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): installation.generate_key_files() # Set mirrors used by pacstrap (outside of installation) - if archinstall.arguments.get('mirror-region', None): - use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + if mirror_config.mirror_regions: + mirrors.use_mirrors(mirror_config.mirror_regions) + if mirror_config.custom_mirrors: + mirrors.add_custom_mirrors(mirror_config.custom_mirrors) installation.minimal_installation( testing=enable_testing, @@ -206,10 +209,8 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): locales=[locale] ) - if archinstall.arguments.get('mirror-region') is not None: - if archinstall.arguments.get("mirrors", None) is not None: - installation.set_mirrors( - archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + installation.set_mirrors(mirror_config) # Set the mirrors in the installation medium if archinstall.arguments.get('swap'): installation.setup_swap('zram') diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 487db4dd..7c4ffed7 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -24,7 +24,7 @@ def ask_user_questions(): global_menu.enable('keyboard-layout') # Set which region to download packages from during the installation - global_menu.enable('mirror-region') + global_menu.enable('mirror_config') global_menu.enable('sys-language') @@ -116,8 +116,11 @@ def perform_installation(mountpoint: Path): installation.generate_key_files() # Set mirrors used by pacstrap (outside of installation) - if archinstall.arguments.get('mirror-region', None): - mirrors.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + if mirror_config.mirror_regions: + mirrors.use_mirrors(mirror_config.mirror_regions) + if mirror_config.custom_mirrors: + mirrors.add_custom_mirrors(mirror_config.custom_mirrors) installation.minimal_installation( testing=enable_testing, @@ -126,9 +129,8 @@ def perform_installation(mountpoint: Path): locales=[locale] ) - if archinstall.arguments.get('mirror-region') is not None: - if archinstall.arguments.get("mirrors", None) is not None: - installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + if mirror_config := archinstall.arguments.get('mirror_config', None): + installation.set_mirrors(mirror_config) # Set the mirrors in the installation medium if archinstall.arguments.get('swap'): installation.setup_swap('zram') -- cgit v1.2.3-54-g00ecf From 06eadb31d4f0bca0c8cb95b6a9eb62ddd2d7cff2 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 5 Jun 2023 18:02:49 +1000 Subject: Move locales and cleanup menu (#1814) * Cleanup imports and unused code * Cleanup imports and unused code * Update build check * Keep deprecation exception * Simplify logging * Move locale into new sub-menu --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 3 +- archinstall/default_profiles/profile.py | 19 +--- archinstall/lib/global_menu.py | 95 +++++++++++----- archinstall/lib/hardware.py | 2 +- archinstall/lib/installer.py | 38 +++---- archinstall/lib/interactions/__init__.py | 3 +- archinstall/lib/interactions/general_conf.py | 12 +-- archinstall/lib/interactions/locale_conf.py | 43 -------- archinstall/lib/interactions/system_conf.py | 6 +- archinstall/lib/locale/__init__.py | 6 ++ archinstall/lib/locale/locale.py | 68 ++++++++++++ archinstall/lib/locale/locale_menu.py | 155 +++++++++++++++++++++++++++ archinstall/lib/menu/abstract_menu.py | 38 +------ archinstall/lib/utils/util.py | 23 +++- archinstall/scripts/guided.py | 17 ++- archinstall/scripts/swiss.py | 16 +-- examples/interactive_installation.py | 17 ++- 17 files changed, 374 insertions(+), 187 deletions(-) delete mode 100644 archinstall/lib/interactions/locale_conf.py create mode 100644 archinstall/lib/locale/__init__.py create mode 100644 archinstall/lib/locale/locale.py create mode 100644 archinstall/lib/locale/locale_menu.py (limited to 'archinstall/scripts') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index e6fcb267..ce58e255 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -213,8 +213,7 @@ def load_config(): """ from .lib.models import NetworkConfiguration - arguments.setdefault('sys-language', 'en_US') - arguments.setdefault('sys-encoding', 'utf-8') + arguments['locale_config'] = locale.LocaleConfiguration.parse_arg(arguments) if (archinstall_lang := arguments.get('archinstall-language', None)) is not None: arguments['archinstall-language'] = TranslationHandler().get_language_by_name(archinstall_lang) diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index b1ad1f50..ce07c286 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -3,7 +3,7 @@ from __future__ import annotations from enum import Enum, auto from typing import List, Optional, Any, Dict, TYPE_CHECKING, TypeVar -from archinstall.lib.output import FormattedOutput +from archinstall.lib.utils.util import format_cols if TYPE_CHECKING: from archinstall.lib.installer import Installer @@ -185,17 +185,6 @@ class Profile: return None def packages_text(self) -> str: - text = str(_('Installed packages')) + ':\n' - - nr_packages = len(self.packages) - if nr_packages <= 5: - col = 1 - elif nr_packages <= 10: - col = 2 - elif nr_packages <= 15: - col = 3 - else: - col = 4 - - text += FormattedOutput.as_columns(self.packages, col) - return text + header = str(_('Installed packages')) + output = format_cols(self.packages, header) + return output diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index fc58a653..91ebc6a0 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -4,6 +4,7 @@ from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING from . import disk from .general import secret +from .locale.locale_menu import LocaleConfiguration, LocaleMenu from .menu import Selector, AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu from .models import NetworkConfiguration @@ -24,9 +25,7 @@ from .interactions import ask_to_configure_network from .interactions import get_password, ask_for_a_timezone from .interactions import select_additional_repositories from .interactions import select_kernel -from .interactions import select_language -from .interactions import select_locale_enc -from .interactions import select_locale_lang +from .utils.util import format_cols from .interactions import ask_ntp from .interactions.disk_conf import select_disk_config @@ -36,6 +35,7 @@ if TYPE_CHECKING: class GlobalMenu(AbstractMenu): def __init__(self, data_store: Dict[str, Any]): + self._defined_text = str(_('Defined')) super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3) def setup_selection_menu_options(self): @@ -46,28 +46,19 @@ class GlobalMenu(AbstractMenu): lambda x: self._select_archinstall_language(x), display_func=lambda x: x.display_name, default=self.translation_handler.get_language_by_abbr('en')) - self._menu_options['keyboard-layout'] = \ + self._menu_options['locale_config'] = \ Selector( - _('Keyboard layout'), - lambda preset: select_language(preset), - default='us') + _('Locales'), + lambda preset: self._locale_selection(preset), + preview_func=self._prev_locale, + display_func=lambda x: self._defined_text if x else '') self._menu_options['mirror_config'] = \ Selector( _('Mirrors'), lambda preset: self._mirror_configuration(preset), - display_func=lambda x: str(_('Defined')) if x else '', + display_func=lambda x: self._defined_text if x else '', preview_func=self._prev_mirror_config ) - self._menu_options['sys-language'] = \ - Selector( - _('Locale language'), - lambda preset: select_locale_lang(preset), - default='en_US') - self._menu_options['sys-encoding'] = \ - Selector( - _('Locale encoding'), - lambda preset: select_locale_enc(preset), - default='UTF-8') self._menu_options['disk_config'] = \ Selector( _('Disk configuration'), @@ -103,32 +94,32 @@ class GlobalMenu(AbstractMenu): Selector( _('Root password'), lambda preset:self._set_root_password(), - display_func=lambda x: secret(x) if x else 'None') + display_func=lambda x: secret(x) if x else '') self._menu_options['!users'] = \ Selector( _('User account'), lambda x: self._create_user_account(x), default=[], - display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else None, + display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else '', preview_func=self._prev_users) self._menu_options['profile_config'] = \ Selector( _('Profile'), lambda preset: self._select_profile(preset), - display_func=lambda x: x.profile.name if x else 'None', + display_func=lambda x: x.profile.name if x else '', preview_func=self._prev_profile ) self._menu_options['audio'] = \ Selector( _('Audio'), lambda preset: self._select_audio(preset), - display_func=lambda x: x if x else 'None', + display_func=lambda x: x if x else '', default=None ) self._menu_options['parallel downloads'] = \ Selector( _('Parallel Downloads'), - add_number_of_parrallel_downloads, + lambda preset: add_number_of_parrallel_downloads(preset), display_func=lambda x: x if x else '0', default=0 ) @@ -141,19 +132,20 @@ class GlobalMenu(AbstractMenu): self._menu_options['packages'] = \ Selector( _('Additional packages'), - # lambda x: ask_additional_packages_to_install(storage['arguments'].get('packages', None)), - ask_additional_packages_to_install, + lambda preset: ask_additional_packages_to_install(preset), + display_func=lambda x: self._defined_text if x else '', + preview_func=self._prev_additional_pkgs, default=[]) self._menu_options['additional-repositories'] = \ Selector( _('Optional repositories'), - select_additional_repositories, + lambda preset: select_additional_repositories(preset), display_func=lambda x: ', '.join(x) if x else None, default=[]) self._menu_options['nic'] = \ Selector( _('Network configuration'), - ask_to_configure_network, + lambda preset: ask_to_configure_network(preset), display_func=lambda x: self._display_network_conf(x), preview_func=self._prev_network_config, default={}) @@ -177,12 +169,37 @@ class GlobalMenu(AbstractMenu): self._menu_options['install'] = \ Selector( self._install_text(), - exec_func=lambda n,v: True if len(self._missing_configs()) == 0 else False, + exec_func=lambda n, v: True if len(self._missing_configs()) == 0 else False, preview_func=self._prev_install_missing_config, no_store=True) self._menu_options['abort'] = Selector(_('Abort'), exec_func=lambda n,v:exit(1)) + def _missing_configs(self) -> List[str]: + def check(s): + return self._menu_options.get(s).has_selection() + + def has_superuser() -> bool: + sel = self._menu_options['!users'] + if sel.current_selection: + return any([u.sudo for u in sel.current_selection]) + return False + + mandatory_fields = dict(filter(lambda x: x[1].is_mandatory(), self._menu_options.items())) + missing = set() + + for key, selector in mandatory_fields.items(): + if key in ['!root-password', '!users']: + if not check('!root-password') and not has_superuser(): + missing.add( + str(_('Either root-password or at least 1 user with sudo privileges must be specified')) + ) + elif key == 'disk_config': + if not check('disk_config'): + missing.add(self._menu_options['disk_config'].description) + + return list(missing) + def _update_install_text(self, name: str, value: str): text = self._install_text() self._menu_options['install'].update_description(text) @@ -216,6 +233,21 @@ class GlobalMenu(AbstractMenu): disk_encryption = disk.DiskEncryptionMenu(mods, data_store, preset=preset).run() return disk_encryption + def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration: + data_store: Dict[str, Any] = {} + locale_config = LocaleMenu(data_store, preset).run() + return locale_config + + def _prev_locale(self) -> Optional[str]: + selector = self._menu_options['locale_config'] + if selector.has_selection(): + config: LocaleConfiguration = selector.current_selection # type: ignore + output = '{}: {}\n'.format(str(_('Keyboard layout')), config.kb_layout) + output += '{}: {}\n'.format(str(_('Locale language')), config.sys_lang) + output += '{}: {}'.format(str(_('Locale encoding')), config.sys_enc) + return output + return None + def _prev_network_config(self) -> Optional[str]: selector = self._menu_options['nic'] if selector.has_selection(): @@ -224,6 +256,13 @@ class GlobalMenu(AbstractMenu): return FormattedOutput.as_table(ifaces) return None + def _prev_additional_pkgs(self): + selector = self._menu_options['packages'] + if selector.has_selection(): + packages: List[str] = selector.current_selection + return format_cols(packages, None) + return None + def _prev_disk_layouts(self) -> Optional[str]: selector = self._menu_options['disk_config'] disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = selector.current_selection diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 220d3d37..2b65e07c 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -3,9 +3,9 @@ from functools import cached_property from pathlib import Path from typing import Optional, Dict, List +from .exceptions import SysCallError from .general import SysCommand from .networking import list_interfaces, enrich_iface_types -from .exceptions import SysCallError from .output import debug AVAILABLE_GFX_DRIVERS = { diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 30442774..6eac85fc 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -12,6 +12,7 @@ from . import disk from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .general import SysCommand from .hardware import SysInfo +from .locale import LocaleConfiguration from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .luks import Luks2 from .mirrors import use_mirrors, MirrorConfiguration, add_custom_mirrors @@ -457,37 +458,36 @@ class Installer: with open(f'{self.target}/etc/hostname', 'w') as fh: fh.write(hostname + '\n') - def set_locale(self, locale :str, encoding :str = 'UTF-8', *args :str, **kwargs :str) -> bool: - if not len(locale): - return True - + def set_locale(self, locale_config: LocaleConfiguration): modifier = '' + lang = locale_config.sys_lang + encoding = locale_config.sys_enc # This is a temporary patch to fix #1200 - if '.' in locale: - locale, potential_encoding = locale.split('.', 1) + if '.' in locale_config.sys_lang: + lang, potential_encoding = locale_config.sys_lang.split('.', 1) # Override encoding if encoding is set to the default parameter # and the "found" encoding differs. - if encoding == 'UTF-8' and encoding != potential_encoding: + if locale_config.sys_enc == 'UTF-8' and locale_config.sys_enc != potential_encoding: encoding = potential_encoding # Make sure we extract the modifier, that way we can put it in if needed. - if '@' in locale: - locale, modifier = locale.split('@', 1) + if '@' in locale_config.sys_lang: + lang, modifier = locale_config.sys_lang.split('@', 1) modifier = f"@{modifier}" # - End patch with open(f'{self.target}/etc/locale.gen', 'a') as fh: - fh.write(f'{locale}.{encoding}{modifier} {encoding}\n') + fh.write(f'{lang}.{encoding}{modifier} {encoding}\n') + with open(f'{self.target}/etc/locale.conf', 'w') as fh: - fh.write(f'LANG={locale}.{encoding}{modifier}\n') + fh.write(f'LANG={lang}.{encoding}{modifier}\n') try: SysCommand(f'/usr/bin/arch-chroot {self.target} locale-gen') - return True - except SysCallError: - return False + except SysCallError as e: + error(f'Failed to run locale-gen on target: {e}') def set_timezone(self, zone :str, *args :str, **kwargs :str) -> bool: if not zone: @@ -620,7 +620,7 @@ class Installer: return True - def mkinitcpio(self, *flags :str) -> bool: + def mkinitcpio(self, flags: List[str], locale_config: LocaleConfiguration) -> bool: for plugin in plugins.values(): if hasattr(plugin, 'on_mkinitcpio'): # Allow plugins to override the usage of mkinitcpio altogether. @@ -630,7 +630,7 @@ class Installer: # mkinitcpio will error out if there's no vconsole. if (vconsole := Path(f"{self.target}/etc/vconsole.conf")).exists() is False: with vconsole.open('w') as fh: - fh.write(f"KEYMAP={storage['arguments']['keyboard-layout']}\n") + fh.write(f"KEYMAP={locale_config.kb_layout}\n") with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit: mkinit.write(f"MODULES=({' '.join(self.modules)})\n") @@ -658,7 +658,7 @@ class Installer: testing: bool = False, multilib: bool = False, hostname: str = 'archinstall', - locales: List[str] = ['en_US.UTF-8 UTF-8'] + locale_config: LocaleConfiguration = LocaleConfiguration.default() ): for mod in self._disk_config.device_modifications: for part in mod.partitions: @@ -734,12 +734,12 @@ class Installer: # sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{localtime} /etc/localtime') # sys_command('/usr/bin/arch-chroot /mnt hwclock --hctosys --localtime') self.set_hostname(hostname) - self.set_locale(*locales[0].split()) + self.set_locale(locale_config) # TODO: Use python functions for this SysCommand(f'/usr/bin/arch-chroot {self.target} chmod 700 /root') - self.mkinitcpio('-P') + self.mkinitcpio(['-P'], locale_config) self.helper_flags['base'] = True diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 158750cc..466cfa0b 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -1,4 +1,3 @@ -from .locale_conf import select_locale_lang, select_locale_enc from .manage_users_conf import UserList, ask_for_additional_users from .network_conf import ManualNetworkConfig, ask_to_configure_network from .utils import get_password @@ -10,7 +9,7 @@ from .disk_conf import ( ) from .general_conf import ( - ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_language, + ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_archinstall_language, ask_additional_packages_to_install, add_number_of_parrallel_downloads, select_additional_repositories ) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 0338c61e..3b78847b 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -3,7 +3,7 @@ from __future__ import annotations import pathlib from typing import List, Any, Optional, TYPE_CHECKING -from ..locale import list_keyboard_languages, list_timezones +from ..locale import list_timezones, list_keyboard_languages from ..menu import MenuSelectionType, Menu, TextInput from ..output import warn from ..packages.packages import validate_package_list @@ -119,18 +119,18 @@ def select_archinstall_language(languages: List[Language], preset: Language) -> raise ValueError('Language selection not handled') -def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: +def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]: # Additional packages (with some light weight error handling for invalid package names) print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) - def read_packages(already_defined: list = []) -> list: - display = ' '.join(already_defined) + def read_packages(p: List = []) -> list: + display = ' '.join(p) input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip() return input_packages.split() if input_packages else [] - pre_set_packages = pre_set_packages if pre_set_packages else [] - packages = read_packages(pre_set_packages) + preset = preset if preset else [] + packages = read_packages(preset) if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']: while True: diff --git a/archinstall/lib/interactions/locale_conf.py b/archinstall/lib/interactions/locale_conf.py deleted file mode 100644 index de115202..00000000 --- a/archinstall/lib/interactions/locale_conf.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Any, TYPE_CHECKING, Optional - -from ..locale import list_locales -from ..menu import Menu, MenuSelectionType - -if TYPE_CHECKING: - _: Any - - -def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: - locales = list_locales() - locale_lang = set([locale.split()[0] for locale in locales]) - - choice = Menu( - _('Choose which locale language to use'), - list(locale_lang), - sort=True, - preset_values=preset - ).run() - - match choice.type_: - case MenuSelectionType.Selection: return choice.single_value - case MenuSelectionType.Skip: return preset - - return None - - -def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: - locales = list_locales() - locale_enc = set([locale.split()[1] for locale in locales]) - - choice = Menu( - _('Choose which locale encoding to use'), - list(locale_enc), - sort=True, - preset_values=preset - ).run() - - match choice.type_: - case MenuSelectionType.Selection: return choice.single_value - case MenuSelectionType.Skip: return preset - - return None diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py index bbcb5b23..ea7e5989 100644 --- a/archinstall/lib/interactions/system_conf.py +++ b/archinstall/lib/interactions/system_conf.py @@ -29,14 +29,14 @@ def select_kernel(preset: List[str] = []) -> List[str]: sort=True, multi=True, preset_values=preset, - allow_reset=True, allow_reset_warning_msg=warning ).run() match choice.type_: case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return [] - case MenuSelectionType.Selection: return choice.value # type: ignore + case MenuSelectionType.Selection: return choice.single_value + + return [] def ask_for_bootloader(preset: Bootloader) -> Bootloader: diff --git a/archinstall/lib/locale/__init__.py b/archinstall/lib/locale/__init__.py new file mode 100644 index 00000000..6c32d6f3 --- /dev/null +++ b/archinstall/lib/locale/__init__.py @@ -0,0 +1,6 @@ +from .locale_menu import LocaleConfiguration +from .locale import ( + list_keyboard_languages, list_locales, list_x11_keyboard_languages, + verify_keyboard_layout, verify_x11_keyboard_layout, set_kb_layout, + list_timezones +) diff --git a/archinstall/lib/locale/locale.py b/archinstall/lib/locale/locale.py new file mode 100644 index 00000000..c3294e83 --- /dev/null +++ b/archinstall/lib/locale/locale.py @@ -0,0 +1,68 @@ +from typing import Iterator, List + +from ..exceptions import ServiceException, SysCallError +from ..general import SysCommand +from ..output import error + + +def list_keyboard_languages() -> Iterator[str]: + for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() + + +def list_locales() -> List[str]: + with open('/etc/locale.gen', 'r') as fp: + locales = [] + # before the list of locales begins there's an empty line with a '#' in front + # so we'll collect the localels from bottom up and halt when we're donw + entries = fp.readlines() + entries.reverse() + + for entry in entries: + text = entry.replace('#', '').strip() + if text == '': + break + locales.append(text) + + locales.reverse() + return locales + + +def list_x11_keyboard_languages() -> Iterator[str]: + for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() + + +def verify_keyboard_layout(layout :str) -> bool: + for language in list_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + + +def verify_x11_keyboard_layout(layout :str) -> bool: + for language in list_x11_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + + +def set_kb_layout(locale :str) -> bool: + if len(locale.strip()): + if not verify_keyboard_layout(locale): + error(f"Invalid keyboard locale specified: {locale}") + return False + + try: + SysCommand(f'localectl set-keymap {locale}') + except SysCallError as err: + raise ServiceException(f"Unable to set locale '{locale}' for console: {err}") + + return True + + return False + + +def list_timezones() -> Iterator[str]: + for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() diff --git a/archinstall/lib/locale/locale_menu.py b/archinstall/lib/locale/locale_menu.py new file mode 100644 index 00000000..29dd775d --- /dev/null +++ b/archinstall/lib/locale/locale_menu.py @@ -0,0 +1,155 @@ +from dataclasses import dataclass +from typing import Dict, Any, TYPE_CHECKING, Optional + +from .locale import set_kb_layout, list_keyboard_languages, list_locales +from ..menu import Selector, AbstractSubMenu, MenuSelectionType, Menu + +if TYPE_CHECKING: + _: Any + + +@dataclass +class LocaleConfiguration: + kb_layout: str + sys_lang: str + sys_enc: str + + @staticmethod + def default() -> 'LocaleConfiguration': + return LocaleConfiguration('us', 'en_US', 'UTF-8') + + def json(self) -> Dict[str, str]: + return { + 'kb_layout': self.kb_layout, + 'sys_lang': self.sys_lang, + 'sys_enc': self.sys_enc + } + + @classmethod + def _load_config(cls, config: 'LocaleConfiguration', args: Dict[str, Any]) -> 'LocaleConfiguration': + if 'sys_lang' in args: + config.sys_lang = args['sys_lang'] + if 'sys_enc' in args: + config.sys_enc = args['sys_enc'] + if 'kb_layout' in args: + config.kb_layout = args['kb_layout'] + + return config + + @classmethod + def parse_arg(cls, args: Dict[str, Any]) -> 'LocaleConfiguration': + default = cls.default() + + if 'locale_config' in args: + default = cls._load_config(default, args['locale_config']) + else: + default = cls._load_config(default, args) + + return default + + +class LocaleMenu(AbstractSubMenu): + def __init__( + self, + data_store: Dict[str, Any], + locele_conf: LocaleConfiguration + ): + self._preset = locele_conf + super().__init__(data_store=data_store) + + def setup_selection_menu_options(self): + self._menu_options['keyboard-layout'] = \ + Selector( + _('Keyboard layout'), + lambda preset: self._select_kb_layout(preset), + default='us', + enabled=True) + self._menu_options['sys-language'] = \ + Selector( + _('Locale language'), + lambda preset: select_locale_lang(preset), + default='en_US', + enabled=True) + self._menu_options['sys-encoding'] = \ + Selector( + _('Locale encoding'), + lambda preset: select_locale_enc(preset), + default='UTF-8', + enabled=True) + + def run(self, allow_reset: bool = True) -> LocaleConfiguration: + super().run(allow_reset=allow_reset) + + return LocaleConfiguration( + self._data_store['keyboard-layout'], + self._data_store['sys-language'], + self._data_store['sys-encoding'] + ) + + def _select_kb_layout(self, preset: Optional[str]) -> Optional[str]: + kb_lang = select_kb_layout(preset) + if kb_lang: + set_kb_layout(kb_lang) + return kb_lang + + +def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: + locales = list_locales() + locale_lang = set([locale.split()[0] for locale in locales]) + + choice = Menu( + _('Choose which locale language to use'), + list(locale_lang), + sort=True, + preset_values=preset + ).run() + + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return preset + + return None + + +def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: + locales = list_locales() + locale_enc = set([locale.split()[1] for locale in locales]) + + choice = Menu( + _('Choose which locale encoding to use'), + list(locale_enc), + sort=True, + preset_values=preset + ).run() + + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return preset + + return None + + +def select_kb_layout(preset: Optional[str] = None) -> Optional[str]: + """ + Asks the user to select a language + Usually this is combined with :ref:`archinstall.list_keyboard_languages`. + + :return: The language/dictionary key of the selected language + :rtype: str + """ + kb_lang = list_keyboard_languages() + # sort alphabetically and then by length + sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) + + choice = Menu( + _('Select keyboard layout'), + sorted_kb_lang, + preset_values=preset, + sort=False + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 2bd56374..eee99747 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING from .menu import Menu, MenuSelectionType -from ..locale import set_keyboard_language from ..output import error from ..translationhandler import TranslationHandler, Language @@ -130,7 +129,7 @@ class Selector: if current: padding += 5 description = str(self._description).ljust(padding, ' ') - current = str(_('set: {}').format(current)) + current = current else: description = self._description current = '' @@ -243,31 +242,6 @@ class AbstractMenu: elif selector is not None and selector.has_selection(): self._data_store[selector_name] = selector.current_selection - def _missing_configs(self) -> List[str]: - def check(s): - return self._menu_options.get(s).has_selection() - - def has_superuser() -> bool: - sel = self._menu_options['!users'] - if sel.current_selection: - return any([u.sudo for u in sel.current_selection]) - return False - - mandatory_fields = dict(filter(lambda x: x[1].is_mandatory(), self._menu_options.items())) - missing = set() - - for key, selector in mandatory_fields.items(): - if key in ['!root-password', '!users']: - if not check('!root-password') and not has_superuser(): - missing.add( - str(_('Either root-password or at least 1 user with sudo privileges must be specified')) - ) - elif key == 'disk_config': - if not check('disk_config'): - missing.add(self._menu_options['disk_config'].description) - - return list(missing) - def setup_selection_menu_options(self): """ Define the menu options. Menu options can be defined here in a subclass or done per program calling self.set_option() @@ -328,9 +302,6 @@ class AbstractMenu: cursor_pos = None while True: - # Before continuing, set the preferred keyboard layout/language in the current terminal. - # This will just help the user with the next following questions. - self._set_kb_language() enabled_menus = self._menus_to_enable() padding = self._get_menu_text_padding(list(enabled_menus.values())) @@ -425,13 +396,6 @@ class AbstractMenu: return True - def _set_kb_language(self): - """ general for ArchInstall""" - # Before continuing, set the preferred keyboard layout/language in the current terminal. - # This will just help the user with the next following questions. - if self._data_store.get('keyboard-layout', None) and len(self._data_store['keyboard-layout']): - set_keyboard_language(self._data_store['keyboard-layout']) - def _verify_selection_enabled(self, selection_name: str) -> bool: """ general """ if selection := self._menu_options.get(selection_name, None): diff --git a/archinstall/lib/utils/util.py b/archinstall/lib/utils/util.py index 34716f4a..8df75ab1 100644 --- a/archinstall/lib/utils/util.py +++ b/archinstall/lib/utils/util.py @@ -1,6 +1,7 @@ from pathlib import Path -from typing import Any, TYPE_CHECKING, Optional +from typing import Any, TYPE_CHECKING, Optional, List +from ..output import FormattedOutput from ..output import info if TYPE_CHECKING: @@ -28,3 +29,23 @@ def is_subpath(first: Path, second: Path): return True except ValueError: return False + + +def format_cols(items: List[str], header: Optional[str]) -> str: + if header: + text = f'{header}:\n' + else: + text = '' + + nr_items = len(items) + if nr_items <= 5: + col = 1 + elif nr_items <= 10: + col = 2 + elif nr_items <= 15: + col = 3 + else: + col = 4 + + text += FormattedOutput.as_columns(items, col) + return text diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 1aecc1cd..7f9b9fd6 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -5,6 +5,7 @@ from typing import Any, TYPE_CHECKING import archinstall from archinstall import info, debug from archinstall import SysInfo +from archinstall.lib import locale from archinstall.lib import disk from archinstall.lib.global_menu import GlobalMenu from archinstall.default_profiles.applications.pipewire import PipewireProfile @@ -42,14 +43,10 @@ def ask_user_questions(): global_menu.enable('archinstall-language') - global_menu.enable('keyboard-layout') - # Set which region to download packages from during the installation global_menu.enable('mirror_config') - global_menu.enable('sys-language') - - global_menu.enable('sys-encoding') + global_menu.enable('locale_config') global_menu.enable('disk_config', mandatory=True) @@ -76,7 +73,7 @@ def ask_user_questions(): global_menu.enable('audio') # Ask for preferred kernel: - global_menu.enable('kernels') + global_menu.enable('kernels', mandatory=True) global_menu.enable('packages') @@ -114,9 +111,7 @@ def perform_installation(mountpoint: Path): # Retrieve list of additional repositories and set boolean values appropriately enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) - - locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" - + locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config'] disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) with Installer( @@ -147,7 +142,7 @@ def perform_installation(mountpoint: Path): testing=enable_testing, multilib=enable_multilib, hostname=archinstall.arguments.get('hostname', 'archlinux'), - locales=[locale] + locale_config=locale_config ) if mirror_config := archinstall.arguments.get('mirror_config', None): @@ -210,7 +205,7 @@ def perform_installation(mountpoint: Path): # This step must be after profile installs to allow profiles_bck to install language pre-requisits. # After which, this step will set the language both for console and x11 if x11 was installed for instance. - installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + installation.set_keyboard_language(locale_config.kb_layout) if profile_config := archinstall.arguments.get('profile_config', None): profile_config.profile.post_install(installation) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 1998f073..375458a1 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -8,6 +8,7 @@ from archinstall import SysInfo, info, debug from archinstall.lib import mirrors from archinstall.lib import models from archinstall.lib import disk +from archinstall.lib import locale from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib import menu @@ -92,14 +93,14 @@ class SwissMainMenu(GlobalMenu): match self._execution_mode: case ExecutionMode.Full | ExecutionMode.Lineal: options_list = [ - 'keyboard-layout', 'mirror_config', 'disk_config', + 'mirror_config', 'disk_config', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' ] if archinstall.arguments.get('advanced', False): - options_list.extend(['sys-language', 'sys-encoding']) + options_list.extend(['locale_config']) mandatory_list = ['disk_config', 'bootloader', 'hostname'] case ExecutionMode.Only_HD: @@ -107,7 +108,7 @@ class SwissMainMenu(GlobalMenu): mandatory_list = ['disk_config'] case ExecutionMode.Only_OS: options_list = [ - 'keyboard-layout', 'mirror_config','bootloader', 'hostname', + 'mirror_config','bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' ] @@ -115,7 +116,7 @@ class SwissMainMenu(GlobalMenu): mandatory_list = ['hostname'] if archinstall.arguments.get('advanced', False): - options_list += ['sys-language','sys-encoding'] + options_list += ['locale_config'] case ExecutionMode.Minimal: pass case _: @@ -176,8 +177,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) - - locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" + locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config'] with Installer( mountpoint, @@ -206,7 +206,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): testing=enable_testing, multilib=enable_multilib, hostname=archinstall.arguments.get('hostname', 'archlinux'), - locales=[locale] + locale_config=locale_config ) if mirror_config := archinstall.arguments.get('mirror_config', None): @@ -263,7 +263,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): # This step must be after profile installs to allow profiles_bck to install language pre-requisits. # After which, this step will set the language both for console and x11 if x11 was installed for instance. - installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + installation.set_keyboard_language(locale_config.kb_layout) if profile_config := archinstall.arguments.get('profile_config', None): profile_config.profile.post_install(installation) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 7c4ffed7..ce1a80ec 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -10,6 +10,7 @@ from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall import disk from archinstall import menu from archinstall import models +from archinstall import locale from archinstall import info, debug if TYPE_CHECKING: @@ -21,14 +22,10 @@ def ask_user_questions(): global_menu.enable('archinstall-language') - global_menu.enable('keyboard-layout') - # Set which region to download packages from during the installation global_menu.enable('mirror_config') - global_menu.enable('sys-language') - - global_menu.enable('sys-encoding') + global_menu.enable('locale_config') global_menu.enable('disk_config', mandatory=True) @@ -55,7 +52,7 @@ def ask_user_questions(): global_menu.enable('audio') # Ask for preferred kernel: - global_menu.enable('kernels') + global_menu.enable('kernels', mandatory=True) global_menu.enable('packages') @@ -93,9 +90,7 @@ def perform_installation(mountpoint: Path): # Retrieve list of additional repositories and set boolean values appropriately enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) - - locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" - + locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config'] disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) with Installer( @@ -126,7 +121,7 @@ def perform_installation(mountpoint: Path): testing=enable_testing, multilib=enable_multilib, hostname=archinstall.arguments.get('hostname', 'archlinux'), - locales=[locale] + locale_config=locale_config ) if mirror_config := archinstall.arguments.get('mirror_config', None): @@ -189,7 +184,7 @@ def perform_installation(mountpoint: Path): # This step must be after profile installs to allow profiles_bck to install language pre-requisits. # After which, this step will set the language both for console and x11 if x11 was installed for instance. - installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + installation.set_keyboard_language(locale_config.kb_layout) if profile_config := archinstall.arguments.get('profile_config', None): profile_config.profile.post_install(installation) -- cgit v1.2.3-54-g00ecf From 885f89c3a15ef9d5ed8711cdb270ed5aea189705 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Wed, 21 Jun 2023 17:54:42 +1000 Subject: Rename encryption method (#1888) * Rename encryption method * Update --------- Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_model.py | 7 +++---- archinstall/lib/disk/encryption_menu.py | 2 +- archinstall/scripts/minimal.py | 2 +- examples/full_automated_installation.py | 2 +- examples/minimal_installation.py | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) (limited to 'archinstall/scripts') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 8e72390c..35fbd40c 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -777,13 +777,12 @@ class DeviceModification: class EncryptionType(Enum): NoEncryption = "no_encryption" - Partition = "partition" + Luks = "luks" @classmethod def _encryption_type_mapper(cls) -> Dict[str, 'EncryptionType']: return { - # str(_('Full disk encryption')): EncryptionType.FullDiskEncryption, - str(_('Partition encryption')): EncryptionType.Partition + 'Luks': EncryptionType.Luks } @classmethod @@ -800,7 +799,7 @@ class EncryptionType(Enum): @dataclass class DiskEncryption: - encryption_type: EncryptionType = EncryptionType.Partition + encryption_type: EncryptionType = EncryptionType.Luks encryption_password: str = '' partitions: List[PartitionModification] = field(default_factory=list) hsm_device: Optional[Fido2Device] = None diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 8c64e65e..89eade2b 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -110,7 +110,7 @@ class DiskEncryptionMenu(AbstractSubMenu): def select_encryption_type(preset: EncryptionType) -> Optional[EncryptionType]: title = str(_('Select disk encryption option')) options = [ - EncryptionType.type_to_text(EncryptionType.Partition) + EncryptionType.type_to_text(EncryptionType.Luks) ] preset_value = EncryptionType.type_to_text(preset) diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py index 704759fc..f6650ff8 100644 --- a/archinstall/scripts/minimal.py +++ b/archinstall/scripts/minimal.py @@ -82,7 +82,7 @@ def parse_disk_encryption(): partitions += list(filter(lambda x: x.mountpoint != Path('/boot'), mod.partitions)) archinstall.arguments['disk_encryption'] = disk.DiskEncryption( - encryption_type=disk.EncryptionType.Partition, + encryption_type=disk.EncryptionType.Luks, encryption_password=enc_password, partitions=partitions ) diff --git a/examples/full_automated_installation.py b/examples/full_automated_installation.py index dcef731a..79e85348 100644 --- a/examples/full_automated_installation.py +++ b/examples/full_automated_installation.py @@ -63,7 +63,7 @@ disk_config = disk.DiskLayoutConfiguration( # disk encryption configuration (Optional) disk_encryption = disk.DiskEncryption( encryption_password="enc_password", - encryption_type=disk.EncryptionType.Partition, + encryption_type=disk.EncryptionType.Luks, partitions=[home_partition], hsm_device=None ) diff --git a/examples/minimal_installation.py b/examples/minimal_installation.py index e31adea4..c91a5d46 100644 --- a/examples/minimal_installation.py +++ b/examples/minimal_installation.py @@ -66,7 +66,7 @@ def parse_disk_encryption(): partitions += list(filter(lambda x: x.mountpoint != Path('/boot'), mod.partitions)) archinstall.arguments['disk_encryption'] = disk.DiskEncryption( - encryption_type=disk.EncryptionType.Partition, + encryption_type=disk.EncryptionType.Luks, encryption_password=enc_password, partitions=partitions ) -- cgit v1.2.3-54-g00ecf From 2f273868d416c3309191db8c616aae683d78370a Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 17 Jul 2023 17:27:21 +1000 Subject: Fix network settings loading from config file (#1921) * Fix network config error and simplify code * Update schema and exmaple --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 7 +- archinstall/lib/global_menu.py | 27 ++-- archinstall/lib/installer.py | 18 +-- archinstall/lib/interactions/__init__.py | 2 +- archinstall/lib/interactions/network_conf.py | 172 -------------------- archinstall/lib/interactions/network_menu.py | 159 +++++++++++++++++++ archinstall/lib/models/__init__.py | 6 +- archinstall/lib/models/network_configuration.py | 200 ++++++++++-------------- archinstall/scripts/guided.py | 11 +- archinstall/scripts/swiss.py | 9 +- docs/installing/guided.rst | 8 +- examples/config-sample.json | 20 ++- examples/custom-command-sample.json | 4 +- examples/interactive_installation.py | 7 +- schema.json | 23 +-- 15 files changed, 313 insertions(+), 360 deletions(-) delete mode 100644 archinstall/lib/interactions/network_conf.py create mode 100644 archinstall/lib/interactions/network_menu.py (limited to 'archinstall/scripts') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index af811465..c4b64912 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -225,10 +225,9 @@ def load_config(): if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) - if arguments.get('nic', None) is not None: - handler = models.NetworkConfigurationHandler() - handler.parse_arguments(arguments.get('nic')) - arguments['nic'] = handler.configuration + if arguments.get('network_config', None) is not None: + config = NetworkConfiguration.parse_arg(arguments.get('network_config')) + arguments['network_config'] = config if arguments.get('!users', None) is not None or arguments.get('!superusers', None) is not None: users = arguments.get('!users', None) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 02b1b0b6..5503d9ce 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -7,7 +7,7 @@ from .general import secret from .locale.locale_menu import LocaleConfiguration, LocaleMenu from .menu import Selector, AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu -from .models import NetworkConfiguration +from .models import NetworkConfiguration, NicType from .models.bootloader import Bootloader from .models.users import User from .output import FormattedOutput @@ -142,7 +142,7 @@ class GlobalMenu(AbstractMenu): lambda preset: select_additional_repositories(preset), display_func=lambda x: ', '.join(x) if x else None, default=[]) - self._menu_options['nic'] = \ + self._menu_options['network_config'] = \ Selector( _('Network configuration'), lambda preset: ask_to_configure_network(preset), @@ -221,14 +221,11 @@ class GlobalMenu(AbstractMenu): return _('Install ({} config(s) missing)').format(missing) return _('Install') - def _display_network_conf(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str: - if not cur_value: - return _('Not configured, unavailable unless setup manually') - else: - if isinstance(cur_value, list): - return str(_('Configured {} interfaces')).format(len(cur_value)) - else: - return str(cur_value) + def _display_network_conf(self, config: Optional[NetworkConfiguration]) -> str: + if not config: + return str(_('Not configured, unavailable unless setup manually')) + + return config.type.display_msg() def _disk_encryption(self, preset: Optional[disk.DiskEncryption]) -> Optional[disk.DiskEncryption]: mods: Optional[List[disk.DeviceModification]] = self._menu_options['disk_config'].current_selection @@ -257,11 +254,11 @@ class GlobalMenu(AbstractMenu): return None def _prev_network_config(self) -> Optional[str]: - selector = self._menu_options['nic'] - if selector.has_selection(): - ifaces = selector.current_selection - if isinstance(ifaces, list): - return FormattedOutput.as_table(ifaces) + selector: Optional[NetworkConfiguration] = self._menu_options['network_config'].current_selection + if selector: + if selector.type == NicType.MANUAL: + output = FormattedOutput.as_table(selector.nics) + return output return None def _prev_additional_pkgs(self): diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 02d48768..8c5e7648 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -19,7 +19,7 @@ from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .luks import Luks2 from .mirrors import use_mirrors, MirrorConfiguration, add_custom_mirrors from .models.bootloader import Bootloader -from .models.network_configuration import NetworkConfiguration +from .models.network_configuration import Nic from .models.users import User from .output import log, error, info, warn, debug from . import pacman @@ -458,20 +458,20 @@ class Installer: def drop_to_shell(self) -> None: subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True) - def configure_nic(self, network_config: NetworkConfiguration) -> None: - conf = network_config.as_systemd_config() + def configure_nic(self, nic: Nic): + conf = nic.as_systemd_config() for plugin in plugins.values(): if hasattr(plugin, 'on_configure_nic'): conf = plugin.on_configure_nic( - network_config.iface, - network_config.dhcp, - network_config.ip, - network_config.gateway, - network_config.dns + nic.iface, + nic.dhcp, + nic.ip, + nic.gateway, + nic.dns ) or conf - with open(f"{self.target}/etc/systemd/network/10-{network_config.iface}.network", "a") as netconf: + with open(f"{self.target}/etc/systemd/network/10-{nic.iface}.network", "a") as netconf: netconf.write(str(conf)) def copy_iso_network_config(self, enable_services :bool = False) -> bool: diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 466cfa0b..53be8e7a 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -1,5 +1,5 @@ from .manage_users_conf import UserList, ask_for_additional_users -from .network_conf import ManualNetworkConfig, ask_to_configure_network +from .network_menu import ManualNetworkConfig, ask_to_configure_network from .utils import get_password from .disk_conf import ( diff --git a/archinstall/lib/interactions/network_conf.py b/archinstall/lib/interactions/network_conf.py deleted file mode 100644 index 18a834a1..00000000 --- a/archinstall/lib/interactions/network_conf.py +++ /dev/null @@ -1,172 +0,0 @@ -from __future__ import annotations - -import ipaddress -from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict - -from ..menu import MenuSelectionType, TextInput -from ..models.network_configuration import NetworkConfiguration, NicType - -from ..networking import list_interfaces -from ..output import FormattedOutput, warn -from ..menu import ListManager, Menu - -if TYPE_CHECKING: - _: Any - - -class ManualNetworkConfig(ListManager): - """ - subclass of ListManager for the managing of network configurations - """ - - def __init__(self, prompt: str, ifaces: List[NetworkConfiguration]): - self._actions = [ - str(_('Add interface')), - str(_('Edit interface')), - str(_('Delete interface')) - ] - - super().__init__(prompt, ifaces, [self._actions[0]], self._actions[1:]) - - def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[NetworkConfiguration]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, iface in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = iface - - return display_data - - def selected_action_display(self, iface: NetworkConfiguration) -> str: - return iface.iface if iface.iface else '' - - def handle_action(self, action: str, entry: Optional[NetworkConfiguration], data: List[NetworkConfiguration]): - if action == self._actions[0]: # add - iface_name = self._select_iface(data) - if iface_name: - iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) - iface = self._edit_iface(iface) - data += [iface] - elif entry: - if action == self._actions[1]: # edit interface - data = [d for d in data if d.iface != entry.iface] - data.append(self._edit_iface(entry)) - elif action == self._actions[2]: # delete - data = [d for d in data if d != entry] - - return data - - def _select_iface(self, data: List[NetworkConfiguration]) -> Optional[Any]: - all_ifaces = list_interfaces().values() - existing_ifaces = [d.iface for d in data] - available = set(all_ifaces) - set(existing_ifaces) - choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() - - if choice.type_ == MenuSelectionType.Skip: - return None - - return choice.value - - def _edit_iface(self, edit_iface: NetworkConfiguration): - iface_name = edit_iface.iface - modes = ['DHCP (auto detect)', 'IP (static)'] - default_mode = 'DHCP (auto detect)' - - prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) - mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() - - if mode.value == 'IP (static)': - while 1: - prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) - ip = TextInput(prompt, edit_iface.ip).run().strip() - # Implemented new check for correct IP/subnet input - try: - ipaddress.ip_interface(ip) - break - except ValueError: - warn("You need to enter a valid IP in IP-config mode") - - # Implemented new check for correct gateway IP address - gateway = None - - while 1: - gateway = TextInput( - _('Enter your gateway (router) IP address or leave blank for none: '), - edit_iface.gateway - ).run().strip() - try: - if len(gateway) > 0: - ipaddress.ip_address(gateway) - break - except ValueError: - warn("You need to enter a valid gateway (router) IP address") - - if edit_iface.dns: - display_dns = ' '.join(edit_iface.dns) - else: - display_dns = None - dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() - - dns = [] - if len(dns_input): - dns = dns_input.split(' ') - - return NetworkConfiguration(NicType.MANUAL, iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) - else: - # this will contain network iface names - return NetworkConfiguration(NicType.MANUAL, iface=iface_name) - - -def ask_to_configure_network( - preset: Union[NetworkConfiguration, List[NetworkConfiguration]] -) -> Optional[NetworkConfiguration | List[NetworkConfiguration]]: - """ - Configure the network on the newly installed system - """ - network_options = { - 'none': str(_('No network configuration')), - 'iso_config': str(_('Copy ISO network configuration to installation')), - 'network_manager': str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')), - 'manual': str(_('Manual configuration')) - } - # for this routine it's easier to set the cursor position rather than a preset value - cursor_idx = None - - if preset and not isinstance(preset, list): - if preset.type == 'iso_config': - cursor_idx = 0 - elif preset.type == 'network_manager': - cursor_idx = 1 - - warning = str(_('Are you sure you want to reset this setting?')) - - choice = Menu( - _('Select one network interface to configure'), - list(network_options.values()), - cursor_index=cursor_idx, - sort=False, - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return None - - if choice.value == network_options['none']: - return None - elif choice.value == network_options['iso_config']: - return NetworkConfiguration(NicType.ISO) - elif choice.value == network_options['network_manager']: - return NetworkConfiguration(NicType.NM) - elif choice.value == network_options['manual']: - preset_ifaces = preset if isinstance(preset, list) else [] - return ManualNetworkConfig('Configure interfaces', preset_ifaces).run() - - return preset diff --git a/archinstall/lib/interactions/network_menu.py b/archinstall/lib/interactions/network_menu.py new file mode 100644 index 00000000..14fc5785 --- /dev/null +++ b/archinstall/lib/interactions/network_menu.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import ipaddress +from typing import Any, Optional, TYPE_CHECKING, List, Dict + +from ..menu import MenuSelectionType, TextInput +from ..models.network_configuration import NetworkConfiguration, NicType, Nic + +from ..networking import list_interfaces +from ..output import FormattedOutput, warn +from ..menu import ListManager, Menu + +if TYPE_CHECKING: + _: Any + + +class ManualNetworkConfig(ListManager): + """ + subclass of ListManager for the managing of network configurations + """ + + def __init__(self, prompt: str, preset: List[Nic]): + self._actions = [ + str(_('Add interface')), + str(_('Edit interface')), + str(_('Delete interface')) + ] + super().__init__(prompt, preset, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[Nic]) -> Dict[str, Optional[Nic]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[Nic]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, iface in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = iface + + return display_data + + def selected_action_display(self, nic: Nic) -> str: + return nic.iface if nic.iface else '' + + def handle_action(self, action: str, entry: Optional[Nic], data: List[Nic]): + if action == self._actions[0]: # add + iface = self._select_iface(data) + if iface: + nic = Nic(iface=iface) + nic = self._edit_iface(nic) + data += [nic] + elif entry: + if action == self._actions[1]: # edit interface + data = [d for d in data if d.iface != entry.iface] + data.append(self._edit_iface(entry)) + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] + + return data + + def _select_iface(self, data: List[Nic]) -> Optional[str]: + all_ifaces = list_interfaces().values() + existing_ifaces = [d.iface for d in data] + available = set(all_ifaces) - set(existing_ifaces) + choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() + + if choice.type_ == MenuSelectionType.Skip: + return None + + return choice.single_value + + def _edit_iface(self, edit_nic: Nic) -> Nic: + iface_name = edit_nic.iface + modes = ['DHCP (auto detect)', 'IP (static)'] + default_mode = 'DHCP (auto detect)' + + prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) + mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() + + if mode.value == 'IP (static)': + while 1: + prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) + ip = TextInput(prompt, edit_nic.ip).run().strip() + # Implemented new check for correct IP/subnet input + try: + ipaddress.ip_interface(ip) + break + except ValueError: + warn("You need to enter a valid IP in IP-config mode") + + # Implemented new check for correct gateway IP address + gateway = None + + while 1: + gateway = TextInput( + _('Enter your gateway (router) IP address or leave blank for none: '), + edit_nic.gateway + ).run().strip() + try: + if len(gateway) > 0: + ipaddress.ip_address(gateway) + break + except ValueError: + warn("You need to enter a valid gateway (router) IP address") + + if edit_nic.dns: + display_dns = ' '.join(edit_nic.dns) + else: + display_dns = None + dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() + + dns = [] + if len(dns_input): + dns = dns_input.split(' ') + + return Nic(iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) + else: + # this will contain network iface names + return Nic(iface=iface_name) + + +def ask_to_configure_network(preset: Optional[NetworkConfiguration]) -> Optional[NetworkConfiguration]: + """ + Configure the network on the newly installed system + """ + options = {n.display_msg(): n for n in NicType} + preset_val = preset.type.display_msg() if preset else None + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select one network interface to configure'), + list(options.keys()), + preset_values=preset_val, + sort=False, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + case MenuSelectionType.Selection: + nic_type = options[choice.single_value] + + match nic_type: + case NicType.ISO: + return NetworkConfiguration(NicType.ISO) + case NicType.NM: + return NetworkConfiguration(NicType.NM) + case NicType.MANUAL: + preset_nics = preset.nics if preset else [] + nics = ManualNetworkConfig('Configure interfaces', preset_nics).run() + if nics: + return NetworkConfiguration(NicType.MANUAL, nics) + + return preset diff --git a/archinstall/lib/models/__init__.py b/archinstall/lib/models/__init__.py index 8cc49ea0..7415f63f 100644 --- a/archinstall/lib/models/__init__.py +++ b/archinstall/lib/models/__init__.py @@ -1,4 +1,8 @@ -from .network_configuration import NetworkConfiguration, NicType, NetworkConfigurationHandler +from .network_configuration import ( + NetworkConfiguration, + NicType, + Nic +) from .bootloader import Bootloader from .gen import VersionDef, PackageSearchResult, PackageSearch, LocalPackage from .users import PasswordStrength, User diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index e564b97b..fac7bbef 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -2,56 +2,64 @@ from __future__ import annotations from dataclasses import dataclass, field from enum import Enum -from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING, Tuple +from typing import List, Optional, Dict, Any, TYPE_CHECKING, Tuple -from ..output import debug from ..profile import ProfileConfiguration if TYPE_CHECKING: _: Any -class NicType(str, Enum): +class NicType(Enum): ISO = "iso" NM = "nm" MANUAL = "manual" + def display_msg(self) -> str: + match self: + case NicType.ISO: + return str(_('Copy ISO network configuration to installation')) + case NicType.NM: + return str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')) + case NicType.MANUAL: + return str(_('Manual configuration')) + @dataclass -class NetworkConfiguration: - type: NicType +class Nic: iface: Optional[str] = None ip: Optional[str] = None dhcp: bool = True gateway: Optional[str] = None dns: List[str] = field(default_factory=list) - def __str__(self): - if self.is_iso(): - return "Copy ISO configuration" - elif self.is_network_manager(): - return "Use NetworkManager" - elif self.is_manual(): - if self.dhcp: - return f'iface={self.iface}, dhcp=auto' - else: - return f'iface={self.iface}, ip={self.ip}, dhcp=staticIp, gateway={self.gateway}, dns={self.dns}' - else: - return 'Unknown type' - def table_data(self) -> Dict[str, Any]: - exclude_fields = ['type'] - data = {} - for k, v in self.__dict__.items(): - if k not in exclude_fields: - if isinstance(v, list) and len(v) == 0: - v = '' - elif v is None: - v = '' - - data[k] = v - - return data + return { + 'iface': self.iface if self.iface else '', + 'ip': self.ip if self.ip else '', + 'dhcp': self.dhcp, + 'gateway': self.gateway if self.gateway else '', + 'dns': self.dns + } + + def __dump__(self) -> Dict[str, Any]: + return { + 'iface': self.iface, + 'ip': self.ip, + 'dhcp': self.dhcp, + 'gateway': self.gateway, + 'dns': self.dns + } + + @staticmethod + def parse_arg(arg: Dict[str, Any]) -> Nic: + return Nic( + iface=arg.get('iface', None), + ip=arg.get('ip', None), + dhcp=arg.get('dhcp', True), + gateway=arg.get('gateway', None), + dns=arg.get('dns', []), + ) def as_systemd_config(self) -> str: match: List[Tuple[str, str]] = [] @@ -80,107 +88,57 @@ class NetworkConfiguration: return config_str - def json(self) -> Dict: - # for json serialization when calling json.dumps(...) on this class - return self.__dict__ - - def is_iso(self) -> bool: - return self.type == NicType.ISO - - def is_network_manager(self) -> bool: - return self.type == NicType.NM - - def is_manual(self) -> bool: - return self.type == NicType.MANUAL - - -class NetworkConfigurationHandler: - def __init__(self, config: Union[None, NetworkConfiguration, List[NetworkConfiguration]] = None): - self._configuration = config - - @property - def configuration(self): - return self._configuration - def config_installer( +@dataclass +class NetworkConfiguration: + type: NicType + nics: List[Nic] = field(default_factory=list) + + def __dump__(self) -> Dict[str, Any]: + config: Dict[str, Any] = {'type': self.type.value} + if self.nics: + config['nics'] = [n.__dump__() for n in self.nics] + + return config + + @staticmethod + def parse_arg(config: Dict[str, Any]) -> Optional[NetworkConfiguration]: + nic_type = config.get('type', None) + if not nic_type: + return None + + match NicType(nic_type): + case NicType.ISO: + return NetworkConfiguration(NicType.ISO) + case NicType.NM: + return NetworkConfiguration(NicType.NM) + case NicType.MANUAL: + nics_arg = config.get('nics', []) + if nics_arg: + nics = [Nic.parse_arg(n) for n in nics_arg] + return NetworkConfiguration(NicType.MANUAL, nics) + + return None + + def install_network_config( self, installation: Any, profile_config: Optional[ProfileConfiguration] = None ): - if self._configuration is None: - return - - if isinstance(self._configuration, list): - for config in self._configuration: - installation.configure_nic(config) - - installation.enable_service('systemd-networkd') - installation.enable_service('systemd-resolved') - else: - # If user selected to copy the current ISO network configuration - # Perform a copy of the config - if self._configuration.is_iso(): + match self.type: + case NicType.ISO: installation.copy_iso_network_config( - enable_services=True # Sources the ISO network configuration to the install medium. + enable_services=True # Sources the ISO network configuration to the install medium. ) - elif self._configuration.is_network_manager(): + case NicType.NM: installation.add_additional_packages(["networkmanager"]) if profile_config and profile_config.profile: if profile_config.profile.is_desktop_type_profile(): installation.add_additional_packages(["network-manager-applet"]) installation.enable_service('NetworkManager.service') + case NicType.MANUAL: + for nic in self.nics: + installation.configure_nic(nic) - def _parse_manual_config(self, configs: List[Dict[str, Any]]) -> Optional[List[NetworkConfiguration]]: - configurations = [] - - for manual_config in configs: - iface = manual_config.get('iface', None) - - if iface is None: - raise ValueError('No iface specified for manual configuration') - - if manual_config.get('dhcp', False) or not any([manual_config.get(v, '') for v in ['ip', 'gateway', 'dns']]): - configurations.append( - NetworkConfiguration(NicType.MANUAL, iface=iface) - ) - else: - ip = manual_config.get('ip', '') - if not ip: - raise ValueError('Manual nic configuration with no auto DHCP requires an IP address') - - dns = manual_config.get('dns', []) - if not isinstance(dns, list): - dns = [dns] - - configurations.append( - NetworkConfiguration( - NicType.MANUAL, - iface=iface, - ip=ip, - gateway=manual_config.get('gateway', ''), - dns=dns, - dhcp=False - ) - ) - - return configurations - - def _parse_nic_type(self, nic_type: str) -> NicType: - try: - return NicType(nic_type) - except ValueError: - options = [e.value for e in NicType] - raise ValueError(f'Unknown nic type: {nic_type}. Possible values are {options}') - - def parse_arguments(self, config: Any): - if isinstance(config, list): # new data format - self._configuration = self._parse_manual_config(config) - elif nic_type := config.get('type', None): # new data format - type_ = self._parse_nic_type(nic_type) - - if type_ != NicType.MANUAL: - self._configuration = NetworkConfiguration(type_) - else: # manual configuration settings - self._configuration = self._parse_manual_config([config]) - else: - debug(f'Unable to parse network configuration: {config}') + installation.enable_service('systemd-networkd') + installation.enable_service('systemd-resolved') diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 7f9b9fd6..c8df590d 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, Optional import archinstall from archinstall import info, debug @@ -14,7 +14,7 @@ from archinstall.lib.installer import Installer from archinstall.lib.menu import Menu from archinstall.lib.mirrors import use_mirrors, add_custom_mirrors from archinstall.lib.models.bootloader import Bootloader -from archinstall.lib.models.network_configuration import NetworkConfigurationHandler +from archinstall.lib.models.network_configuration import NetworkConfiguration from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler @@ -82,7 +82,7 @@ def ask_user_questions(): global_menu.enable('parallel downloads') # Ask or Call the helper function that asks the user to optionally configure a network. - global_menu.enable('nic') + global_menu.enable('network_config') global_menu.enable('timezone') @@ -158,11 +158,10 @@ def perform_installation(mountpoint: Path): # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config = archinstall.arguments.get('nic', None) + network_config: Optional[NetworkConfiguration] = archinstall.arguments.get('network_config', None) if network_config: - handler = NetworkConfigurationHandler(network_config) - handler.config_installer( + network_config.install_network_config( installation, archinstall.arguments.get('profile_config', None) ) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 375458a1..a2ab0549 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -95,7 +95,7 @@ class SwissMainMenu(GlobalMenu): options_list = [ 'mirror_config', 'disk_config', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', - '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', + '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] @@ -110,7 +110,7 @@ class SwissMainMenu(GlobalMenu): options_list = [ 'mirror_config','bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', - 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' + 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] mandatory_list = ['hostname'] @@ -222,11 +222,10 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config = archinstall.arguments.get('nic', None) + network_config = archinstall.arguments.get('network_config', None) if network_config: - handler = models.NetworkConfigurationHandler(network_config) - handler.config_installer( + network_config.install_network_config( installation, archinstall.arguments.get('profile_config', None) ) diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index 4cb07ae1..0a075282 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -29,7 +29,7 @@ To start the installer, run the following in the latest Arch Linux ISO: .. code-block:: sh archinstall --script guided - + | The ``--script guided`` argument is optional as it's the default behavior. | But this will use our most guided installation and if you skip all the option steps it will install a minimal Arch Linux experience. @@ -49,7 +49,7 @@ There are three different configuration files, all of which are optional. .. note:: You can always get the latest options with ``archinstall --dry-run``, but edit the following json according to your needs. Save the configuration as a ``.json`` file. Archinstall can source it via a local or remote path (URL) - + .. code-block:: json { @@ -72,8 +72,8 @@ There are three different configuration files, all of which are optional. ], "keyboard-language": "us", "mirror-region": "Worldwide", - "nic": { - "type": "NM" + "network_config": { + "type": "nm" }, "ntp": true, "packages": ["docker", "git", "wget", "zsh"], diff --git a/examples/config-sample.json b/examples/config-sample.json index a7c5d537..38415b2c 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -99,13 +99,19 @@ "http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch": true, } }, - "nic": { - "dhcp": true, - "dns": null, - "gateway": null, - "iface": null, - "ip": null, - "type": "nm" + "network_config": { + "nics": [ + { + "dhcp": false, + "dns": [ + "3.3.3.3" + ], + "gateway": "2.2.2.2", + "iface": "enp0s31f6", + "ip": "1.1.1.1" + } + ], + "type": "manual" }, "no_pkg_lookups": false, "ntp": true, diff --git a/examples/custom-command-sample.json b/examples/custom-command-sample.json index 8d8d611d..b2250e2c 100644 --- a/examples/custom-command-sample.json +++ b/examples/custom-command-sample.json @@ -12,8 +12,8 @@ ], "keyboard-layout": "us", "mirror-region": "Worldwide", - "nic": { - "type": "NM" + "network_config": { + "type": "nm" }, "ntp": true, "packages": ["docker", "git", "wget", "zsh"], diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index ce1a80ec..8e82ca7e 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -61,7 +61,7 @@ def ask_user_questions(): global_menu.enable('parallel downloads') # Ask or Call the helper function that asks the user to optionally configure a network. - global_menu.enable('nic') + global_menu.enable('network_config') global_menu.enable('timezone') @@ -137,11 +137,10 @@ def perform_installation(mountpoint: Path): # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config = archinstall.arguments.get('nic', None) + network_config = archinstall.arguments.get('network_config', None) if network_config: - handler = models.NetworkConfigurationHandler(network_config) - handler.config_installer( + network_config.install_network_config( installation, archinstall.arguments.get('profile_config', None) ) diff --git a/schema.json b/schema.json index 0a41ebf0..b74588a1 100644 --- a/schema.json +++ b/schema.json @@ -69,21 +69,26 @@ "description": "By default, it will autodetect the best region. Enter a region or a dictionary of regions and mirrors to use specific ones", "type": "object" }, - "nic": { + "network_config": { "description": "Choose between NetworkManager, manual configuration, use systemd-networkd from the ISO or no configuration", "type": "object", "properties": { "type": "string", - "iface": "string", - "dhcp": "boolean", - "ip": "string", - "gateway": "string", - "dns": { - "description": "List of DNS servers", + "nics": [ "type": "array", "items": { - "type": "string" - } + "iface": "string", + "dhcp": "boolean", + "ip": "string", + "gateway": "string", + "dns": { + "description": "List of DNS servers", + "type": "array", + "items": { + "type": "string" + } + } + ] } } }, -- cgit v1.2.3-54-g00ecf From 439bb5428bb6a6f512f695a83ee6b3b8f6537598 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 25 Jul 2023 19:17:09 +1000 Subject: Fix 1934 - audio server regression (#1946) * Audio configuration * Update * Update schema --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 3 ++ archinstall/lib/global_menu.py | 27 ++++++++------ archinstall/lib/interactions/general_conf.py | 30 +++++++++++---- archinstall/lib/models/__init__.py | 1 + archinstall/lib/models/audio_configuration.py | 54 +++++++++++++++++++++++++++ archinstall/scripts/guided.py | 19 +++------- archinstall/scripts/swiss.py | 19 ++++------ docs/installing/guided.rst | 2 +- examples/config-sample.json | 22 ++++------- examples/custom-command-sample.json | 1 - examples/interactive_installation.py | 22 +++-------- schema.json | 19 ++++++---- 12 files changed, 137 insertions(+), 82 deletions(-) create mode 100644 archinstall/lib/models/audio_configuration.py (limited to 'archinstall/scripts') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index c4b64912..cfaecd16 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -237,6 +237,9 @@ def load_config(): if arguments.get('bootloader', None) is not None: arguments['bootloader'] = models.Bootloader.from_arg(arguments['bootloader']) + if arguments.get('audio_config', None) is not None: + arguments['audio_config'] = models.AudioConfiguration.parse_arg(arguments['audio_config']) + if arguments.get('disk_encryption', None) is not None and disk_config is not None: password = arguments.get('encryption_password', '') arguments['disk_encryption'] = disk.DiskEncryption.parse_arg( diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 5503d9ce..fb62b7b5 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING +from typing import Any, List, Optional, Dict, TYPE_CHECKING from . import disk from .general import secret @@ -9,6 +9,7 @@ from .menu import Selector, AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu from .models import NetworkConfiguration, NicType from .models.bootloader import Bootloader +from .models.audio_configuration import Audio, AudioConfiguration from .models.users import User from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration @@ -109,12 +110,11 @@ class GlobalMenu(AbstractMenu): display_func=lambda x: x.profile.name if x else '', preview_func=self._prev_profile ) - self._menu_options['audio'] = \ + self._menu_options['audio_config'] = \ Selector( _('Audio'), lambda preset: self._select_audio(preset), - display_func=lambda x: x if x else '', - default=None + display_func=lambda x: self._display_audio(x) ) self._menu_options['parallel downloads'] = \ Selector( @@ -421,13 +421,18 @@ class GlobalMenu(AbstractMenu): profile_config = ProfileMenu(store, preset=current_profile).run() return profile_config - def _select_audio(self, current: Union[str, None]) -> Optional[str]: - profile_config: Optional[ProfileConfiguration] = self._menu_options['profile_config'].current_selection - if profile_config and profile_config.profile: - is_desktop = profile_config.profile.is_desktop_profile() if profile_config else False - selection = ask_for_audio_selection(is_desktop, current) - return selection - return None + def _select_audio( + self, + current: Optional[AudioConfiguration] = None + ) -> Optional[AudioConfiguration]: + selection = ask_for_audio_selection(current) + return selection + + def _display_audio(self, current: Optional[AudioConfiguration]) -> str: + if not current: + return Audio.no_audio_text() + else: + return current.audio.name def _create_user_account(self, defined_users: List[User]) -> List[User]: users = ask_for_additional_users(defined_users=defined_users) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index ad9ee386..1c570a69 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -5,6 +5,7 @@ from typing import List, Any, Optional, TYPE_CHECKING from ..locale import list_timezones, list_keyboard_languages from ..menu import MenuSelectionType, Menu, TextInput +from ..models.audio_configuration import Audio, AudioConfiguration from ..output import warn from ..packages.packages import validate_package_list from ..storage import storage @@ -55,16 +56,31 @@ def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: return None -def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]: - no_audio = str(_('No audio server')) - choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] - default = 'pipewire' if desktop else no_audio +def ask_for_audio_selection( + current: Optional[AudioConfiguration] = None +) -> Optional[AudioConfiguration]: + choices = [ + Audio.Pipewire.name, + Audio.Pulseaudio.name, + Audio.no_audio_text() + ] - choice = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=default).run() + preset = current.audio.name if current else None + + choice = Menu( + _('Choose an audio server'), + choices, + preset_values=preset + ).run() match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return current + case MenuSelectionType.Selection: + value = choice.single_value + if value == Audio.no_audio_text(): + return None + else: + return AudioConfiguration(Audio[value]) return None diff --git a/archinstall/lib/models/__init__.py b/archinstall/lib/models/__init__.py index 7415f63f..a1c90e48 100644 --- a/archinstall/lib/models/__init__.py +++ b/archinstall/lib/models/__init__.py @@ -6,3 +6,4 @@ from .network_configuration import ( from .bootloader import Bootloader from .gen import VersionDef, PackageSearchResult, PackageSearch, LocalPackage from .users import PasswordStrength, User +from .audio_configuration import Audio, AudioConfiguration diff --git a/archinstall/lib/models/audio_configuration.py b/archinstall/lib/models/audio_configuration.py new file mode 100644 index 00000000..3a4029db --- /dev/null +++ b/archinstall/lib/models/audio_configuration.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Any, TYPE_CHECKING, Dict + +from ..hardware import SysInfo +from ..output import info +from ...default_profiles.applications.pipewire import PipewireProfile + +if TYPE_CHECKING: + _: Any + + +@dataclass +class Audio(Enum): + Pipewire = 'pipewire' + Pulseaudio = 'pulseaudio' + + @staticmethod + def no_audio_text() -> str: + return str(_('No audio server')) + + +@dataclass +class AudioConfiguration: + audio: Audio + + def __dump__(self) -> Dict[str, Any]: + return { + 'audio': self.audio.value + } + + @staticmethod + def parse_arg(arg: Dict[str, Any]) -> 'AudioConfiguration': + return AudioConfiguration( + Audio(arg['audio']) + ) + + def install_audio_config( + self, + installation: Any + ): + info(f'Installing audio server: {self.audio.name}') + + match self.audio: + case Audio.Pipewire: + PipewireProfile().install(installation) + case Audio.Pulseaudio: + installation.add_additional_packages("pulseaudio") + + if SysInfo.requires_sof_fw(): + installation.add_additional_packages('sof-firmware') + + if SysInfo.requires_alsa_fw(): + installation.add_additional_packages('alsa-firmware') diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index c8df590d..605d2b0e 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -8,11 +8,11 @@ from archinstall import SysInfo from archinstall.lib import locale from archinstall.lib import disk from archinstall.lib.global_menu import GlobalMenu -from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.installer import Installer from archinstall.lib.menu import Menu from archinstall.lib.mirrors import use_mirrors, add_custom_mirrors +from archinstall.lib.models import AudioConfiguration from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.network_configuration import NetworkConfiguration from archinstall.lib.networking import check_mirror_reachable @@ -70,7 +70,7 @@ def ask_user_questions(): global_menu.enable('profile_config') # Ask about audio server selection if one is not already set - global_menu.enable('audio') + global_menu.enable('audio_config') # Ask for preferred kernel: global_menu.enable('kernels', mandatory=True) @@ -172,18 +172,9 @@ def perform_installation(mountpoint: Path): if users := archinstall.arguments.get('!users', None): installation.create_users(users) - if audio := archinstall.arguments.get('audio', None): - info(f'Installing audio server: {audio}') - if audio == 'pipewire': - PipewireProfile().install(installation) - elif audio == 'pulseaudio': - installation.add_additional_packages("pulseaudio") - - if SysInfo.requires_sof_fw(): - installation.add_additional_packages('sof-firmware') - - if SysInfo.requires_alsa_fw(): - installation.add_additional_packages('alsa-firmware') + audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None) + if audio_config: + audio_config.install_audio_config(installation) else: info("No audio server will be installed") diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index a2ab0549..cd532f6d 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -1,7 +1,7 @@ import os from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict +from typing import TYPE_CHECKING, Any, Dict, Optional import archinstall from archinstall import SysInfo, info, debug @@ -9,13 +9,13 @@ from archinstall.lib import mirrors from archinstall.lib import models from archinstall.lib import disk from archinstall.lib import locale +from archinstall.lib.models import AudioConfiguration from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib import menu from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer from archinstall.lib.configuration import ConfigurationOutput -from archinstall.default_profiles.applications.pipewire import PipewireProfile if TYPE_CHECKING: _: Any @@ -95,7 +95,7 @@ class SwissMainMenu(GlobalMenu): options_list = [ 'mirror_config', 'disk_config', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', - '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'network_config', + '!users', 'profile_config', 'audio_config', 'kernels', 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] @@ -109,7 +109,7 @@ class SwissMainMenu(GlobalMenu): case ExecutionMode.Only_OS: options_list = [ 'mirror_config','bootloader', 'hostname', - '!root-password', '!users', 'profile_config', 'audio', 'kernels', + '!root-password', '!users', 'profile_config', 'audio_config', 'kernels', 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] @@ -236,14 +236,11 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): if users := archinstall.arguments.get('!users', None): installation.create_users(users) - if audio := archinstall.arguments.get('audio', None): - info(f'Installing audio server: {audio}') - if audio == 'pipewire': - PipewireProfile().install(installation) - elif audio == 'pulseaudio': - installation.add_additional_packages("pulseaudio") + audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None) + if audio_config: + audio_config.install_audio_config(installation) else: - info("No audio server will be installed.") + info("No audio server will be installed") if profile_config := archinstall.arguments.get('profile_config', None): profile_handler.install_profile_config(installation, profile_config) diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index 0a075282..c5e7f1ed 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -53,7 +53,7 @@ There are three different configuration files, all of which are optional. .. code-block:: json { - "audio": "pipewire", + "audio_config": {"audio": "pipewire"}, "bootloader": "systemd-bootctl", "custom-commands": [ "cd /home/devel; git clone https://aur.archlinux.org/paru.git", diff --git a/examples/config-sample.json b/examples/config-sample.json index 38415b2c..ed1cc38e 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -2,7 +2,7 @@ "config_version": "2.5.2", "additional-repositories": [], "archinstall-language": "English", - "audio": "pipewire", + "audio_config": {"audio": "pipewire"}, "bootloader": "Systemd-boot", "debug": false, "disk_config": { @@ -99,19 +99,13 @@ "http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch": true, } }, - "network_config": { - "nics": [ - { - "dhcp": false, - "dns": [ - "3.3.3.3" - ], - "gateway": "2.2.2.2", - "iface": "enp0s31f6", - "ip": "1.1.1.1" - } - ], - "type": "manual" + "nic": { + "dhcp": true, + "dns": null, + "gateway": null, + "iface": null, + "ip": null, + "type": "nm" }, "no_pkg_lookups": false, "ntp": true, diff --git a/examples/custom-command-sample.json b/examples/custom-command-sample.json index b2250e2c..34d63d74 100644 --- a/examples/custom-command-sample.json +++ b/examples/custom-command-sample.json @@ -1,6 +1,5 @@ { "dry_run": true, - "audio": "none", "bootloader": "systemd-bootctl", "debug": false, "harddrives": [ diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 8e82ca7e..e075df9b 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -1,12 +1,11 @@ from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Optional import archinstall from archinstall import Installer from archinstall import profile from archinstall import SysInfo from archinstall import mirrors -from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall import disk from archinstall import menu from archinstall import models @@ -49,7 +48,7 @@ def ask_user_questions(): global_menu.enable('profile_config') # Ask about audio server selection if one is not already set - global_menu.enable('audio') + global_menu.enable('audio_config') # Ask for preferred kernel: global_menu.enable('kernels', mandatory=True) @@ -151,20 +150,11 @@ def perform_installation(mountpoint: Path): if users := archinstall.arguments.get('!users', None): installation.create_users(users) - if audio := archinstall.arguments.get('audio', None): - info(f'Installing audio server: {audio}') - if audio == 'pipewire': - PipewireProfile().install(installation) - elif audio == 'pulseaudio': - installation.add_additional_packages("pulseaudio") - - if SysInfo.requires_sof_fw(): - installation.add_additional_packages('sof-firmware') - - if SysInfo.requires_alsa_fw(): - installation.add_additional_packages('alsa-firmware') + audio_config: Optional[models.AudioConfiguration] = archinstall.arguments.get('audio_config', None) + if audio_config: + audio_config.install_audio_config(installation) else: - info("No audio server will be installed.") + info("No audio server will be installed") if profile_config := archinstall.arguments.get('profile_config', None): profile.profile_handler.install_profile_config(installation, profile_config) diff --git a/schema.json b/schema.json index b74588a1..5616ed41 100644 --- a/schema.json +++ b/schema.json @@ -12,14 +12,19 @@ "testing" ] }, - "audio": { + "audio_config": { "description": "Audio server to be installed", - "type": "string", - "enum": [ - "pipewire", - "pulseaudio", - "none" - ] + "type": "object", + "properties": { + "audio": { + "description": "Audio server to be installed", + "type": "string", + "enum": [ + "pipewire", + "pulseaudio" + ] + } + }, }, "bootloader": { "description": "Bootloader to be installed", -- cgit v1.2.3-54-g00ecf From 3ba1a3718e3187eafdaaaab4c1ad6551408cc701 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Thu, 14 Sep 2023 19:59:32 +1000 Subject: Fix 1973 - pipewire (#2030) Co-authored-by: Daniel Girtler --- archinstall/scripts/guided.py | 6 +++--- archinstall/scripts/swiss.py | 6 +++--- examples/interactive_installation.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) (limited to 'archinstall/scripts') diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 605d2b0e..51549fa8 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -166,9 +166,6 @@ def perform_installation(mountpoint: Path): archinstall.arguments.get('profile_config', None) ) - if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', None)) - if users := archinstall.arguments.get('!users', None): installation.create_users(users) @@ -178,6 +175,9 @@ def perform_installation(mountpoint: Path): 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 profile_config := archinstall.arguments.get('profile_config', None): profile_handler.install_profile_config(installation, profile_config) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index cd532f6d..80fa0a48 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -230,9 +230,6 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): archinstall.arguments.get('profile_config', None) ) - if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', [])) - if users := archinstall.arguments.get('!users', None): installation.create_users(users) @@ -242,6 +239,9 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): 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', [])) + if profile_config := archinstall.arguments.get('profile_config', None): profile_handler.install_profile_config(installation, profile_config) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index e075df9b..9eac029c 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -144,9 +144,6 @@ def perform_installation(mountpoint: Path): archinstall.arguments.get('profile_config', None) ) - if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', [])) - if users := archinstall.arguments.get('!users', None): installation.create_users(users) @@ -156,6 +153,9 @@ def perform_installation(mountpoint: Path): 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', [])) + if profile_config := archinstall.arguments.get('profile_config', None): profile.profile_handler.install_profile_config(installation, profile_config) -- cgit v1.2.3-54-g00ecf From 12b7017240a040fd4fbebf7c5794a1ca5560f0ea Mon Sep 17 00:00:00 2001 From: Alexander Seiler Date: Mon, 18 Sep 2023 14:04:36 +0200 Subject: Fix many typos (#1692) Signed-off-by: Alexander Seiler --- archinstall/__init__.py | 2 +- archinstall/default_profiles/profile.py | 6 +++--- archinstall/lib/disk/device_model.py | 2 +- archinstall/lib/disk/fido.py | 2 +- archinstall/lib/general.py | 2 +- archinstall/lib/global_menu.py | 4 ++-- archinstall/lib/installer.py | 10 +++++----- archinstall/lib/interactions/__init__.py | 2 +- archinstall/lib/interactions/general_conf.py | 2 +- archinstall/lib/menu/list_manager.py | 2 +- archinstall/lib/menu/menu.py | 2 +- archinstall/lib/output.py | 2 +- archinstall/lib/translationhandler.py | 2 +- archinstall/scripts/guided.py | 4 ++-- archinstall/scripts/swiss.py | 6 +++--- examples/interactive_installation.py | 4 ++-- 16 files changed, 27 insertions(+), 27 deletions(-) (limited to 'archinstall/scripts') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 39588904..7645ae39 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -43,7 +43,7 @@ __version__ = "2.6.0" storage['__version__'] = __version__ -# add the custome _ as a builtin, it can now be used anywhere in the +# add the custom _ as a builtin, it can now be used anywhere in the # project to mark strings as translatable with _('translate me') DeferredTranslation.install() diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index 982bd5a3..49a9c19d 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -81,7 +81,7 @@ class Profile: def packages(self) -> List[str]: """ Returns a list of packages that should be installed when - this profile is among the choosen ones + this profile is among the chosen ones """ return self._packages @@ -128,7 +128,7 @@ class Profile: """ Set the custom settings for the profile. This is also called when the settings are parsed from the config - and can be overriden to perform further actions based on the profile + and can be overridden to perform further actions based on the profile """ self.custom_settings = settings @@ -179,7 +179,7 @@ class Profile: def preview_text(self) -> Optional[str]: """ Used for preview text in profiles_bck. If a description is set for a - profile it will automatically display that one in the preivew. + profile it will automatically display that one in the preview. If no preview or a different text should be displayed just """ if self.description: diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 6eeb0d91..69038b01 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -202,7 +202,7 @@ class Size: # not sure why we would ever wanna convert to percentages if target_unit == Unit.Percent and total_size is None: - raise ValueError('Missing paramter total size to be able to convert to percentage') + raise ValueError('Missing parameter total size to be able to convert to percentage') if self.unit == target_unit: return self diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 96a74991..9eeba56a 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -34,7 +34,7 @@ class Fido2: /dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID """ - # to prevent continous reloading which will slow + # to prevent continuous reloading which will slow # down moving the cursor in the menu if not cls._loaded or reload: try: diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 611378ee..e22e7eed 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: def generate_password(length :int = 64) -> str: - haystack = string.printable # digits, ascii_letters, punctiation (!"#$[] etc) and whitespace + haystack = string.printable # digits, ascii_letters, punctuation (!"#$[] etc) and whitespace return ''.join(secrets.choice(haystack) for i in range(length)) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index fb62b7b5..b38dac0b 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -15,7 +15,7 @@ from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration from .storage import storage from .configuration import save_config -from .interactions import add_number_of_parrallel_downloads +from .interactions import add_number_of_parallel_downloads from .interactions import ask_additional_packages_to_install from .interactions import ask_for_additional_users from .interactions import ask_for_audio_selection @@ -119,7 +119,7 @@ class GlobalMenu(AbstractMenu): self._menu_options['parallel downloads'] = \ Selector( _('Parallel Downloads'), - lambda preset: add_number_of_parrallel_downloads(preset), + lambda preset: add_number_of_parallel_downloads(preset), display_func=lambda x: x if x else '0', default=0 ) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 09e91ab8..34c9441f 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -194,7 +194,7 @@ class Installer: for part_mod in sorted_part_mods: if luks_handler := luks_handlers.get(part_mod): # mount encrypted partition - self._mount_luks_partiton(part_mod, luks_handler) + self._mount_luks_partition(part_mod, luks_handler) else: # partition is not encrypted self._mount_partition(part_mod) @@ -219,7 +219,7 @@ class Installer: if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.dev_path: self._mount_btrfs_subvol(part_mod.dev_path, part_mod.btrfs_subvols) - def _mount_luks_partiton(self, part_mod: disk.PartitionModification, luks_handler: Luks2): + def _mount_luks_partition(self, part_mod: disk.PartitionModification, luks_handler: Luks2): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined if part_mod.mountpoint and luks_handler.mapper_dev: target = self.target / part_mod.relative_mountpoint @@ -315,7 +315,7 @@ class Installer: raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {err}') if not gen_fstab: - raise RequirementError(f'Genrating fstab returned empty value') + raise RequirementError(f'Generating fstab returned empty value') with open(fstab_path, 'a') as fp: fp.write(gen_fstab) @@ -434,7 +434,7 @@ class Installer: return False - def activate_time_syncronization(self) -> None: + def activate_time_synchronization(self) -> None: info('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers') self.enable_service('systemd-timesyncd') @@ -1008,7 +1008,7 @@ When = PostTransaction Exec = /bin/sh -c \\"/usr/bin/limine bios-install /dev/disk/by-uuid/{root_uuid} && /usr/bin/cp /usr/share/limine/limine-bios.sys /boot/\\" """) - # Limine does not ship with a default configuation file. We are going to + # Limine does not ship with a default configuration file. We are going to # create a basic one that is similar to the one GRUB generates. try: config = f""" diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 53be8e7a..50c0012d 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -11,7 +11,7 @@ from .disk_conf import ( from .general_conf import ( ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_archinstall_language, ask_additional_packages_to_install, - add_number_of_parrallel_downloads, select_additional_repositories + add_number_of_parallel_downloads, select_additional_repositories ) from .system_conf import ( diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 14fcc3f8..8dd6e94f 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -164,7 +164,7 @@ def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]: return packages -def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Optional[int]: +def add_number_of_parallel_downloads(input_number :Optional[int] = None) -> Optional[int]: max_recommended = 5 print(_(f"This option enables the number of parallel downloads that can occur during package downloads")) print(_("Enter the number of parallel downloads to be enabled.\n\nNote:\n")) diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index be31fdf0..54fb6a1b 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -80,7 +80,7 @@ class ListManager: self._data = self.handle_action(choice.value, None, self._data) elif choice.value in self._terminate_actions: break - else: # an entry of the existing selection was choosen + else: # an entry of the existing selection was chosen selected_entry = data_formatted[choice.value] # type: ignore self._run_actions_on_entry(selected_entry) diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 358ba5e4..3bd31b88 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -123,7 +123,7 @@ class Menu(TerminalMenu): :param allow_reset: This will explicitly handle a ctrl+c instead and return that specific state :type allow_reset: bool - param allow_reset_warning_msg: If raise_error_on_interrupt is True the warnign is set, a user confirmation is displayed + param allow_reset_warning_msg: If raise_error_on_interrupt is True the warning is set, a user confirmation is displayed type allow_reset_warning_msg: str :param extra_bottom_space: Add an extra empty line at the end of the menu diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index 63d9c1fb..62a1ba27 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -22,7 +22,7 @@ class FormattedOutput: ) -> Dict[str, Any]: """ the original values returned a dataclass as dict thru the call to some specific methods - this version allows thru the parameter class_formatter to call a dynamicly selected formatting method. + this version allows thru the parameter class_formatter to call a dynamically selected formatting method. Can transmit a filter list to the class_formatter, """ if class_formatter: diff --git a/archinstall/lib/translationhandler.py b/archinstall/lib/translationhandler.py index a2e44065..33230562 100644 --- a/archinstall/lib/translationhandler.py +++ b/archinstall/lib/translationhandler.py @@ -138,7 +138,7 @@ class TranslationHandler: def get_language_by_abbr(self, abbr: str) -> Language: """ - Get a language object by its abbrevation, e.g. en + Get a language object by its abbreviation, e.g. en """ try: return next(filter(lambda x: x.abbr == abbr, self._translated_languages)) diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 51549fa8..d7cf16cd 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -185,7 +185,7 @@ def perform_installation(mountpoint: Path): installation.set_timezone(timezone) if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() + installation.activate_time_synchronization() if archinstall.accessibility_tools_in_use(): installation.enable_espeakup() @@ -193,7 +193,7 @@ def perform_installation(mountpoint: Path): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) - # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # This step must be after profile installs to allow profiles_bck to install language pre-requisites. # After which, this step will set the language both for console and x11 if x11 was installed for instance. installation.set_keyboard_language(locale_config.kb_layout) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 80fa0a48..c04ccca4 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -54,7 +54,7 @@ class SetupMenu(GlobalMenu): super().setup_selection_menu_options() self._menu_options['mode'] = menu.Selector( - 'Excution mode', + 'Execution mode', lambda x : select_mode(), display_func=lambda x: x.value if x else '', default=ExecutionMode.Full) @@ -249,7 +249,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): installation.set_timezone(timezone) if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() + installation.activate_time_synchronization() if archinstall.accessibility_tools_in_use(): installation.enable_espeakup() @@ -257,7 +257,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) - # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # This step must be after profile installs to allow profiles_bck to install language pre-requisites. # After which, this step will set the language both for console and x11 if x11 was installed for instance. installation.set_keyboard_language(locale_config.kb_layout) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 9eac029c..f8cc75fc 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -163,7 +163,7 @@ def perform_installation(mountpoint: Path): installation.set_timezone(timezone) if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() + installation.activate_time_synchronization() if archinstall.accessibility_tools_in_use(): installation.enable_espeakup() @@ -171,7 +171,7 @@ def perform_installation(mountpoint: Path): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) - # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # This step must be after profile installs to allow profiles_bck to install language pre-requisites. # After which, this step will set the language both for console and x11 if x11 was installed for instance. installation.set_keyboard_language(locale_config.kb_layout) -- cgit v1.2.3-54-g00ecf From bc3b3a35e6408144587f8c2ace95c4ac68d53bcc Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Tue, 17 Oct 2023 05:23:09 -0400 Subject: Add support for unified kernel image (#1519) --- archinstall/lib/global_menu.py | 10 ++ archinstall/lib/installer.py | 166 ++++++++++++++++++++-------- archinstall/lib/interactions/__init__.py | 2 +- archinstall/lib/interactions/system_conf.py | 16 +++ archinstall/scripts/guided.py | 9 +- schema.json | 4 + 6 files changed, 160 insertions(+), 47 deletions(-) (limited to 'archinstall/scripts') diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 86c341a7..e4aa1235 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -4,6 +4,7 @@ from typing import Any, List, Optional, Dict, TYPE_CHECKING from . import disk from .general import secret +from .hardware import SysInfo from .locale.locale_menu import LocaleConfiguration, LocaleMenu from .menu import Selector, AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu @@ -20,6 +21,7 @@ from .interactions import ask_additional_packages_to_install from .interactions import ask_for_additional_users from .interactions import ask_for_audio_selection from .interactions import ask_for_bootloader +from .interactions import ask_for_uki from .interactions import ask_for_swap from .interactions import ask_hostname from .interactions import ask_to_configure_network @@ -85,6 +87,11 @@ class GlobalMenu(AbstractMenu): lambda preset: ask_for_bootloader(preset), display_func=lambda x: x.value, default=Bootloader.get_default()) + self._menu_options['uki'] = \ + Selector( + _('Unified kernel images'), + lambda preset: ask_for_uki(preset), + default=False) self._menu_options['hostname'] = \ Selector( _('Hostname'), @@ -216,6 +223,9 @@ class GlobalMenu(AbstractMenu): self._menu_options['install'].update_description(text) def post_callback(self, name: Optional[str] = None, value: Any = None): + if not SysInfo.has_uefi(): + self._menu_options['uki'].set_enabled(False) + self._update_install_text(name, value) def _install_text(self): diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 39298204..4d6c65b3 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -542,18 +542,13 @@ class Installer: return True - def mkinitcpio(self, flags: List[str], locale_config: LocaleConfiguration) -> bool: + def mkinitcpio(self, flags: List[str]) -> bool: for plugin in plugins.values(): if hasattr(plugin, 'on_mkinitcpio'): # Allow plugins to override the usage of mkinitcpio altogether. if plugin.on_mkinitcpio(self): return True - # mkinitcpio will error out if there's no vconsole. - if (vconsole := Path(f"{self.target}/etc/vconsole.conf")).exists() is False: - with vconsole.open('w') as fh: - fh.write(f"KEYMAP={locale_config.kb_layout}\n") - with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit: mkinit.write(f"MODULES=({' '.join(self.modules)})\n") mkinit.write(f"BINARIES=({' '.join(self._binaries)})\n") @@ -587,6 +582,7 @@ class Installer: self, testing: bool = False, multilib: bool = False, + mkinitcpio: bool = True, hostname: str = 'archinstall', locale_config: LocaleConfiguration = LocaleConfiguration.default() ): @@ -674,7 +670,7 @@ class Installer: # TODO: Use python functions for this SysCommand(f'/usr/bin/arch-chroot {self.target} chmod 700 /root') - if not self.mkinitcpio(['-P'], locale_config): + if mkinitcpio and not self.mkinitcpio(['-P']): error(f"Error generating initramfs (continuing anyway)") self.helper_flags['base'] = True @@ -783,7 +779,8 @@ class Installer: self, boot_partition: disk.PartitionModification, root_partition: disk.PartitionModification, - efi_partition: Optional[disk.PartitionModification] + efi_partition: Optional[disk.PartitionModification], + uki_enabled: bool = False ): self.pacman.strap('efibootmgr') @@ -815,11 +812,18 @@ class Installer: loader_dir = self.target / 'boot/loader' loader_dir.mkdir(parents=True, exist_ok=True) + default_kernel = self.kernels[0] + if uki_enabled: + default_entry = f'arch-{default_kernel}.efi' + else: + entry_name = self.init_time + '_{kernel}{variant}.conf' + default_entry = entry_name.format(kernel=default_kernel, variant='') + + default = f'default {default_entry}' + # Modify or create a loader.conf loader_conf = loader_dir / 'loader.conf' - default = f'default {self.init_time}_{self.kernels[0]}.conf' - try: loader_data = loader_conf.read_text().splitlines() except FileNotFoundError: @@ -837,6 +841,9 @@ class Installer: loader_conf.write_text('\n'.join(loader_data) + '\n') + if uki_enabled: + return + # Ensure that the $BOOT/loader/entries/ directory exists before we try to create files in it entries_dir = loader_dir / 'entries' entries_dir.mkdir(parents=True, exist_ok=True) @@ -867,7 +874,8 @@ class Installer: options, ] - entry_conf = entries_dir / f'{self.init_time}_{kernel}{variant}.conf' + name = entry_name.format(kernel=kernel, variant=variant) + entry_conf = entries_dir / name entry_conf.write_text('\n'.join(entry) + '\n') self.helper_flags['bootloader'] = 'systemd' @@ -876,17 +884,19 @@ class Installer: self, boot_partition: disk.PartitionModification, root_partition: disk.PartitionModification, - efi_partition: Optional[disk.PartitionModification] + efi_partition: Optional[disk.PartitionModification], + uki_enabled: bool = False ): self.pacman.strap('grub') # no need? - grub_default = self.target / 'etc/default/grub' - config = grub_default.read_text() + if not uki_enabled: + grub_default = self.target / 'etc/default/grub' + config = grub_default.read_text() - kernel_parameters = ' '.join(self._get_kernel_params(root_partition, False, False)) - config = re.sub(r'(GRUB_CMDLINE_LINUX=")("\n)', rf'\1{kernel_parameters}\2', config, 1) + kernel_parameters = ' '.join(self._get_kernel_params(root_partition, False, False)) + config = re.sub(r'(GRUB_CMDLINE_LINUX=")("\n)', rf'\1{kernel_parameters}\2', config, 1) - grub_default.write_text(config) + grub_default.write_text(config) info(f"GRUB boot partition: {boot_partition.dev_path}") @@ -1067,7 +1077,8 @@ TIMEOUT=5 def _add_efistub_bootloader( self, boot_partition: disk.PartitionModification, - root_partition: disk.PartitionModification + root_partition: disk.PartitionModification, + uki_enabled: bool = False ): self.pacman.strap('efibootmgr') @@ -1078,41 +1089,103 @@ TIMEOUT=5 # points towards the same disk and/or partition. # And in which case we should do some clean up. - microcode = [] + if not uki_enabled: + loader = '/vmlinuz-{kernel}' - if ucode := self._get_microcode(): - microcode.append(f'initrd=\\{ucode}') - else: - debug('Archinstall will not add any ucode to firmware boot entry.') + microcode = [] + + if ucode := self._get_microcode(): + microcode.append(f'initrd=/{ucode}') + else: + debug('Archinstall will not add any ucode to firmware boot entry.') - kernel_parameters = self._get_kernel_params(root_partition) + entries = ( + *microcode, + 'initrd=/initramfs-{kernel}.img', + *self._get_kernel_params(root_partition) + ) + + cmdline = tuple(' '.join(entries)) + else: + loader = '/EFI/Linux/arch-{kernel}.efi' + cmdline = tuple() parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path) + cmd_template = ( + 'efibootmgr', + '--create', + '--disk', str(parent_dev_path), + '--part', str(boot_partition.partn), + '--label', 'Arch Linux ({kernel})', + '--loader', loader, + '--unicode', *cmdline, + '--verbose' + ) + for kernel in self.kernels: # Setup the firmware entry - cmdline = [ - *microcode, - f"initrd=\\initramfs-{kernel}.img", - *kernel_parameters, - ] - - cmd = [ - 'efibootmgr', - '--disk', str(parent_dev_path), - '--part', str(boot_partition.partn), - '--create', - '--label', f'Arch Linux ({kernel})', - '--loader', f"/vmlinuz-{kernel}", - '--unicode', ' '.join(cmdline), - '--verbose' - ] - + cmd = [arg.format(kernel=kernel) for arg in cmd_template] SysCommand(cmd) self.helper_flags['bootloader'] = "efistub" - def add_bootloader(self, bootloader: Bootloader): + def _config_uki( + self, + root_partition: disk.PartitionModification, + efi_partition: Optional[disk.PartitionModification] + ): + if not efi_partition or not efi_partition.mountpoint: + raise ValueError(f'Could not detect ESP at mountpoint {self.target}') + + # Set up kernel command line + with open(self.target / 'etc/kernel/cmdline', 'w') as cmdline: + kernel_parameters = self._get_kernel_params(root_partition) + cmdline.write(' '.join(kernel_parameters) + '\n') + + ucode = self._get_microcode() + + esp = efi_partition.mountpoint + + diff_mountpoint = None + if esp != Path('/efi'): + diff_mountpoint = str(esp) + + image_re = re.compile('(.+_image="/([^"]+).+\n)') + uki_re = re.compile('#((.+_uki=")/[^/]+(.+\n))') + + # Modify .preset files + for kernel in self.kernels: + preset = self.target / 'etc/mkinitcpio.d' / (kernel + '.preset') + config = preset.read_text().splitlines(True) + + for index, line in enumerate(config): + if not ucode and line.startswith('ALL_microcode='): + config[index] = '#' + line + # Avoid storing redundant image file + elif m := image_re.match(line): + image = self.target / m.group(2) + image.unlink(missing_ok=True) + config[index] = '#' + m.group(1) + elif m := uki_re.match(line): + if diff_mountpoint: + config[index] = m.group(2) + diff_mountpoint + m.group(3) + else: + config[index] = m.group(1) + elif line.startswith('#default_options='): + config[index] = line.removeprefix('#') + + preset.write_text(''.join(config)) + + # Directory for the UKIs + uki_dir = self.target / esp.relative_to(Path('/')) / 'EFI/Linux' + uki_dir.mkdir(parents=True, exist_ok=True) + + # Build the UKIs + if not self.mkinitcpio(['-P']): + error(f"Error generating initramfs (continuing anyway)") + + def add_bootloader(self, bootloader: Bootloader, uki_enabled: bool = False): """ Adds a bootloader to the installation instance. Archinstall supports one of three types: @@ -1143,13 +1216,16 @@ TIMEOUT=5 info(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}') + if uki_enabled: + self._config_uki(root_partition, efi_partition) + match bootloader: case Bootloader.Systemd: - self._add_systemd_bootloader(boot_partition, root_partition, efi_partition) + self._add_systemd_bootloader(boot_partition, root_partition, efi_partition, uki_enabled) case Bootloader.Grub: - self._add_grub_bootloader(boot_partition, root_partition, efi_partition) + self._add_grub_bootloader(boot_partition, root_partition, efi_partition, uki_enabled) case Bootloader.Efistub: - self._add_efistub_bootloader(boot_partition, root_partition) + self._add_efistub_bootloader(boot_partition, root_partition, uki_enabled) case Bootloader.Limine: self._add_limine_bootloader(boot_partition, root_partition) diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 50c0012d..4b696a78 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -15,5 +15,5 @@ from .general_conf import ( ) from .system_conf import ( - select_kernel, ask_for_bootloader, select_driver, ask_for_swap + select_kernel, ask_for_bootloader, ask_for_uki, select_driver, ask_for_swap ) diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py index 0e5e0f1e..aa72748e 100644 --- a/archinstall/lib/interactions/system_conf.py +++ b/archinstall/lib/interactions/system_conf.py @@ -65,6 +65,22 @@ def ask_for_bootloader(preset: Bootloader) -> Bootloader: return preset +def ask_for_uki(preset: bool = True) -> bool: + if preset: + preset_val = Menu.yes() + else: + preset_val = Menu.no() + + prompt = _('Would you like to use unified kernel images?') + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), preset_values=preset_val).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True + + return preset + + def select_driver(options: List[GfxDriver] = [], current_value: Optional[GfxDriver] = None) -> Optional[GfxDriver]: """ Some what convoluted function, whose job is simple. diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index d7cf16cd..fdf05c99 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -56,6 +56,8 @@ def ask_user_questions(): # Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB) global_menu.enable('bootloader') + global_menu.enable('uki') + global_menu.enable('swap') # Get the hostname for the machine @@ -111,6 +113,7 @@ def perform_installation(mountpoint: Path): # Retrieve list of additional repositories and set boolean values appropriately enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) + run_mkinitcpio = not archinstall.arguments.get('uki') locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config'] disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) @@ -141,6 +144,7 @@ def perform_installation(mountpoint: Path): installation.minimal_installation( testing=enable_testing, multilib=enable_multilib, + mkinitcpio=run_mkinitcpio, hostname=archinstall.arguments.get('hostname', 'archlinux'), locale_config=locale_config ) @@ -154,7 +158,10 @@ def perform_installation(mountpoint: Path): if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages("grub") - installation.add_bootloader(archinstall.arguments["bootloader"]) + installation.add_bootloader( + archinstall.arguments["bootloader"], + archinstall.arguments["uki"] + ) # If user selected to copy the current ISO network configuration # Perform a copy of the config diff --git a/schema.json b/schema.json index 5616ed41..b1d45f64 100644 --- a/schema.json +++ b/schema.json @@ -35,6 +35,10 @@ "efistub" ] }, + "uki": { + "description": "Set to true to use a unified kernel images", + "type": "boolean" + }, "custom-commands": { "description": "Custom commands to be run post install", "type": "array", -- cgit v1.2.3-54-g00ecf From ce9828c2934937217daee6ba178dafedf6cabe6d Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Wed, 18 Oct 2023 16:57:44 +1100 Subject: Fixes for prev PR (#2175) * Fixes for prev PR * Update --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 21 ++++++++++++++++++--- archinstall/lib/exceptions.py | 16 ++++++++-------- archinstall/lib/networking.py | 15 +-------------- archinstall/scripts/guided.py | 7 ------- archinstall/scripts/only_hd.py | 13 +------------ archinstall/scripts/swiss.py | 12 ------------ 6 files changed, 28 insertions(+), 56 deletions(-) (limited to 'archinstall/scripts') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 3949d38a..b9a0c2fb 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -89,6 +89,8 @@ def define_arguments(): 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") def parse_unspecified_argument_list(unknowns: list, multiple: bool = False, err: bool = False) -> dict: @@ -280,8 +282,20 @@ def plugin(f, *args, **kwargs): def _check_new_version(): info("Checking version...") - Pacman.run("-Sy") - upgrade = Pacman.run("-Qu archinstall").decode() + + try: + Pacman.run("-Sy") + except Exception as e: + debug(f'Failed to perform version check: {e}') + info(f'Arch Linux mirrors are not reachable. Please check your internet connection') + exit(1) + + upgrade = None + + try: + upgrade = Pacman.run("-Qu archinstall").decode() + except Exception as e: + debug(f'Failed determine pacman version: {e}') if upgrade: text = f'New version available: {upgrade}' @@ -295,7 +309,8 @@ def main(): 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 """ - _check_new_version() + if not arguments.get('skip_version_check', False): + _check_new_version() script = arguments.get('script', None) diff --git a/archinstall/lib/exceptions.py b/archinstall/lib/exceptions.py index 53458d2c..80926e0b 100644 --- a/archinstall/lib/exceptions.py +++ b/archinstall/lib/exceptions.py @@ -4,19 +4,19 @@ if TYPE_CHECKING: from .general import SysCommandWorker -class RequirementError(BaseException): +class RequirementError(Exception): pass -class DiskError(BaseException): +class DiskError(Exception): pass -class UnknownFilesystemFormat(BaseException): +class UnknownFilesystemFormat(Exception): pass -class SysCallError(BaseException): +class SysCallError(Exception): def __init__(self, message :str, exit_code :Optional[int] = None, worker :Optional['SysCommandWorker'] = None) -> None: super(SysCallError, self).__init__(message) self.message = message @@ -24,17 +24,17 @@ class SysCallError(BaseException): self.worker = worker -class HardwareIncompatibilityError(BaseException): +class HardwareIncompatibilityError(Exception): pass -class ServiceException(BaseException): +class ServiceException(Exception): pass -class PackageError(BaseException): +class PackageError(Exception): pass -class Deprecated(BaseException): +class Deprecated(Exception): pass diff --git a/archinstall/lib/networking.py b/archinstall/lib/networking.py index 2a086f39..fb26bd3d 100644 --- a/archinstall/lib/networking.py +++ b/archinstall/lib/networking.py @@ -8,7 +8,7 @@ from urllib.parse import urlencode from urllib.request import urlopen from .exceptions import SysCallError -from .output import error, info, debug +from .output import error, info from .pacman import Pacman @@ -32,19 +32,6 @@ def list_interfaces(skip_loopback :bool = True) -> Dict[str, str]: return interfaces -def check_mirror_reachable() -> bool: - info("Testing connectivity to the Arch Linux mirrors...") - try: - Pacman.run("-Sy") - return True - except SysCallError as err: - if os.geteuid() != 0: - error("check_mirror_reachable() uses 'pacman -Sy' which requires root.") - debug(f'exit_code: {err.exit_code}, Error: {err.message}') - - return False - - def update_keyring() -> bool: info("Updating archlinux-keyring ...") try: diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index fdf05c99..6acbdbf3 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -1,4 +1,3 @@ -import os from pathlib import Path from typing import Any, TYPE_CHECKING, Optional @@ -15,7 +14,6 @@ from archinstall.lib.mirrors import use_mirrors, add_custom_mirrors from archinstall.lib.models import AudioConfiguration from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.network_configuration import NetworkConfiguration -from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler if TYPE_CHECKING: @@ -232,11 +230,6 @@ def perform_installation(mountpoint: Path): debug(f"Disk states after installing: {disk.disk_layouts()}") -if archinstall.arguments.get('skip-mirror-check', False) is False and check_mirror_reachable() is False: - log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - info(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.") - exit(1) - if not archinstall.arguments.get('silent'): ask_user_questions() diff --git a/archinstall/scripts/only_hd.py b/archinstall/scripts/only_hd.py index d0ee1e39..2293cbee 100644 --- a/archinstall/scripts/only_hd.py +++ b/archinstall/scripts/only_hd.py @@ -1,16 +1,10 @@ -import os from pathlib import Path import archinstall -from archinstall import info, debug +from archinstall import debug from archinstall.lib.installer import Installer from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib import disk -from archinstall.lib.networking import check_mirror_reachable - -if archinstall.arguments.get('help'): - print("See `man archinstall` for help.") - exit(0) def ask_user_questions(): @@ -58,11 +52,6 @@ def perform_installation(mountpoint: Path): debug(f"Disk states after installing: {disk.disk_layouts()}") -if archinstall.arguments.get('skip-mirror-check', False) is False and check_mirror_reachable() is False: - log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - info(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'") - exit(1) - if not archinstall.arguments.get('silent'): ask_user_questions() diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index c04ccca4..1db304a8 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -1,4 +1,3 @@ -import os from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, Optional @@ -10,7 +9,6 @@ from archinstall.lib import models from archinstall.lib import disk from archinstall.lib import locale from archinstall.lib.models import AudioConfiguration -from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib import menu from archinstall.lib.global_menu import GlobalMenu @@ -21,11 +19,6 @@ if TYPE_CHECKING: _: Any -if archinstall.arguments.get('help'): - print("See `man archinstall` for help.") - exit(0) - - class ExecutionMode(Enum): Full = 'full' Lineal = 'lineal' @@ -290,11 +283,6 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): debug(f"Disk states after installing: {disk.disk_layouts()}") -if not check_mirror_reachable(): - log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - info(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'") - exit(1) - param_mode = archinstall.arguments.get('mode', ExecutionMode.Full.value).lower() try: -- cgit v1.2.3-54-g00ecf From cc806d9c4ce29212c2848c15b4b184feace3e1ac Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Tue, 21 Nov 2023 14:34:30 +0100 Subject: Started a re-write of the docs (#1967) * Started a re-write of the docs, using CSV for tables and re-organizing parameter definitions * Added final config options in --config, some have external references which needs to be populated * Forgot to escape a comma * Clarified a note * Added disk configuration and disk encryption docs * Changed way of installing using source and python * Added a 'list script' that lists available scripts. This could be converted to a argparse later. But this does the trick for now. And it's added to ease documentation and listing of available options. * Added a 'Known issues' section, as well as renamed the issues tab * Finished up the known issues section * Added a section regarding --plugin * Added plugin description, tweaked disk_config to the latest changes from #2221 * Added custom-commands docs, and improved some creds and known issue links --- archinstall/__init__.py | 3 +- archinstall/scripts/list.py | 10 + docs/_static/style.css | 3 + docs/_templates/layout.html | 4 + docs/archinstall/Application.rst | 10 - docs/archinstall/Profile.rst | 16 - docs/archinstall/general.rst | 117 ------- docs/archinstall/plugins.rst | 57 ++++ docs/cli_parameters/config/config_options.csv | 24 ++ docs/cli_parameters/config/custom_commands.rst | 22 ++ docs/cli_parameters/config/disk_config.rst | 245 ++++++++++++++ docs/cli_parameters/config/disk_encryption.rst | 19 ++ docs/cli_parameters/config/manual_options.csv | 4 + docs/examples/binary.rst | 23 -- docs/examples/python.rst | 83 +++-- docs/help/issues.rst | 33 -- docs/help/known_issues.rst | 103 ++++++ docs/help/report_bug.rst | 33 ++ docs/index.rst | 38 +-- docs/installing/binary.rst | 52 --- docs/installing/guided.rst | 425 ++++++++++++++----------- docs/installing/python.rst | 6 +- 22 files changed, 827 insertions(+), 503 deletions(-) create mode 100644 archinstall/scripts/list.py create mode 100644 docs/_static/style.css create mode 100644 docs/_templates/layout.html delete mode 100644 docs/archinstall/Application.rst delete mode 100644 docs/archinstall/Profile.rst delete mode 100644 docs/archinstall/general.rst create mode 100644 docs/archinstall/plugins.rst create mode 100644 docs/cli_parameters/config/config_options.csv create mode 100644 docs/cli_parameters/config/custom_commands.rst create mode 100644 docs/cli_parameters/config/disk_config.rst create mode 100644 docs/cli_parameters/config/disk_encryption.rst create mode 100644 docs/cli_parameters/config/manual_options.csv delete mode 100644 docs/examples/binary.rst delete mode 100644 docs/help/issues.rst create mode 100644 docs/help/known_issues.rst create mode 100644 docs/help/report_bug.rst delete mode 100644 docs/installing/binary.rst (limited to 'archinstall/scripts') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index b9a0c2fb..67230c71 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -1,6 +1,7 @@ """Arch Linux installer - guided, templates etc.""" import importlib import os +import sys import time import traceback from argparse import ArgumentParser, Namespace @@ -58,7 +59,7 @@ 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: {disk.disk_layouts()}") -if os.getuid() != 0: +if 'sphinx' not in sys.modules and os.getuid() != 0: print(_("Archinstall requires root privileges to run. See --help for more.")) exit(1) diff --git a/archinstall/scripts/list.py b/archinstall/scripts/list.py new file mode 100644 index 00000000..0e0363a1 --- /dev/null +++ b/archinstall/scripts/list.py @@ -0,0 +1,10 @@ +import glob +import pathlib + +print("The following are viable --script options:") + +for script in [pathlib.Path(x) for x in glob.glob(f"{pathlib.Path(__file__).parent}/*.py")]: + if script.stem in ['__init__', 'list']: + continue + + print(f" {script.stem}") \ No newline at end of file diff --git a/docs/_static/style.css b/docs/_static/style.css new file mode 100644 index 00000000..579fe077 --- /dev/null +++ b/docs/_static/style.css @@ -0,0 +1,3 @@ +.wy-nav-content { + max-width: none; +} \ No newline at end of file diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 00000000..b0a44806 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,4 @@ +{% extends "!layout.html" %} +{% block extrahead %} + +{% endblock %} \ No newline at end of file diff --git a/docs/archinstall/Application.rst b/docs/archinstall/Application.rst deleted file mode 100644 index c6eb0d4d..00000000 --- a/docs/archinstall/Application.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _archinstall.Application: - -archinstall.Application -======================= - -This class enables access to pre-programmed application configurations. -This is not to be confused with :ref:`archinstall.Profile` which is for pre-programmed profiles for a wider set of installation sets. - - -.. autofunction:: archinstall.Application diff --git a/docs/archinstall/Profile.rst b/docs/archinstall/Profile.rst deleted file mode 100644 index a4c1f3bb..00000000 --- a/docs/archinstall/Profile.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. _archinstall.Profile: - -archinstall.Profile -=================== - -This class enables access to pre-programmed profiles. -This is not to be confused with :ref:`archinstall.Application` which is for pre-programmed application profiles. - -Profiles in general is a set or group of installation steps. -Where as applications are a specific set of instructions for a very specific application. - -An example would be the *(currently fictional)* profile called `database`. -The profile `database` might contain the application profile `postgresql`. -And that's the difference between :ref:`archinstall.Profile` and :ref:`archinstall.Application`. - -.. autofunction:: archinstall.Profile diff --git a/docs/archinstall/general.rst b/docs/archinstall/general.rst deleted file mode 100644 index 970d40f2..00000000 --- a/docs/archinstall/general.rst +++ /dev/null @@ -1,117 +0,0 @@ -.. _archinstall.helpers: - -.. warning:: - All these helper functions are mostly, if not all, related to outside-installation-instructions. Meaning the calls will affect your current running system - and not touch your installed system. - -Profile related helpers -======================= - -.. autofunction:: archinstall.list_profiles - -Packages -======== - -.. autofunction:: archinstall.find_package - -.. autofunction:: archinstall.find_packages - -Locale related -============== - -.. autofunction:: archinstall.list_keyboard_languages - -.. autofunction:: archinstall.search_keyboard_layout - -.. autofunction:: archinstall.set_keyboard_language - -.. - autofunction:: archinstall.Installer.set_keyboard_layout - -Services -======== - -.. autofunction:: archinstall.service_state - -Mirrors -======= - -.. autofunction:: archinstall.filter_mirrors_by_region - -.. autofunction:: archinstall.add_custom_mirrors - -.. autofunction:: archinstall.insert_mirrors - -.. autofunction:: archinstall.use_mirrors - -.. autofunction:: archinstall.re_rank_mirrors - -.. autofunction:: archinstall.list_mirrors - -Disk related -============ - -.. autofunction:: archinstall.BlockDevice - -.. autofunction:: archinstall.Partition - -.. autofunction:: archinstall.Filesystem - -.. autofunction:: archinstall.device_state - -.. autofunction:: archinstall.all_blockdevices - -Luks (Disk encryption) -====================== - -.. autofunction:: archinstall.luks2 - -Networking -========== - -.. autofunction:: archinstall.get_hw_addr - -.. autofunction:: archinstall.list_interfaces - -.. autofunction:: archinstall.check_mirror_reachable - -.. autofunction:: archinstall.update_keyring - -.. autofunction:: archinstall.enrich_iface_types - -.. autofunction:: archinstall.get_interface_from_mac - -.. autofunction:: archinstall.wireless_scan - -.. autofunction:: archinstall.get_wireless_networks - -General -======= - -.. autofunction:: archinstall.log - -.. autofunction:: archinstall.locate_binary - -.. autofunction:: archinstall.SysCommand - -.. autofunction:: archinstall.SysCommandWorker - -Exceptions -========== - -.. autofunction:: archinstall.RequirementError - -.. autofunction:: archinstall.DiskError - -.. autofunction:: archinstall.ProfileError - -.. autofunction:: archinstall.SysCallError - -.. autofunction:: archinstall.ProfileNotFound - -.. autofunction:: archinstall.HardwareIncompatibilityError - -.. autofunction:: archinstall.PermissionError - -.. autofunction:: archinstall.UserError - -.. autofunction:: archinstall.ServiceException diff --git a/docs/archinstall/plugins.rst b/docs/archinstall/plugins.rst new file mode 100644 index 00000000..898f9006 --- /dev/null +++ b/docs/archinstall/plugins.rst @@ -0,0 +1,57 @@ +.. _archinstall.Plugins: + +Python Plugins +============== + +``archinstall`` supports plugins via two methods. + +First method is directly via the ``--plugin`` parameter when running as a CLI tool. This will load a specific plugin locally or remotely via a path. + +The second method is via Python's built in `plugin discovery`_ using `entry points`_ categorized as ``archinstall.plugin``. + +``--plugin`` parameter +---------------------- + +The parameter has the benefit of being stored in the ``--conf`` state, meaning when re-running an installation — the plugin will automatically be loaded. +It's limitation is that it requires an initial path to be known and written and be cumbersome. + +Plugin Discovery +---------------- + +This method allows for multiple plugins to be loaded with the drawback that they have to be installed beforehand on the system running ``archinstall``. +This mainly targets those who build their own ISO's and package specific setups for their needs. + + +What's supported? +----------------- + +Currently the documentation for this is scarse. Until that is resolved, the best way to find supported features is to search the source code for `plugin.on_ `_ as this will give a clear indication of which calls are made to plugins. + +How does it work? +----------------- + +``archinstall`` plugins use a discovery-driven approach where plugins are queried for certain functions. +As an example, if a plugin has the following function: + +.. code-block:: python + + def on_pacstrap(*packages): + ... + +The function :code:`archinstall.Pacman().strap(["some packages"])` is hardcoded to iterate plugins and look for :code:`on_pacstrap` in the plugin. +If the function exists, :code:`.strap()` will call the plugin's function and replace the initial package list with the result from the plugin. + +The best way to document these calls is currently undecided, as it's hard to document this behavior dynamically. + +Writing your own? +----------------- + +The simplest way currently is to look at a reference implementation or the community. Two of these are: + +* `torxed/archinstall-aur `_ +* `phisch/archinstall-aur `_ + +And search for `plugin.on_ `_ in the code base to find what ``archinstall`` will look for. PR's are welcome to widen the support for this. + +.. _plugin discovery: https://packaging.python.org/en/latest/specifications/entry-points/ +.. _entry points: https://docs.python.org/3/library/importlib.metadata.html#entry-points \ No newline at end of file diff --git a/docs/cli_parameters/config/config_options.csv b/docs/cli_parameters/config/config_options.csv new file mode 100644 index 00000000..1861b1e1 --- /dev/null +++ b/docs/cli_parameters/config/config_options.csv @@ -0,0 +1,24 @@ +Key,Value(s),Description,Required +additional-repositories,[ `multilib `_!, `testing `_ ],Enables one or more of the testing and multilib repositories before proceeding with installation,No +archinstall-language,`lang `__,Sets the TUI language used *(make sure to use the ``lang`` value not the ``abbr``)*,No +audio_config,`pipewire `_!, `pulseaudio `_,Audioserver to be installed,No +bootloader,`Systemd-boot `_!, `grub `_,Bootloader to be installed *(grub being mandatory on BIOS machines)*,Yes +debug,``true``!, ``false``,Enables debug output,No +disk_config,*Read more under* :ref:`disk config`,Contains the desired disk setup to be used during installation,No +disk_encryption,*Read more about under* :ref:`disk encryption`,Parameters for disk encryption applied ontop of ``disk_config``,No +hostname,``str``,A string definining your machines hostname on the network *(defaults to ``archinstall``)*,No +kernels,[ `linux `_!, `linux-hardened `_!, `linux-lts `_!, `linux-rt `_!, `linux-rt-lts `_!, `linux-zen `_ ],Defines which kernels should be installed and setup in the boot loader options,Yes +custom-commands,*Read more under* :ref:`custom commands`,Custom commands that will be run post-install chrooted inside the installed system,No +locale_config,{kb_layout: `lang `__!, sys_enc: `Character encoding `_!, sys_lang: `locale `_},Defines the keyboard key map!, system encoding and system locale,No +mirror_config,{custom_mirrors: [ https://... ]!, mirror_regions: { "Worldwide": [ "https://geo.mirror.pkgbuild.com/$repo/os/$arch" ] } },Sets various mirrors *(defaults to ISO's ``/etc/pacman.d/mirrors`` if not defined)*,No +network_config,*`see options under Network Configuration`*,Sets which type of *(if any)* network configuration should be used,No +no_pkg_lookups,``true``!, ``false``,Disabled package checking against https://archlinux.org/packages/,No +ntp,``true``!, ``false``,enables or disables `NTP `_ during installation,No +offline,``true``!, ``false``,enables or disables certain online checks such as mirror reachability etc,No +packages,[ !, !, ... ],A list of packages to install during installation,No +parallel downloads,0-∞,sets a given number of paralell downloads to be used by `pacman `_,No +profile_config,*`read more under the profiles section`*,Installs a given profile if defined,No +script,`guided `__! *(default)*!, `minimal `__!, `only_hdd `_!, `swiss `_!, `unattended `_,When used to autorun an installation!, this sets which script to autorun with,No +silent,``true``!, ``false``,disables or enables user questions using the TUI,No +swap,``true``!, ``false``,enables or disables swap,No +timezone,`timezone `_,sets a timezone for the installed system,No \ No newline at end of file diff --git a/docs/cli_parameters/config/custom_commands.rst b/docs/cli_parameters/config/custom_commands.rst new file mode 100644 index 00000000..c1529020 --- /dev/null +++ b/docs/cli_parameters/config/custom_commands.rst @@ -0,0 +1,22 @@ +.. _custom commands: + +Custom Commands +=============== + +| Custom commands is a configuration entry that allows for executing custom commands post-installation. +| The commands are executed with `arch-chroot `_. + +The option takes a list of arguments, an example is: + +.. code-block:: json + + { + "custom-commands": [ + "hostname new-hostname" + ] + } + +| The following example will set a new hostname in the installed system. +| The example is just to illustrate that the command is not run in the ISO but inside the installed system after the base system is installed. + +More examples can be found in the code repository under `examples/ `_ \ No newline at end of file diff --git a/docs/cli_parameters/config/disk_config.rst b/docs/cli_parameters/config/disk_config.rst new file mode 100644 index 00000000..ed5f42c1 --- /dev/null +++ b/docs/cli_parameters/config/disk_config.rst @@ -0,0 +1,245 @@ +.. _disk config: + +Disk Configuration +================== + +There are only three modes in the ``disk_config`` option. They are described in more detail below. + +"Leave as is" +-------------- + +.. code-block:: json + + { + "config_type": "pre_mounted_config", + "mountpoint": "/mnt/archinstall" + } + +This mode will not perform any partitioning what so ever. +Instead it relies on what's mounted manually by the user under ``/mnt/archinstall``. + +Given the following disk example: + +.. code-block:: + + /mnt/archinstall (/dev/sda2) + ├── boot (/dev/sda1) + └── home (/dev/sda3) + +Runing ``archinstall --conf your.json --silent`` where the above JSON is configured. The disk will be left alone — and a working system will be installed to the above folders and mountpoints will be translated into the installed system. + +.. note:: + + Some disk layouts can be too complicated to detect, such as RAID setups. Please do report those setups on the `Issue Tracker `__ so we can support them. + +Best Effort +----------- + +.. warning:: + + This mode will wipe data! + +.. note:: + + Note that this options is similar to the manual partitioning but is generated through the menu system! And the best effort layout might deviate slightly from some wiki guidelines in order to facilitate some optional configurations at a later stage. + +.. code-block:: json + + { + "disk_config": { + "config_type": "default_layout", + "device_modifications": [ + { + "device": "/dev/sda", + "wipe": true, + "partitions": "..." + } + ] + } + } + +This mode will attempt to configure a sane default layout on the selected disks. +Based on the chosen filesystem, and potential optional settings for said filesystem — different default layouts will be provided. + +Manual Partitioning +------------------- + +.. code-block:: json + + { + "disk_config": { + "config_type": "manual_partitioning", + "device_modifications": [ + "filesystem struct" + ] + } + } + +Manual partitioning is the most complex one of the three. It offers you near endless flexibility of how to partition your disk. It integrates against `pyparted `__ and some control logic in ``archinstall`` that deals with creating things like subvolumes and compression. + +Sizes are by default ``sector`` units, but other units are supported. + +The options supplied to ``manual_partitioning`` are dictionary definitions, where the following parameters must exist: + +.. csv-table:: JSON options + :file: ./manual_options.csv + :widths: 15, 15, 65, 5 + :escape: ! + :header-rows: 1 + +Each partition definition heavily relies on what filesystem is used. +Below follow two of the more common filesystems, anything else will best be described by running ``archinstall`` to generate a desired configuration for the desired filesystem type — and copy the relevant parts for permanent configurations. + +.. warning:: + + Important to note that the units and positions in the examples below — are highly user specific! + +FAT32 +^^^^^ + +.. code-block:: json + + { + "btrfs": [], + "flags": [ + "Boot" + ], + "fs_type": "fat32", + "length": { + "sector_size": null, + "total_size": null, + "unit": "B", + "value": 99982592 + }, + "mount_options": [], + "mountpoint": "/boot", + "obj_id": "369f31a8-2781-4d6b-96e7-75680552b7c9", + "start": { + "sector_size": { + "sector_size": null, + "total_size": null, + "unit": "B", + "value": 512 + }, + "total_size": null, + "unit": "sectors", + "value": 34 + }, + "status": "create", + "type": "primary" + } + +.. note:: + + The ``Boot`` flag will make ``archinstall`` automatically set the correct ESP partition GUID if the system is booted with ``EFI`` support. The GUID will then be set to ``C12A7328-F81F-11D2-BA4B-00A0C93EC93B``. + +EXT4 +^^^^ + +.. code-block:: json + + { + "btrfs": [], + "flags": [], + "fs_type": "ext4", + "length": { + "sector_size": null, + "total_size": null, + "unit": "B", + "value": 15805127360 + }, + "mount_options": [], + "mountpoint": "/", + "obj_id": "3e75d045-21a4-429d-897e-8ec19a006e8b", + "start": { + "sector_size": { + "sector_size": null, + "total_size": null, + "unit": "B", + "value": 512 + }, + "total_size": { + "sector_size": null, + "total_size": null, + "unit": "B", + "value": 16106127360 + }, + "unit": "MB", + "value": 301 + }, + "status": "create", + "type": "primary" + } + +BTRFS +^^^^^ + +The BTRFS filesystem is inherently more complicated, thus the options are a bit more involved. +This example contains both subvolumes and compression. + +.. note:: + + Note that the ``"mountpoint": null`` is used for the overall partition, and instead individual subvolumes have mountpoints set. + +.. code-block:: json + + { + "btrfs": [ + { + "compress": false, + "mountpoint": "/", + "name": "@", + "nodatacow": false + }, + { + "compress": false, + "mountpoint": "/home", + "name": "@home", + "nodatacow": false + }, + { + "compress": false, + "mountpoint": "/var/log", + "name": "@log", + "nodatacow": false + }, + { + "compress": false, + "mountpoint": "/var/cache/pacman/pkg", + "name": "@pkg", + "nodatacow": false + }, + { + "compress": false, + "mountpoint": "/.snapshots", + "name": "@.snapshots", + "nodatacow": false + } + ], + "dev_path": null, + "flags": [], + "fs_type": "btrfs", + "mount_options": [ + "compress=zstd" + ], + "mountpoint": null, + "obj_id": "d712357f-97cc-40f8-a095-24ff244d4539", + "size": { + "sector_size": { + "unit": "B", + "value": 512 + }, + "unit": "B", + "value": 15568207872 + }, + "start": { + "sector_size": { + "unit": "B", + "value": 512 + }, + "unit": "MiB", + "value": 513 + }, + "status": "create", + "type": "primary" + } \ No newline at end of file diff --git a/docs/cli_parameters/config/disk_encryption.rst b/docs/cli_parameters/config/disk_encryption.rst new file mode 100644 index 00000000..df2e2fa7 --- /dev/null +++ b/docs/cli_parameters/config/disk_encryption.rst @@ -0,0 +1,19 @@ +.. _disk encryption: + +Disk Encryption +=============== + +Disk encryption consists of a top level entry in the user configuration. + +.. code-block:: json + + { + "disk_encryption": { + "encryption_type": "luks", + "partitions": [ + "d712357f-97cc-40f8-a095-24ff244d4539" + ] + } + } + +The ``UID`` in the ``partitions`` list is an internal reference to the ``obj_id`` in the :ref:`disk config` entries. \ No newline at end of file diff --git a/docs/cli_parameters/config/manual_options.csv b/docs/cli_parameters/config/manual_options.csv new file mode 100644 index 00000000..2fcc26f0 --- /dev/null +++ b/docs/cli_parameters/config/manual_options.csv @@ -0,0 +1,4 @@ +Key,Value(s),Description,Required +device,``str``,Which block-device to format,yes +partitions,[ {key: val} ],The data describing the change/addition in a partition,yes +wipe,``bool``,clear the disk before adding any partitions,No \ No newline at end of file diff --git a/docs/examples/binary.rst b/docs/examples/binary.rst deleted file mode 100644 index 51dbd1dd..00000000 --- a/docs/examples/binary.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. _examples.binary: - -Binary executable -================= - -.. warning:: The binary option is limited and stiff. It's hard to modify or create your own installer-scripts this way unless you compile the source manually. If your usecase needs custom scripts, either use the pypi setup method or you'll need to adjust the PKGBUILD prior to building the arch package. - -The binary executable is a standalone compiled version of the library. -It's compiled using `nuitka `_ with the flag `--standalone`. - -Executing the binary --------------------- - -As an example we'll use the `guided `_ installer. -To run the `guided` installed, all you have to do *(after installing or compiling the binary)*, is run: - - -.. code-block:: console - - ./archinstall guided - -As mentioned, the binary is a bit rudimentary and only supports executing whatever is found directly under `./archinstall/examples`. -Anything else won't be found. This is subject to change in the future to make it a bit more flexible. diff --git a/docs/examples/python.rst b/docs/examples/python.rst index b5478fe2..7fb3f6c3 100644 --- a/docs/examples/python.rst +++ b/docs/examples/python.rst @@ -4,7 +4,7 @@ Python module ============= Archinstall supports running in `module mode `_. -The way the library is invoked in module mode is limited to executing scripts under the **example** folder. +The way the library is invoked in module mode is limited to executing scripts under the `scripts`_ folder. It's therefore important to place any script or profile you wish to invoke in the examples folder prior to building and installing. @@ -12,7 +12,7 @@ Pre-requisites -------------- We'll assume you've followed the :ref:`installing.python.manual` method. -Before actually installing the library, you will need to place your custom installer-scripts under `./archinstall/examples/` as a python file. +Before actually installing the library, you will need to place your custom installer-scripts under `scripts`_ as a python file. More on how you create these in the next section. @@ -24,36 +24,73 @@ Creating a script ----------------- Lets create a `test_installer` - installer as an example. This is assuming that the folder `./archinstall` is a git-clone of the main repo. -We begin by creating `./archinstall/examples/test_installer.py`. The placement here is important later. +We begin by creating "`scripts`_:code:`/test_installer.py`". The placement here is important later. -This script can now already be called using `python -m archinstall test_installer` after a successful installation of the library itself. +This script can now already be called using :code:`python -m archinstall test_installer` after a successful installation of the library itself. But the script won't do much. So we'll do something simple like list all the hard drives as an example. -To do this, we'll begin by importing `archinstall` in our `./archinstall/examples/test_installer.py` and call some functions. +To do this, we'll begin by importing :code:`archinstall` in our "`scripts`_:code:`/test_installer.py`" and call a function whtin ``archinstall``. .. code-block:: python import archinstall - all_drives = archinstall.all_blockdevices(partitions=False) - print(list(all_drives.keys())) + print(archinstall.disk.device_handler.devices) + +Now, go ahead and reference the :ref:`installing.python.manual` installation method. +After runnig ``python -m archinstall test_installer`` it should print something that looks like: + +.. code-block:: text + + [ + BDevice( + disk=, + device_info=_DeviceInfo( + model='PC801 NVMe SK hynix 512GB', + path=PosixPath('/dev/nvme0n1'), + type='nvme', + total_size=Size(value=512110190592, unit=, + sector_size=SectorSize(value=512, unit=)), + free_space_regions=[ + , + , + ], + sector_size=SectorSize(value=512, unit=), + read_only=False, + dirty=False + ), + partition_infos=[ + _PartitionInfo( + partition=, + name='primary', + type=, + fs_type=, + path='/dev/nvme0n1p1', + start=Size(value=2048, unit=, sector_size=SectorSize(value=512, unit=)), + length=Size(value=535822336, unit=, sector_size=SectorSize(value=512, unit=)), + flags=[ + , + + ], + partn=1, + partuuid='a26be943-c193-41f4-9930-9341cf5f6b19', + uuid='6EE9-2C00', + disk=, + mountpoints=[ + PosixPath('/boot') + ], + btrfs_subvol_infos=[] + ), + _PartitionInfo(...) + ] + ) + ] + +That means your script is in the right place, and ``archinstall`` is working as intended. -This should print out a list of drives and some meta-information about them. -As an example, this will do just fine. - -Now, go ahead and install the library either as a user-module or system-wide. - -Calling a module ----------------- - -Assuming you've followed the example in `Creating a script`_, you can now safely call it with: - -.. code-block:: console - - python -m archinstall test_installer +.. note:: -This should now print all available drives on your system. + Most calls, including the one above requires `root `_ privileges. -.. note:: - This should work on any system, not just Arch Linux based ones. But note that other functions in the library rely heavily on Arch Linux based commands to execute the installation steps. Such as `arch-chroot`. +.. _scripts: https://github.com/archlinux/archinstall/tree/master/archinstall/scripts \ No newline at end of file diff --git a/docs/help/issues.rst b/docs/help/issues.rst deleted file mode 100644 index 7d690b65..00000000 --- a/docs/help/issues.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _help.issues: - -Issue tracker & bugs -==================== - -Issues and bugs should be reported over at `https://github.com/archlinux/archinstall/issues `_. - -General questions, enhancements and security issues can be reported over there too. -For quick issues or if you need help, head over to the Discord server which has a help channel. - -Log files ---------- - -| When submitting a help ticket, please include the :code:`/var/log/archinstall/install.log`. -| It can be found both on the live ISO but also in the installed filesystem if the base packages were strapped in. - -.. tip:: - | An easy way to submit logs is ``curl -F'file=@/var/log/archinstall/install.log' https://0x0.st``. - | Use caution when submitting other log files, but ``archinstall`` pledges to keep ``install.log`` safe for posting publicly! - -| There are additional log files under ``/var/log/archinstall/`` that can be useful: - - - ``/var/log/archinstall/user_configuration.json`` - Stores most of the guided answers in the installer - - ``/var/log/archinstall/user_credentials.json`` - Stores any usernames or passwords, can be passed to ``--creds`` - - ``/var/log/archinstall/user_disk_layouts.json`` - Stores the chosen disks and their layouts - - ``/var/log/archinstall/install.log`` - A log file over what steps were taken by archinstall - - ``/var/log/archinstall/cmd_history.txt`` - A complete command history, command by command in order - - ``/var/log/archinstall/cmd_output.txt`` - A raw output from all the commands that were executed by archinstall - -.. warning:: - - We only try to guarantee that ``/var/log/archinstall/install.log`` is free from sensitive information. - Any other log file should be pasted with **utmost care**! diff --git a/docs/help/known_issues.rst b/docs/help/known_issues.rst new file mode 100644 index 00000000..2f1f62cb --- /dev/null +++ b/docs/help/known_issues.rst @@ -0,0 +1,103 @@ +.. _help.known_issues: + +Known Issues +============ + +| Some issues are out of the `archinstall`_ projects scope, and the ones we know of are listed below. + +.. _waiting for time sync: + +Waiting for time sync `#2144`_ +------------------------------ + +| The usual root cause of this is the network topology. +| More specifically `timedatectl show`_ cannot perform a proper time sync against the default servers. + +| A *"fix"* for this is mentioned in the issue above. +| That is to configure ``/etc/systemd/timesyncd.conf`` and restart ``systemd-timesyncd.service``. + +.. note:: + + A proposal to override the time sync check has been put up for discussion in `#2144`_. + +Missing Nvidia Proprietary Driver `#2002`_ +------------------------------------------ + +| In some instances, the nvidia driver might not have all the nessecary packages installed. +| This is due to the kernel selection and/or hardware setups requiring additional packages to work properly. + +A common workaround is to install the package `linux-headers`_ and `nvidia-dkms`_ + +ARM, 32bit and other CPU types error out `#1686`_, `#2185`_ +----------------------------------------------------------- + +| This is a bit of a catch-all known issue. +| Officially `x86_64`_ is only supported by Arch Linux. +| Hence little effort have been put into supporting other platforms. + +| In theory, other architectures should work but small quirks might arise. + +| PR's are welcome but please be respectful of the delays in merging. +| Other fixes, issues or features will be prioritized for the above reasons. + +Keyring is out of date `#2213`_ +------------------------------- + +| Missing key-issues tend to be that the `archlinux-keyring`_ package is out of date, usually as a result of an outdated ISO. +| There is an attempt from upstream to fix this issue, and it's the `archlinux-keyring-wkd-sync.service`_ + +| The service starts almost immediately during boot, and if network is not configured in time — the service will fail. +| Subsequently the ``archinstall`` run might operate on a old keyring despite there being an update service for this. + +| There is really no way to reliably over time work around this issue in ``archinstall``. +| Instead, efforts to the upstream service should be considered the way forward. And/or keys not expiring betwene a sane ammount of ISO's. + +.. note:: + + The issue can happen on new ISO's too even as little as a few days after release, as some keys might expire right after the keyring is *"burnt in"* to the ISO. + +.. note:: + + Another common issue relating to the network not being configured, is that time might not be set correctly - resulting in the keyring not being able to update. See :ref:`waiting for time sync`. + +AUR packages +------------ + +| This is also a catch-all issue. +| `AUR is unsupported `_, and until that changes we cannot use AUR packages to solve feature requests in ``archinstall``. + +| This means that feature requests like supporting filesystems such as `ZFS`_ can not be added, and issues cannot be solved by using AUR packages either. + +.. note:: + + But in spirit of giving the community options, ``archinstall`` supports :ref:`archinstall.Plugins`, which means you can run ``archinstall --plugin `` and source an AUR plugin. + + `torxed/archinstall-aur `_ is a reference implementation for plugins: + + .. code-block:: console + + # archinstall --plugin https://archlinux.life/aur-plugin + + `phisch/archinstall-aur `_ is another alternative: + + .. code-block:: console + + # archinstall --plugin https://raw.githubusercontent.com/phisch/archinstall-aur/master/archinstall-aur.py + + .. warning:: + + This will allow for unsupported usage of AUR during installation. + +.. _#2213: https://github.com/archlinux/archinstall/issues/2213 +.. _#2185: https://github.com/archlinux/archinstall/issues/2185 +.. _#2144: https://github.com/archlinux/archinstall/issues/2144 +.. _#2002: https://github.com/archlinux/archinstall/issues/2002 +.. _#1686: https://github.com/archlinux/archinstall/issues/1686 +.. _linux-headers: https://archlinux.org/packages/core/x86_64/linux-headers/ +.. _nvidia-dkms: https://archlinux.org/packages/extra/x86_64/nvidia-dkms/ +.. _x86_64: https://wiki.archlinux.org/title/Frequently_asked_questions#What_architectures_does_Arch_support? +.. _archlinux-keyring: https://archlinux.org/packages/core/any/archlinux-keyring/ +.. _archlinux-keyring-wkd-sync.service: https://gitlab.archlinux.org/archlinux/archlinux-keyring/-/blob/7e672dad10652a80d1cc575d75cdb46442cd7f96/wkd_sync/archlinux-keyring-wkd-sync.service.in +.. _ZFS: https://aur.archlinux.org/packages/zfs-linux +.. _archinstall: https://github.com/archlinux/archinstall/ +.. _timedatectl show: https://github.com/archlinux/archinstall/blob/e6344f93f7e476d05bbcd642f2ed91fdde545870/archinstall/lib/installer.py#L136 \ No newline at end of file diff --git a/docs/help/report_bug.rst b/docs/help/report_bug.rst new file mode 100644 index 00000000..bd0ac50a --- /dev/null +++ b/docs/help/report_bug.rst @@ -0,0 +1,33 @@ +.. _help.issues: + +Report Issues & Bugs +==================== + +Issues and bugs should be reported over at `https://github.com/archlinux/archinstall/issues `_. + +General questions, enhancements and security issues can be reported over there too. +For quick issues or if you need help, head over to the Discord server which has a help channel. + +Log files +--------- + +| When submitting a help ticket, please include the :code:`/var/log/archinstall/install.log`. +| It can be found both on the live ISO but also in the installed filesystem if the base packages were strapped in. + +.. tip:: + | An easy way to submit logs is ``curl -F'file=@/var/log/archinstall/install.log' https://0x0.st``. + | Use caution when submitting other log files, but ``archinstall`` pledges to keep ``install.log`` safe for posting publicly! + +| There are additional log files under ``/var/log/archinstall/`` that can be useful: + + - ``/var/log/archinstall/user_configuration.json`` - Stores most of the guided answers in the installer + - ``/var/log/archinstall/user_credentials.json`` - Stores any usernames or passwords, can be passed to ``--creds`` + - ``/var/log/archinstall/user_disk_layouts.json`` - Stores the chosen disks and their layouts + - ``/var/log/archinstall/install.log`` - A log file over what steps were taken by archinstall + - ``/var/log/archinstall/cmd_history.txt`` - A complete command history, command by command in order + - ``/var/log/archinstall/cmd_output.txt`` - A raw output from all the commands that were executed by archinstall + +.. warning:: + + We only try to guarantee that ``/var/log/archinstall/install.log`` is free from sensitive information. + Any other log file should be pasted with **utmost care**! diff --git a/docs/index.rst b/docs/index.rst index a76a58d6..6a81bbb0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,17 +8,15 @@ archinstall Documentation Some of the features of Archinstall are: -* **No external dependencies or installation requirements.** Runs without any external requirements or installation processes. - * **Context friendly.** The library always executes calls in sequential order to ensure installation-steps don't overlap or execute in the wrong order. It also supports *(and uses)* context wrappers to ensure cleanup and final tasks such as ``mkinitcpio`` are called when needed. -* **Full transparency** Logs and insights can be found at ``/var/log/archinstall`` both in the live ISO and the installed system. +* **Full transparency** Logs and insights can be found at ``/var/log/archinstall`` both in the live ISO and partially on the installed system. * **Accessibility friendly** Archinstall works with ``espeakup`` and other accessibility tools thanks to the use of a TUI. .. toctree:: - :maxdepth: 3 - :caption: Running the installer + :maxdepth: 1 + :caption: Running Archinstall installing/guided @@ -26,8 +24,9 @@ Some of the features of Archinstall are: :maxdepth: 3 :caption: Getting help + help/known_issues + help/report_bug help/discord - help/issues .. toctree:: :maxdepth: 3 @@ -35,35 +34,10 @@ Some of the features of Archinstall are: installing/python examples/python - -.. toctree:: - :maxdepth: 3 - :caption: Archinstall as a binary - - installing/binary - examples/binary -.. - examples/scripting - -.. - .. toctree:: - :maxdepth: 3 - :caption: Programming Guide - -.. - programming_guide/requirements - programming_guide/basic_concept + archinstall/plugins .. toctree:: :maxdepth: 3 :caption: API Reference archinstall/Installer - archinstall/Profile - archinstall/Application - -.. toctree:: - :maxdepth: 3 - :caption: API Helper functions - - archinstall/general diff --git a/docs/installing/binary.rst b/docs/installing/binary.rst deleted file mode 100644 index eeb9d79d..00000000 --- a/docs/installing/binary.rst +++ /dev/null @@ -1,52 +0,0 @@ -.. _installing.binary: - -Binary executable -================= - -Archinstall can be compiled into a standalone executable. -For Arch Linux based systems, there's a package for this called `archinstall `_. - -.. warning:: - This is not required if you're running archinstall on a pre-built ISO. The installation is only required if you're creating your own scripted installations. - -Using pacman ------------- - -Archinstall is on the `official repositories `_. - -.. code-block:: console - - sudo pacman -S archinstall - -Using PKGBUILD --------------- - -The `source `_ contains a binary `PKGBUILD `_ which can be either copied straight off the website or cloned using :code:`git clone https://github.com/Torxed/archinstall`. - -Once you've obtained the `PKGBUILD`, building it is pretty straight forward. - -.. code-block:: console - - makepkg -s - -Which should produce an `archinstall-X.x.z-1.pkg.tar.zst` which can be installed using: - -.. code-block:: console - - sudo pacman -U archinstall-X.x.z-1.pkg.tar.zst - -.. note:: - - For a complete guide on the build process, please consult the `PKGBUILD on ArchWiki `_. - -Manual compilation ------------------- - -You can compile the source manually without using a custom mirror or the `PKGBUILD` that is shipped. -Simply clone or download the source, and while standing in the cloned folder `./archinstall`, execute: - -.. code-block:: console - - nuitka3 --standalone --show-progress archinstall - -This requires the `nuitka `_ package as well as `python3` to be installed locally. diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index c5e7f1ed..dcedfc10 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -3,20 +3,16 @@ Guided installation =================== -| This is the default script the Arch Linux `Archinstall package `_. -| It will guide you through a very basic installation of Arch Linux. +Archinstall ships with a pre-programmed `Guided Installer`_ guiding you through the mandatory steps as well as some optional configurations that can be done. .. note:: - There are other scripts and they can be invoked by executing `archinstall