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/default_profiles/__init__.py | 0 .../default_profiles/applications/__init__.py | 0 .../default_profiles/applications/pipewire.py | 40 ++++ archinstall/default_profiles/custom.py | 218 +++++++++++++++++++++ archinstall/default_profiles/desktop.py | 87 ++++++++ archinstall/default_profiles/desktops/__init__.py | 0 archinstall/default_profiles/desktops/awesome.py | 36 ++++ archinstall/default_profiles/desktops/bspwm.py | 30 +++ archinstall/default_profiles/desktops/budgie.py | 30 +++ archinstall/default_profiles/desktops/cinnamon.py | 31 +++ archinstall/default_profiles/desktops/cutefish.py | 31 +++ archinstall/default_profiles/desktops/deepin.py | 28 +++ .../default_profiles/desktops/enlightenment.py | 27 +++ archinstall/default_profiles/desktops/gnome.py | 27 +++ archinstall/default_profiles/desktops/i3.py | 33 ++++ archinstall/default_profiles/desktops/kde.py | 32 +++ archinstall/default_profiles/desktops/lxqt.py | 35 ++++ archinstall/default_profiles/desktops/mate.py | 27 +++ archinstall/default_profiles/desktops/qtile.py | 27 +++ archinstall/default_profiles/desktops/sway.py | 66 +++++++ archinstall/default_profiles/desktops/xfce4.py | 30 +++ archinstall/default_profiles/minimal.py | 15 ++ archinstall/default_profiles/profile.py | 206 +++++++++++++++++++ archinstall/default_profiles/server.py | 57 ++++++ archinstall/default_profiles/servers/__init__.py | 0 archinstall/default_profiles/servers/cockpit.py | 19 ++ archinstall/default_profiles/servers/docker.py | 33 ++++ archinstall/default_profiles/servers/httpd.py | 19 ++ archinstall/default_profiles/servers/lighttpd.py | 19 ++ archinstall/default_profiles/servers/mariadb.py | 25 +++ archinstall/default_profiles/servers/nginx.py | 19 ++ archinstall/default_profiles/servers/postgresql.py | 26 +++ archinstall/default_profiles/servers/sshd.py | 19 ++ archinstall/default_profiles/servers/tomcat.py | 19 ++ archinstall/default_profiles/tailored.py | 21 ++ archinstall/default_profiles/xorg.py | 21 ++ 36 files changed, 1353 insertions(+) create mode 100644 archinstall/default_profiles/__init__.py create mode 100644 archinstall/default_profiles/applications/__init__.py create mode 100644 archinstall/default_profiles/applications/pipewire.py create mode 100644 archinstall/default_profiles/custom.py create mode 100644 archinstall/default_profiles/desktop.py create mode 100644 archinstall/default_profiles/desktops/__init__.py create mode 100644 archinstall/default_profiles/desktops/awesome.py create mode 100644 archinstall/default_profiles/desktops/bspwm.py create mode 100644 archinstall/default_profiles/desktops/budgie.py create mode 100644 archinstall/default_profiles/desktops/cinnamon.py create mode 100644 archinstall/default_profiles/desktops/cutefish.py create mode 100644 archinstall/default_profiles/desktops/deepin.py create mode 100644 archinstall/default_profiles/desktops/enlightenment.py create mode 100644 archinstall/default_profiles/desktops/gnome.py create mode 100644 archinstall/default_profiles/desktops/i3.py create mode 100644 archinstall/default_profiles/desktops/kde.py create mode 100644 archinstall/default_profiles/desktops/lxqt.py create mode 100644 archinstall/default_profiles/desktops/mate.py create mode 100644 archinstall/default_profiles/desktops/qtile.py create mode 100644 archinstall/default_profiles/desktops/sway.py create mode 100644 archinstall/default_profiles/desktops/xfce4.py create mode 100644 archinstall/default_profiles/minimal.py create mode 100644 archinstall/default_profiles/profile.py create mode 100644 archinstall/default_profiles/server.py create mode 100644 archinstall/default_profiles/servers/__init__.py create mode 100644 archinstall/default_profiles/servers/cockpit.py create mode 100644 archinstall/default_profiles/servers/docker.py create mode 100644 archinstall/default_profiles/servers/httpd.py create mode 100644 archinstall/default_profiles/servers/lighttpd.py create mode 100644 archinstall/default_profiles/servers/mariadb.py create mode 100644 archinstall/default_profiles/servers/nginx.py create mode 100644 archinstall/default_profiles/servers/postgresql.py create mode 100644 archinstall/default_profiles/servers/sshd.py create mode 100644 archinstall/default_profiles/servers/tomcat.py create mode 100644 archinstall/default_profiles/tailored.py create mode 100644 archinstall/default_profiles/xorg.py (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/__init__.py b/archinstall/default_profiles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/archinstall/default_profiles/applications/__init__.py b/archinstall/default_profiles/applications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/archinstall/default_profiles/applications/pipewire.py b/archinstall/default_profiles/applications/pipewire.py new file mode 100644 index 00000000..5d2e5ea3 --- /dev/null +++ b/archinstall/default_profiles/applications/pipewire.py @@ -0,0 +1,40 @@ +from typing import List, Union, Any, TYPE_CHECKING + +import archinstall + +from archinstall.default_profiles.profile import Profile, ProfileType +from archinstall.lib.models import User + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +class PipewireProfile(Profile): + def __init__(self): + super().__init__('Pipewire', ProfileType.Application) + + @property + def packages(self) -> List[str]: + return [ + 'pipewire', + 'pipewire-alsa', + 'pipewire-jack', + 'pipewire-pulse', + 'gst-plugin-pipewire', + 'libpulse', + 'wireplumber' + ] + + def _enable_pipewire_for_all(self, install_session: 'Installer'): + users: Union[User, List[User]] = archinstall.arguments.get('!users', None) + if not isinstance(users, list): + users = [users] + + for user in users: + install_session.arch_chroot('systemctl enable --user pipewire-pulse.service', run_as=user.username) + + def install(self, install_session: 'Installer'): + super().install(install_session) + install_session.add_additional_packages(self.packages) + self._enable_pipewire_for_all(install_session) diff --git a/archinstall/default_profiles/custom.py b/archinstall/default_profiles/custom.py new file mode 100644 index 00000000..f7e100ed --- /dev/null +++ b/archinstall/default_profiles/custom.py @@ -0,0 +1,218 @@ +from typing import List, Dict, Optional, TYPE_CHECKING, Any + +from ..lib import menu +from archinstall.lib.output import log, FormattedOutput +from archinstall.lib.profile.profiles_handler import profile_handler +from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult, ProfileInfo, TProfile + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +class CustomProfileList(menu.ListManager): + def __init__(self, prompt: str, profiles: List[TProfile]): + self._actions = [ + str(_('Add profile')), + str(_('Edit profile')), + str(_('Delete profile')) + ] + super().__init__(prompt, profiles, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[TProfile]) -> Dict[str, Optional[TProfile]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any profile 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[TProfile]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, profile in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = profile + + return display_data + + def selected_action_display(self, profile: TProfile) -> str: + return profile.name + + def handle_action( + self, + action: str, + entry: Optional['CustomTypeProfile'], + data: List['CustomTypeProfile'] + ) -> List['CustomTypeProfile']: + if action == self._actions[0]: # add + new_profile = self._add_profile() + if new_profile is not None: + # in case a profile with the same name as an existing profile + # was created we'll replace the existing one + data = [d for d in data if d.name != new_profile.name] + data += [new_profile] + elif entry is not None: + if action == self._actions[1]: # edit + new_profile = self._add_profile(entry) + if new_profile is not None: + # we'll remove the original profile and add the modified version + data = [d for d in data if d.name != entry.name and d.name != new_profile.name] + data += [new_profile] + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] + + return data + + def _is_new_profile_name(self, name: str) -> bool: + existing_profile = profile_handler.get_profile_by_name(name) + if existing_profile is not None and existing_profile.profile_type != ProfileType.CustomType: + return False + return True + + def _add_profile(self, editing: Optional['CustomTypeProfile'] = None) -> Optional['CustomTypeProfile']: + name_prompt = '\n\n' + str(_('Profile name: ')) + + while True: + profile_name = menu.TextInput(name_prompt, editing.name if editing else '').run().strip() + + if not profile_name: + return None + + if not self._is_new_profile_name(profile_name): + error_prompt = str(_("The profile name you entered is already in use. Try again")) + print(error_prompt) + else: + break + + packages_prompt = str(_('Packages to be install with this profile (space separated, leave blank to skip): ')) + edit_packages = ' '.join(editing.packages) if editing else '' + packages = menu.TextInput(packages_prompt, edit_packages).run().strip() + + services_prompt = str(_('Services to be enabled with this profile (space separated, leave blank to skip): ')) + edit_services = ' '.join(editing.services) if editing else '' + services = menu.TextInput(services_prompt, edit_services).run().strip() + + choice = menu.Menu( + str(_('Should this profile be enabled for installation?')), + menu.Menu.yes_no(), + skip=False, + default_option=menu.Menu.no(), + clear_screen=False, + show_search_hint=False + ).run() + + enable_profile = True if choice.value == menu.Menu.yes() else False + + profile = CustomTypeProfile( + profile_name, + enabled=enable_profile, + packages=packages.split(' '), + services=services.split(' ') + ) + + return profile + + +# TODO +# Still needs some ironing out +class CustomProfile(): + def __init__(self): + super().__init__( + 'Custom', + ProfileType.Custom, + description=str(_('Create your own')) + ) + + def json(self) -> Dict[str, Any]: + data: Dict[str, Any] = {'main': self.name, 'gfx_driver': self.gfx_driver, 'custom': []} + + for profile in self._current_selection: + data['custom'].append({ + 'name': profile.name, + 'packages': profile.packages, + 'services': profile.services, + 'enabled': profile.custom_enabled + }) + + return data + + def do_on_select(self) -> SelectResult: + custom_profile_list = CustomProfileList('', profile_handler.get_custom_profiles()) + custom_profiles = custom_profile_list.run() + + # we'll first remove existing custom default_profiles with + # the same name and then add the new ones this + # will avoid errors of default_profiles with duplicate naming + profile_handler.remove_custom_profiles(custom_profiles) + profile_handler.add_custom_profiles(custom_profiles) + + self.set_current_selection(custom_profiles) + + if custom_profile_list.is_last_choice_cancel(): + return SelectResult.SameSelection + + enabled_profiles = [p for p in self._current_selection if p.custom_enabled] + # in case we only created inactive default_profiles we wanna store them but + # we want to reset the original setting + if not enabled_profiles: + return SelectResult.ResetCurrent + + return SelectResult.NewSelection + + def post_install(self, install_session: 'Installer'): + for profile in self._current_selection: + profile.post_install(install_session) + + def install(self, install_session: 'Installer'): + driver_packages = self.gfx_driver_packages() + install_session.add_additional_packages(driver_packages) + + for profile in self._current_selection: + if profile.custom_enabled: + log(f'Installing custom profile {profile.name}...') + + install_session.add_additional_packages(profile.packages) + install_session.enable_service(profile.services) + + profile.install(install_session) + + def info(self) -> Optional[ProfileInfo]: + enabled_profiles = [p for p in self._current_selection if p.custom_enabled] + if enabled_profiles: + details = ', '.join([p.name for p in enabled_profiles]) + gfx_driver = self.gfx_driver + return ProfileInfo(self.name, details, gfx_driver) + + return None + + def reset(self): + for profile in self._current_selection: + profile.set_enabled(False) + + self.gfx_driver = None + + +class CustomTypeProfile(Profile): + def __init__( + self, + name: str, + enabled: bool = False, + packages: List[str] = [], + services: List[str] = [] + ): + super().__init__( + name, + ProfileType.CustomType, + packages=packages, + services=services, + support_gfx_driver=True + ) + + self.custom_enabled = enabled + + def json(self) -> Dict[str, Any]: + return { + 'name': self.name, + 'packages': self.packages, + 'services': self.services, + 'enabled': self.custom_enabled + } diff --git a/archinstall/default_profiles/desktop.py b/archinstall/default_profiles/desktop.py new file mode 100644 index 00000000..2351bd08 --- /dev/null +++ b/archinstall/default_profiles/desktop.py @@ -0,0 +1,87 @@ +from typing import Any, TYPE_CHECKING, List, Optional, Dict + +from archinstall.lib import menu +from archinstall.lib.output import log +from archinstall.lib.profile.profiles_handler import profile_handler +from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult, GreeterType + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +class DesktopProfile(Profile): + def __init__(self, current_selection: List[Profile] = []): + super().__init__( + 'Desktop', + ProfileType.Desktop, + description=str(_('Provides a selection of desktop environments and tiling window managers, e.g. gnome, kde, sway')), + current_selection=current_selection, + support_greeter=True + ) + + @property + def packages(self) -> List[str]: + return [ + 'nano', + 'vim', + 'openssh', + 'htop', + 'wget', + 'iwd', + 'wireless_tools', + 'wpa_supplicant', + 'smartmontools', + 'xdg-utils' + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + combined_greeters: Dict[GreeterType, int] = {} + for profile in self.current_selection: + if profile.default_greeter_type: + combined_greeters.setdefault(profile.default_greeter_type, 0) + combined_greeters[profile.default_greeter_type] += 1 + + if len(combined_greeters) >= 1: + return list(combined_greeters)[0] + + return None + + def _do_on_select_profiles(self): + for profile in self.current_selection: + profile.do_on_select() + + def do_on_select(self) -> SelectResult: + choice = profile_handler.select_profile( + profile_handler.get_desktop_profiles(), + self._current_selection, + title=str(_('Select your desired desktop environment')), + multi=True + ) + + match choice.type_: + case menu.MenuSelectionType.Selection: + self.set_current_selection(choice.value) # type: ignore + self._do_on_select_profiles() + return SelectResult.NewSelection + case menu.MenuSelectionType.Skip: + return SelectResult.SameSelection + case menu.MenuSelectionType.Reset: + return SelectResult.ResetCurrent + + def post_install(self, install_session: 'Installer'): + for profile in self._current_selection: + profile.post_install(install_session) + + def install(self, install_session: 'Installer'): + # Install common packages for all desktop environments + install_session.add_additional_packages(self.packages) + + for profile in self._current_selection: + log(f'Installing profile {profile.name}...') + + install_session.add_additional_packages(profile.packages) + install_session.enable_service(profile.services) + + profile.install(install_session) diff --git a/archinstall/default_profiles/desktops/__init__.py b/archinstall/default_profiles/desktops/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/archinstall/default_profiles/desktops/awesome.py b/archinstall/default_profiles/desktops/awesome.py new file mode 100644 index 00000000..bb481914 --- /dev/null +++ b/archinstall/default_profiles/desktops/awesome.py @@ -0,0 +1,36 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +class AwesomeProfile(XorgProfile): + def __init__(self): + super().__init__('Awesome', ProfileType.WindowMgr, description='') + + @property + def packages(self) -> List[str]: + return ['alacritty'] + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() + + def install(self, install_session: 'Installer'): + super().install(install_session) + + # TODO: Copy a full configuration to ~/.config/awesome/rc.lua instead. + with open(f"{install_session.target}/etc/xdg/awesome/rc.lua", 'r') as fh: + awesome_lua = fh.read() + + # Replace xterm with alacritty for a smoother experience. + awesome_lua = awesome_lua.replace('"xterm"', '"alacritty"') + + with open(f"{install_session.target}/etc/xdg/awesome/rc.lua", 'w') as fh: + fh.write(awesome_lua) + + # TODO: Configure the right-click-menu to contain the above packages that were installed. (as a user config) diff --git a/archinstall/default_profiles/desktops/bspwm.py b/archinstall/default_profiles/desktops/bspwm.py new file mode 100644 index 00000000..f3bc982d --- /dev/null +++ b/archinstall/default_profiles/desktops/bspwm.py @@ -0,0 +1,30 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class BspwmProfile(XorgProfile): + def __init__(self): + super().__init__('Bspwm', ProfileType.WindowMgr, description='') + + @property + def packages(self) -> List[str]: + return [ + 'bspwm', + 'sxhkd', + 'dmenu', + 'xdo', + 'rxvt-unicode' + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/budgie.py b/archinstall/default_profiles/desktops/budgie.py new file mode 100644 index 00000000..32bd718d --- /dev/null +++ b/archinstall/default_profiles/desktops/budgie.py @@ -0,0 +1,30 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class BudgieProfile(XorgProfile): + def __init__(self): + super().__init__('Budgie', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + "arc-gtk-theme", + "budgie", + "mate-terminal", + "nemo", + "papirus-icon-theme", + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/cinnamon.py b/archinstall/default_profiles/desktops/cinnamon.py new file mode 100644 index 00000000..22fd0d9d --- /dev/null +++ b/archinstall/default_profiles/desktops/cinnamon.py @@ -0,0 +1,31 @@ +from typing import Optional, List, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class CinnamonProfile(XorgProfile): + def __init__(self): + super().__init__('Cinnamon', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + "cinnamon", + "system-config-printer", + "gnome-keyring", + "gnome-terminal", + "blueberry", + "metacity" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/cutefish.py b/archinstall/default_profiles/desktops/cutefish.py new file mode 100644 index 00000000..6f88c47a --- /dev/null +++ b/archinstall/default_profiles/desktops/cutefish.py @@ -0,0 +1,31 @@ +from typing import Optional, List, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +class CutefishProfile(XorgProfile): + def __init__(self): + super().__init__('Cutefish', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + "cutefish", + "noto-fonts" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Sddm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() + + def install(self, install_session: 'Installer'): + super().install(install_session) diff --git a/archinstall/default_profiles/desktops/deepin.py b/archinstall/default_profiles/desktops/deepin.py new file mode 100644 index 00000000..054c8fdf --- /dev/null +++ b/archinstall/default_profiles/desktops/deepin.py @@ -0,0 +1,28 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class DeepinProfile(XorgProfile): + def __init__(self): + super().__init__('Deepin', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + "deepin", + "deepin-terminal", + "deepin-editor" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/enlightenment.py b/archinstall/default_profiles/desktops/enlightenment.py new file mode 100644 index 00000000..164f64fe --- /dev/null +++ b/archinstall/default_profiles/desktops/enlightenment.py @@ -0,0 +1,27 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class EnlighenmentProfile(XorgProfile): + def __init__(self): + super().__init__('Enlightenment', ProfileType.WindowMgr, description='') + + @property + def packages(self) -> List[str]: + return [ + "enlightenment", + "terminology" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/gnome.py b/archinstall/default_profiles/desktops/gnome.py new file mode 100644 index 00000000..3cbd49bd --- /dev/null +++ b/archinstall/default_profiles/desktops/gnome.py @@ -0,0 +1,27 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class GnomeProfile(XorgProfile): + def __init__(self): + super().__init__('Gnome', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + 'gnome', + 'gnome-tweaks' + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Gdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/i3.py b/archinstall/default_profiles/desktops/i3.py new file mode 100644 index 00000000..7c6f24ca --- /dev/null +++ b/archinstall/default_profiles/desktops/i3.py @@ -0,0 +1,33 @@ +from typing import Optional, List, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class I3wmProfile(XorgProfile): + def __init__(self): + super().__init__('i3-wm', ProfileType.WindowMgr, description='') + + @property + def packages(self) -> List[str]: + return [ + 'i3-wm', + 'i3lock', + 'i3status', + 'i3blocks', + 'xterm', + 'lightdm-gtk-greeter', + 'lightdm', + 'dmenu', + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/kde.py b/archinstall/default_profiles/desktops/kde.py new file mode 100644 index 00000000..cd02e069 --- /dev/null +++ b/archinstall/default_profiles/desktops/kde.py @@ -0,0 +1,32 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class KdeProfile(XorgProfile): + def __init__(self): + super().__init__('Kde', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + "plasma-meta", + "konsole", + "kwrite", + "dolphin", + "ark", + "plasma-wayland-session", + "egl-wayland" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Sddm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/lxqt.py b/archinstall/default_profiles/desktops/lxqt.py new file mode 100644 index 00000000..146e168e --- /dev/null +++ b/archinstall/default_profiles/desktops/lxqt.py @@ -0,0 +1,35 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class LxqtProfile(XorgProfile): + def __init__(self): + super().__init__('Lxqt', ProfileType.DesktopEnv, description='') + + # NOTE: SDDM is the only officially supported greeter for LXQt, so unlike other DEs, lightdm is not used here. + # LXQt works with lightdm, but since this is not supported, we will not default to this. + # https://github.com/lxqt/lxqt/issues/795 + @property + def packages(self) -> List[str]: + return [ + "lxqt", + "breeze-icons", + "oxygen-icons", + "xdg-utils", + "ttf-freefont", + "leafpad", + "slock", + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Sddm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/mate.py b/archinstall/default_profiles/desktops/mate.py new file mode 100644 index 00000000..0ddaaaab --- /dev/null +++ b/archinstall/default_profiles/desktops/mate.py @@ -0,0 +1,27 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class MateProfile(XorgProfile): + def __init__(self): + super().__init__('Mate', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + "mate", + "mate-extra" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/qtile.py b/archinstall/default_profiles/desktops/qtile.py new file mode 100644 index 00000000..66c6fa1b --- /dev/null +++ b/archinstall/default_profiles/desktops/qtile.py @@ -0,0 +1,27 @@ +from typing import Optional, List, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class QtileProfile(XorgProfile): + def __init__(self): + super().__init__('Qtile', ProfileType.WindowMgr, description='') + + @property + def packages(self) -> List[str]: + return [ + 'qtile', + 'alacritty' + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/sway.py b/archinstall/default_profiles/desktops/sway.py new file mode 100644 index 00000000..519f5bbb --- /dev/null +++ b/archinstall/default_profiles/desktops/sway.py @@ -0,0 +1,66 @@ +from typing import List, Optional, TYPE_CHECKING, Any + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile +from archinstall.lib.menu import Menu + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +class SwayProfile(XorgProfile): + def __init__(self): + super().__init__( + 'Sway', + ProfileType.WindowMgr, + description='' + ) + self._control_preference = [] + + @property + def packages(self) -> List[str]: + return [ + "sway", + "swaybg", + "swaylock", + "swayidle", + "waybar", + "dmenu", + "brightnessctl", + "grim", + "slurp", + "pavucontrol", + "foot", + "xorg-xwayland" + ] + self._control_preference + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + @property + def services(self) -> List[str]: + if "seatd" in self._control_preference: + return ['seatd'] + elif "polkit" in self._control_preference: + return ['polkit'] + + return [] + + def _get_system_privelege_control_preference(self): + # need to activate seat service and add to seat group + title = str(_('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')) + title += str(_('\n\nChoose an option to give Sway access to your hardware')) + choice = Menu(title, ["polkit", "seatd"], skip=False).run() + self._control_preference = [choice.value] + + def do_on_select(self): + self._get_system_privelege_control_preference() + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() + + def install(self, install_session: 'Installer'): + super().install(install_session) diff --git a/archinstall/default_profiles/desktops/xfce4.py b/archinstall/default_profiles/desktops/xfce4.py new file mode 100644 index 00000000..bd6c3038 --- /dev/null +++ b/archinstall/default_profiles/desktops/xfce4.py @@ -0,0 +1,30 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class Xfce4Profile(XorgProfile): + def __init__(self): + super().__init__('Xfce4', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + "xfce4", + "xfce4-goodies", + "pavucontrol", + "gvfs", + "xarchiver" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/minimal.py b/archinstall/default_profiles/minimal.py new file mode 100644 index 00000000..f78708e9 --- /dev/null +++ b/archinstall/default_profiles/minimal.py @@ -0,0 +1,15 @@ +from typing import Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import Profile, ProfileType + +if TYPE_CHECKING: + _: Any + + +class MinimalProfile(Profile): + def __init__(self): + super().__init__( + 'Minimal', + ProfileType.Minimal, + description=str(_('A very basic installation that allows you to customize Arch Linux as you see fit.')) + ) diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py new file mode 100644 index 00000000..c7d6b3dc --- /dev/null +++ b/archinstall/default_profiles/profile.py @@ -0,0 +1,206 @@ +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum, auto +from typing import List, Optional, Any, Dict, TYPE_CHECKING, TypeVar + +from archinstall.lib.output import FormattedOutput + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +TProfile = TypeVar('TProfile', bound='Profile') + + +class ProfileType(Enum): + # top level default_profiles + Server = 'Server' + Desktop = 'Desktop' + Xorg = 'Xorg' + Minimal = 'Minimal' + Custom = 'Custom' + # detailed selection default_profiles + ServerType = 'ServerType' + WindowMgr = 'Window Manager' + DesktopEnv = 'Desktop Environment' + CustomType = 'CustomType' + # special things + Tailored = 'Tailored' + Application = 'Application' + + +class GreeterType(Enum): + Lightdm = 'lightdm' + Sddm = 'sddm' + Gdm = 'gdm' + + +class SelectResult(Enum): + NewSelection = auto() + SameSelection = auto() + ResetCurrent = auto() + + +@dataclass +class ProfileInfo: + name: str + details: Optional[str] + gfx_driver: Optional[str] = None + greeter: Optional[str] = None + + @property + def absolute_name(self) -> str: + if self.details is not None: + return self.details + return self.name + + +class Profile: + def __init__( + self, + name: str, + profile_type: ProfileType, + description: str = '', + current_selection: List[TProfile] = [], + packages: List[str] = [], + services: List[str] = [], + support_gfx_driver: bool = False, + support_greeter: bool = False + ): + self.name = name + self.description = description + self.profile_type = profile_type + self._support_gfx_driver = support_gfx_driver + self._support_greeter = support_greeter + + # self.gfx_driver: Optional[str] = None + + self._current_selection = current_selection + self._packages = packages + self._services = services + + # Only used for custom default_profiles + self.custom_enabled = False + + @property + def current_selection(self) -> List[TProfile]: + return self._current_selection + + @property + def packages(self) -> List[str]: + """ + Returns a list of packages that should be installed when + this profile is among the choosen ones + """ + return self._packages + + @property + def services(self) -> List[str]: + """ + Returns a list of services that should be enabled when + this profile is among the chosen ones + """ + return self._services + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + """ + Setting a default greeter type for a desktop profile + """ + return None + + def install(self, install_session: 'Installer'): + """ + Performs installation steps when this profile was selected + """ + + def post_install(self, install_session: 'Installer'): + """ + Hook that will be called when the installation process is + finished and custom installation steps for specific default_profiles + are needed + """ + + def json(self) -> Dict: + """ + Returns a json representation of the profile + """ + return {} + + def do_on_select(self) -> SelectResult: + """ + Hook that will be called when a profile is selected + """ + return SelectResult.NewSelection + + def current_selection_names(self) -> List[str]: + if self._current_selection: + return [s.name for s in self._current_selection] + return [] + + def reset(self): + self.set_current_selection([]) + + def set_current_selection(self, current_selection: List[TProfile]): + self._current_selection = current_selection + + def is_top_level_profile(self) -> bool: + top_levels = [ProfileType.Desktop, ProfileType.Server, ProfileType.Xorg, ProfileType.Minimal, ProfileType.Custom] + return self.profile_type in top_levels + + def is_desktop_profile(self) -> bool: + return self.profile_type == ProfileType.Desktop + + def is_server_type_profile(self) -> bool: + return self.profile_type == ProfileType.ServerType + + def is_desktop_type_profile(self) -> bool: + return self.profile_type == ProfileType.DesktopEnv or self.profile_type == ProfileType.WindowMgr + + def is_xorg_type_profile(self) -> bool: + return self.profile_type == ProfileType.Xorg + + def is_tailored(self) -> bool: + return self.profile_type == ProfileType.Tailored + + def is_custom_type_profile(self) -> bool: + return self.profile_type == ProfileType.CustomType + + def is_graphic_driver_supported(self) -> bool: + if not self._current_selection: + return self._support_gfx_driver + else: + if any([p._support_gfx_driver for p in self._current_selection]): + return True + return False + + def is_greeter_supported(self) -> bool: + return self._support_greeter + + 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. + If no preview or a different text should be displayed just + """ + if self.description: + return self.description + 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 diff --git a/archinstall/default_profiles/server.py b/archinstall/default_profiles/server.py new file mode 100644 index 00000000..e240b3ef --- /dev/null +++ b/archinstall/default_profiles/server.py @@ -0,0 +1,57 @@ +import logging +from typing import Any, TYPE_CHECKING, List + +from archinstall.lib.output import log +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 + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +class ServerProfile(Profile): + def __init__(self, current_value: List[TProfile] = []): + super().__init__( + 'Server', + ProfileType.Server, + description=str(_('Provides a selection of various server packages to install and enable, e.g. httpd, nginx, mariadb')), + current_selection=current_value + ) + + def do_on_select(self) -> SelectResult: + available_servers = profile_handler.get_server_profiles() + + choice = profile_handler.select_profile( + available_servers, + self._current_selection, + title=str(_('Choose which servers to install, if none then a minimal installation will be done')), + multi=True + ) + + match choice.type_: + case MenuSelectionType.Selection: + self.set_current_selection(choice.value) # type: ignore + return SelectResult.NewSelection + case MenuSelectionType.Skip: + return SelectResult.SameSelection + case MenuSelectionType.Reset: + return SelectResult.ResetCurrent + + def post_install(self, install_session: 'Installer'): + for profile in self._current_selection: + profile.post_install(install_session) + + 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) + + for server in self._current_selection: + log(f'Installing {server.name}...', level=logging.INFO) + 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) diff --git a/archinstall/default_profiles/servers/__init__.py b/archinstall/default_profiles/servers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/archinstall/default_profiles/servers/cockpit.py b/archinstall/default_profiles/servers/cockpit.py new file mode 100644 index 00000000..8cac0976 --- /dev/null +++ b/archinstall/default_profiles/servers/cockpit.py @@ -0,0 +1,19 @@ +from typing import List + +from archinstall.default_profiles.profile import Profile, ProfileType + + +class CockpitProfile(Profile): + def __init__(self): + super().__init__( + 'Cockpit', + ProfileType.ServerType + ) + + @property + def packages(self) -> List[str]: + return ['cockpit', 'udisks2', 'packagekit'] + + @property + def services(self) -> List[str]: + return ['cockpit.socket'] diff --git a/archinstall/default_profiles/servers/docker.py b/archinstall/default_profiles/servers/docker.py new file mode 100644 index 00000000..e6e17831 --- /dev/null +++ b/archinstall/default_profiles/servers/docker.py @@ -0,0 +1,33 @@ +from typing import List, Union, TYPE_CHECKING + +import archinstall + +from archinstall.default_profiles.profile import Profile, ProfileType +from archinstall.lib.models import User + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + + +class DockerProfile(Profile): + def __init__(self): + super().__init__( + 'Docker', + ProfileType.ServerType + ) + + @property + def packages(self) -> List[str]: + return ['docker'] + + @property + def services(self) -> List[str]: + return ['docker'] + + def post_install(self, install_session: 'Installer'): + users: Union[User, List[User]] = archinstall.arguments.get('!users', None) + if not isinstance(users, list): + users = [users] + + for user in users: + install_session.arch_chroot(f'usermod -a -G docker {user.username}') diff --git a/archinstall/default_profiles/servers/httpd.py b/archinstall/default_profiles/servers/httpd.py new file mode 100644 index 00000000..595ce84f --- /dev/null +++ b/archinstall/default_profiles/servers/httpd.py @@ -0,0 +1,19 @@ +from typing import List + +from archinstall.default_profiles.profile import Profile, ProfileType + + +class HttpdProfile(Profile): + def __init__(self): + super().__init__( + 'httpd', + ProfileType.ServerType + ) + + @property + def packages(self) -> List[str]: + return ['apache'] + + @property + def services(self) -> List[str]: + return ['httpd'] diff --git a/archinstall/default_profiles/servers/lighttpd.py b/archinstall/default_profiles/servers/lighttpd.py new file mode 100644 index 00000000..00aa5564 --- /dev/null +++ b/archinstall/default_profiles/servers/lighttpd.py @@ -0,0 +1,19 @@ +from typing import List + +from archinstall.default_profiles.profile import Profile, ProfileType + + +class LighttpdProfile(Profile): + def __init__(self): + super().__init__( + 'Lighttpd', + ProfileType.ServerType + ) + + @property + def packages(self) -> List[str]: + return ['lighttpd'] + + @property + def services(self) -> List[str]: + return ['lighttpd'] diff --git a/archinstall/default_profiles/servers/mariadb.py b/archinstall/default_profiles/servers/mariadb.py new file mode 100644 index 00000000..4506f1bc --- /dev/null +++ b/archinstall/default_profiles/servers/mariadb.py @@ -0,0 +1,25 @@ +from typing import List, TYPE_CHECKING + +from archinstall.default_profiles.profile import Profile, ProfileType + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + + +class MariadbProfile(Profile): + def __init__(self): + super().__init__( + 'Mariadb', + ProfileType.ServerType + ) + + @property + def packages(self) -> List[str]: + return ['mariadb'] + + @property + def services(self) -> List[str]: + return ['mariadb'] + + def post_install(self, install_session: 'Installer'): + install_session.arch_chroot('mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql') diff --git a/archinstall/default_profiles/servers/nginx.py b/archinstall/default_profiles/servers/nginx.py new file mode 100644 index 00000000..6038616c --- /dev/null +++ b/archinstall/default_profiles/servers/nginx.py @@ -0,0 +1,19 @@ +from typing import List + +from archinstall.default_profiles.profile import Profile, ProfileType + + +class NginxProfile(Profile): + def __init__(self): + super().__init__( + 'Nginx', + ProfileType.ServerType + ) + + @property + def packages(self) -> List[str]: + return ['nginx'] + + @property + def services(self) -> List[str]: + return ['nginx'] diff --git a/archinstall/default_profiles/servers/postgresql.py b/archinstall/default_profiles/servers/postgresql.py new file mode 100644 index 00000000..dba722ce --- /dev/null +++ b/archinstall/default_profiles/servers/postgresql.py @@ -0,0 +1,26 @@ +from typing import List, TYPE_CHECKING + +from archinstall.default_profiles.profile import Profile, ProfileType + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + + +class PostgresqlProfile(Profile): + def __init__(self): + super().__init__( + 'Postgresql', + ProfileType.ServerType, + '' + ) + + @property + def packages(self) -> List[str]: + return ['postgresql'] + + @property + def services(self) -> List[str]: + return ['postgresql'] + + def post_install(self, install_session: 'Installer'): + install_session.arch_chroot("initdb -D /var/lib/postgres/data", run_as='postgres') diff --git a/archinstall/default_profiles/servers/sshd.py b/archinstall/default_profiles/servers/sshd.py new file mode 100644 index 00000000..7f855b1a --- /dev/null +++ b/archinstall/default_profiles/servers/sshd.py @@ -0,0 +1,19 @@ +from typing import List + +from archinstall.default_profiles.profile import Profile, ProfileType + + +class SshdProfile(Profile): + def __init__(self): + super().__init__( + 'sshd', + ProfileType.ServerType + ) + + @property + def packages(self) -> List[str]: + return ['openssh'] + + @property + def services(self) -> List[str]: + return ['sshd'] diff --git a/archinstall/default_profiles/servers/tomcat.py b/archinstall/default_profiles/servers/tomcat.py new file mode 100644 index 00000000..9bd8837b --- /dev/null +++ b/archinstall/default_profiles/servers/tomcat.py @@ -0,0 +1,19 @@ +from typing import List + +from archinstall.default_profiles.profile import Profile, ProfileType + + +class TomcatProfile(Profile): + def __init__(self): + super().__init__( + 'Tomcat', + ProfileType.ServerType + ) + + @property + def packages(self) -> List[str]: + return ['tomcat10'] + + @property + def services(self) -> List[str]: + return ['tomcat10'] diff --git a/archinstall/default_profiles/tailored.py b/archinstall/default_profiles/tailored.py new file mode 100644 index 00000000..62666249 --- /dev/null +++ b/archinstall/default_profiles/tailored.py @@ -0,0 +1,21 @@ +from typing import List, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +class TailoredProfile(XorgProfile): + def __init__(self): + super().__init__('52-54-00-12-34-56', ProfileType.Tailored, description='') + + @property + def packages(self) -> List[str]: + return ['nano', 'wget', 'git'] + + def install(self, install_session: 'Installer'): + super().install(install_session) + # do whatever you like here :) diff --git a/archinstall/default_profiles/xorg.py b/archinstall/default_profiles/xorg.py new file mode 100644 index 00000000..553421a4 --- /dev/null +++ b/archinstall/default_profiles/xorg.py @@ -0,0 +1,21 @@ +from typing import Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import Profile, ProfileType + +if TYPE_CHECKING: + _: Any + + +class XorgProfile(Profile): + def __init__( + self, + name: str = 'Xorg', + profile_type: ProfileType = ProfileType.Xorg, + description: str = str(_('Installs a minimal system as well as xorg and graphics drivers.')), + ): + super().__init__( + name, + profile_type, + description=description, + support_gfx_driver=True + ) -- cgit v1.2.3-54-g00ecf From 32982b880bd4e76b158442e444c001f8ebd8bb06 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sat, 22 Apr 2023 16:04:36 +1000 Subject: Fix import and configuration loading issues (#1764) * Fix import and configuration loading issues * Fix mypy --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 8 +- archinstall/default_profiles/custom.py | 436 ++++++++++++++-------------- archinstall/lib/profile/profiles_handler.py | 44 +-- mypy.ini | 3 +- 4 files changed, 245 insertions(+), 246 deletions(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 8cb6ced9..3d0768a5 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -4,7 +4,7 @@ from argparse import ArgumentParser, Namespace from .lib import disk from .lib import menu -from .lib import models as models +from .lib import models from .lib import packages from .lib.exceptions import * @@ -203,17 +203,17 @@ def load_config(): storage['_selected_servers'] = arguments.get('servers', None) if arguments.get('nic', None) is not None: - handler = NetworkConfigurationHandler() + handler = models.NetworkConfigurationHandler() handler.parse_arguments(arguments.get('nic')) arguments['nic'] = handler.configuration if arguments.get('!users', None) is not None or arguments.get('!superusers', None) is not None: users = arguments.get('!users', None) superusers = arguments.get('!superusers', None) - arguments['!users'] = User.parse_arguments(users, superusers) + arguments['!users'] = models.User.parse_arguments(users, superusers) if arguments.get('bootloader', None) is not None: - arguments['bootloader'] = Bootloader.from_arg(arguments['bootloader']) + arguments['bootloader'] = models.Bootloader.from_arg(arguments['bootloader']) if arguments.get('disk_encryption', None) is not None and disk_config is not None: password = arguments.get('encryption_password', '') diff --git a/archinstall/default_profiles/custom.py b/archinstall/default_profiles/custom.py index f7e100ed..5f9db620 100644 --- a/archinstall/default_profiles/custom.py +++ b/archinstall/default_profiles/custom.py @@ -1,218 +1,218 @@ -from typing import List, Dict, Optional, TYPE_CHECKING, Any - -from ..lib import menu -from archinstall.lib.output import log, FormattedOutput -from archinstall.lib.profile.profiles_handler import profile_handler -from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult, ProfileInfo, TProfile - -if TYPE_CHECKING: - from archinstall.lib.installer import Installer - _: Any - - -class CustomProfileList(menu.ListManager): - def __init__(self, prompt: str, profiles: List[TProfile]): - self._actions = [ - str(_('Add profile')), - str(_('Edit profile')), - str(_('Delete profile')) - ] - super().__init__(prompt, profiles, [self._actions[0]], self._actions[1:]) - - def reformat(self, data: List[TProfile]) -> Dict[str, Optional[TProfile]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any profile 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[TProfile]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, profile in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = profile - - return display_data - - def selected_action_display(self, profile: TProfile) -> str: - return profile.name - - def handle_action( - self, - action: str, - entry: Optional['CustomTypeProfile'], - data: List['CustomTypeProfile'] - ) -> List['CustomTypeProfile']: - if action == self._actions[0]: # add - new_profile = self._add_profile() - if new_profile is not None: - # in case a profile with the same name as an existing profile - # was created we'll replace the existing one - data = [d for d in data if d.name != new_profile.name] - data += [new_profile] - elif entry is not None: - if action == self._actions[1]: # edit - new_profile = self._add_profile(entry) - if new_profile is not None: - # we'll remove the original profile and add the modified version - data = [d for d in data if d.name != entry.name and d.name != new_profile.name] - data += [new_profile] - elif action == self._actions[2]: # delete - data = [d for d in data if d != entry] - - return data - - def _is_new_profile_name(self, name: str) -> bool: - existing_profile = profile_handler.get_profile_by_name(name) - if existing_profile is not None and existing_profile.profile_type != ProfileType.CustomType: - return False - return True - - def _add_profile(self, editing: Optional['CustomTypeProfile'] = None) -> Optional['CustomTypeProfile']: - name_prompt = '\n\n' + str(_('Profile name: ')) - - while True: - profile_name = menu.TextInput(name_prompt, editing.name if editing else '').run().strip() - - if not profile_name: - return None - - if not self._is_new_profile_name(profile_name): - error_prompt = str(_("The profile name you entered is already in use. Try again")) - print(error_prompt) - else: - break - - packages_prompt = str(_('Packages to be install with this profile (space separated, leave blank to skip): ')) - edit_packages = ' '.join(editing.packages) if editing else '' - packages = menu.TextInput(packages_prompt, edit_packages).run().strip() - - services_prompt = str(_('Services to be enabled with this profile (space separated, leave blank to skip): ')) - edit_services = ' '.join(editing.services) if editing else '' - services = menu.TextInput(services_prompt, edit_services).run().strip() - - choice = menu.Menu( - str(_('Should this profile be enabled for installation?')), - menu.Menu.yes_no(), - skip=False, - default_option=menu.Menu.no(), - clear_screen=False, - show_search_hint=False - ).run() - - enable_profile = True if choice.value == menu.Menu.yes() else False - - profile = CustomTypeProfile( - profile_name, - enabled=enable_profile, - packages=packages.split(' '), - services=services.split(' ') - ) - - return profile - - -# TODO -# Still needs some ironing out -class CustomProfile(): - def __init__(self): - super().__init__( - 'Custom', - ProfileType.Custom, - description=str(_('Create your own')) - ) - - def json(self) -> Dict[str, Any]: - data: Dict[str, Any] = {'main': self.name, 'gfx_driver': self.gfx_driver, 'custom': []} - - for profile in self._current_selection: - data['custom'].append({ - 'name': profile.name, - 'packages': profile.packages, - 'services': profile.services, - 'enabled': profile.custom_enabled - }) - - return data - - def do_on_select(self) -> SelectResult: - custom_profile_list = CustomProfileList('', profile_handler.get_custom_profiles()) - custom_profiles = custom_profile_list.run() - - # we'll first remove existing custom default_profiles with - # the same name and then add the new ones this - # will avoid errors of default_profiles with duplicate naming - profile_handler.remove_custom_profiles(custom_profiles) - profile_handler.add_custom_profiles(custom_profiles) - - self.set_current_selection(custom_profiles) - - if custom_profile_list.is_last_choice_cancel(): - return SelectResult.SameSelection - - enabled_profiles = [p for p in self._current_selection if p.custom_enabled] - # in case we only created inactive default_profiles we wanna store them but - # we want to reset the original setting - if not enabled_profiles: - return SelectResult.ResetCurrent - - return SelectResult.NewSelection - - def post_install(self, install_session: 'Installer'): - for profile in self._current_selection: - profile.post_install(install_session) - - def install(self, install_session: 'Installer'): - driver_packages = self.gfx_driver_packages() - install_session.add_additional_packages(driver_packages) - - for profile in self._current_selection: - if profile.custom_enabled: - log(f'Installing custom profile {profile.name}...') - - install_session.add_additional_packages(profile.packages) - install_session.enable_service(profile.services) - - profile.install(install_session) - - def info(self) -> Optional[ProfileInfo]: - enabled_profiles = [p for p in self._current_selection if p.custom_enabled] - if enabled_profiles: - details = ', '.join([p.name for p in enabled_profiles]) - gfx_driver = self.gfx_driver - return ProfileInfo(self.name, details, gfx_driver) - - return None - - def reset(self): - for profile in self._current_selection: - profile.set_enabled(False) - - self.gfx_driver = None - - -class CustomTypeProfile(Profile): - def __init__( - self, - name: str, - enabled: bool = False, - packages: List[str] = [], - services: List[str] = [] - ): - super().__init__( - name, - ProfileType.CustomType, - packages=packages, - services=services, - support_gfx_driver=True - ) - - self.custom_enabled = enabled - - def json(self) -> Dict[str, Any]: - return { - 'name': self.name, - 'packages': self.packages, - 'services': self.services, - 'enabled': self.custom_enabled - } +# from typing import List, Dict, Optional, TYPE_CHECKING, Any +# +# from ..lib import menu +# from archinstall.lib.output import log, FormattedOutput +# from archinstall.lib.profile.profiles_handler import profile_handler +# from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult, ProfileInfo, TProfile +# +# if TYPE_CHECKING: +# from archinstall.lib.installer import Installer +# _: Any +# +# +# class CustomProfileList(menu.ListManager): +# def __init__(self, prompt: str, profiles: List[TProfile]): +# self._actions = [ +# str(_('Add profile')), +# str(_('Edit profile')), +# str(_('Delete profile')) +# ] +# super().__init__(prompt, profiles, [self._actions[0]], self._actions[1:]) +# +# def reformat(self, data: List[TProfile]) -> Dict[str, Optional[TProfile]]: +# table = FormattedOutput.as_table(data) +# rows = table.split('\n') +# +# # these are the header rows of the table and do not map to any profile 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[TProfile]] = {f' {rows[0]}': None, f' {rows[1]}': None} +# +# for row, profile in zip(rows[2:], data): +# row = row.replace('|', '\\|') +# display_data[row] = profile +# +# return display_data +# +# def selected_action_display(self, profile: TProfile) -> str: +# return profile.name +# +# def handle_action( +# self, +# action: str, +# entry: Optional['CustomTypeProfile'], +# data: List['CustomTypeProfile'] +# ) -> List['CustomTypeProfile']: +# if action == self._actions[0]: # add +# new_profile = self._add_profile() +# if new_profile is not None: +# # in case a profile with the same name as an existing profile +# # was created we'll replace the existing one +# data = [d for d in data if d.name != new_profile.name] +# data += [new_profile] +# elif entry is not None: +# if action == self._actions[1]: # edit +# new_profile = self._add_profile(entry) +# if new_profile is not None: +# # we'll remove the original profile and add the modified version +# data = [d for d in data if d.name != entry.name and d.name != new_profile.name] +# data += [new_profile] +# elif action == self._actions[2]: # delete +# data = [d for d in data if d != entry] +# +# return data +# +# def _is_new_profile_name(self, name: str) -> bool: +# existing_profile = profile_handler.get_profile_by_name(name) +# if existing_profile is not None and existing_profile.profile_type != ProfileType.CustomType: +# return False +# return True +# +# def _add_profile(self, editing: Optional['CustomTypeProfile'] = None) -> Optional['CustomTypeProfile']: +# name_prompt = '\n\n' + str(_('Profile name: ')) +# +# while True: +# profile_name = menu.TextInput(name_prompt, editing.name if editing else '').run().strip() +# +# if not profile_name: +# return None +# +# if not self._is_new_profile_name(profile_name): +# error_prompt = str(_("The profile name you entered is already in use. Try again")) +# print(error_prompt) +# else: +# break +# +# packages_prompt = str(_('Packages to be install with this profile (space separated, leave blank to skip): ')) +# edit_packages = ' '.join(editing.packages) if editing else '' +# packages = menu.TextInput(packages_prompt, edit_packages).run().strip() +# +# services_prompt = str(_('Services to be enabled with this profile (space separated, leave blank to skip): ')) +# edit_services = ' '.join(editing.services) if editing else '' +# services = menu.TextInput(services_prompt, edit_services).run().strip() +# +# choice = menu.Menu( +# str(_('Should this profile be enabled for installation?')), +# menu.Menu.yes_no(), +# skip=False, +# default_option=menu.Menu.no(), +# clear_screen=False, +# show_search_hint=False +# ).run() +# +# enable_profile = True if choice.value == menu.Menu.yes() else False +# +# profile = CustomTypeProfile( +# profile_name, +# enabled=enable_profile, +# packages=packages.split(' '), +# services=services.split(' ') +# ) +# +# return profile +# +# +# # TODO +# # Still needs some ironing out +# class CustomProfile(): +# def __init__(self): +# super().__init__( +# 'Custom', +# ProfileType.Custom, +# description=str(_('Create your own')) +# ) +# +# def json(self) -> Dict[str, Any]: +# data: Dict[str, Any] = {'main': self.name, 'gfx_driver': self.gfx_driver, 'custom': []} +# +# for profile in self._current_selection: +# data['custom'].append({ +# 'name': profile.name, +# 'packages': profile.packages, +# 'services': profile.services, +# 'enabled': profile.custom_enabled +# }) +# +# return data +# +# def do_on_select(self) -> SelectResult: +# custom_profile_list = CustomProfileList('', profile_handler.get_custom_profiles()) +# custom_profiles = custom_profile_list.run() +# +# # we'll first remove existing custom default_profiles with +# # the same name and then add the new ones this +# # will avoid errors of default_profiles with duplicate naming +# profile_handler.remove_custom_profiles(custom_profiles) +# profile_handler.add_custom_profiles(custom_profiles) +# +# self.set_current_selection(custom_profiles) +# +# if custom_profile_list.is_last_choice_cancel(): +# return SelectResult.SameSelection +# +# enabled_profiles = [p for p in self._current_selection if p.custom_enabled] +# # in case we only created inactive default_profiles we wanna store them but +# # we want to reset the original setting +# if not enabled_profiles: +# return SelectResult.ResetCurrent +# +# return SelectResult.NewSelection +# +# def post_install(self, install_session: 'Installer'): +# for profile in self._current_selection: +# profile.post_install(install_session) +# +# def install(self, install_session: 'Installer'): +# driver_packages = self.gfx_driver_packages() +# install_session.add_additional_packages(driver_packages) +# +# for profile in self._current_selection: +# if profile.custom_enabled: +# log(f'Installing custom profile {profile.name}...') +# +# install_session.add_additional_packages(profile.packages) +# install_session.enable_service(profile.services) +# +# profile.install(install_session) +# +# def info(self) -> Optional[ProfileInfo]: +# enabled_profiles = [p for p in self._current_selection if p.custom_enabled] +# if enabled_profiles: +# details = ', '.join([p.name for p in enabled_profiles]) +# gfx_driver = self.gfx_driver +# return ProfileInfo(self.name, details, gfx_driver) +# +# return None +# +# def reset(self): +# for profile in self._current_selection: +# profile.set_enabled(False) +# +# self.gfx_driver = None +# +# +# class CustomTypeProfile(Profile): +# def __init__( +# self, +# name: str, +# enabled: bool = False, +# packages: List[str] = [], +# services: List[str] = [] +# ): +# super().__init__( +# name, +# ProfileType.CustomType, +# packages=packages, +# services=services, +# support_gfx_driver=True +# ) +# +# self.custom_enabled = enabled +# +# def json(self) -> Dict[str, Any]: +# return { +# 'name': self.name, +# 'packages': self.packages, +# 'services': self.services, +# 'enabled': self.custom_enabled +# } diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 063b12ea..a8b5cc22 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -71,28 +71,28 @@ class ProfileHandler: else: self._import_profile_from_url(url_path) - if custom := profile_config.get('custom', None): - from archinstall.default_profiles.custom import CustomTypeProfile - custom_types = [] - - for entry in custom: - custom_types.append( - CustomTypeProfile( - entry['name'], - entry['enabled'], - entry.get('packages', []), - entry.get('services', []) - ) - ) - - self.remove_custom_profiles(custom_types) - self.add_custom_profiles(custom_types) - - # this doesn't mean it's actual going to be set as a selection - # but we are simply populating the custom profile with all - # possible custom definitions - if custom_profile := self.get_profile_by_name('Custom'): - custom_profile.set_current_selection(custom_types) + # if custom := profile_config.get('custom', None): + # from archinstall.default_profiles.custom import CustomTypeProfile + # custom_types = [] + # + # for entry in custom: + # custom_types.append( + # CustomTypeProfile( + # entry['name'], + # entry['enabled'], + # entry.get('packages', []), + # entry.get('services', []) + # ) + # ) + # + # self.remove_custom_profiles(custom_types) + # self.add_custom_profiles(custom_types) + # + # # this doesn't mean it's actual going to be set as a selection + # # but we are simply populating the custom profile with all + # # possible custom definitions + # if custom_profile := self.get_profile_by_name('Custom'): + # custom_profile.set_current_selection(custom_types) if main := profile_config.get('main', None): profile = self.get_profile_by_name(main) if main else None diff --git a/mypy.ini b/mypy.ini index 0add1eb1..a08b2d88 100644 --- a/mypy.ini +++ b/mypy.ini @@ -10,6 +10,5 @@ exclude = (?x)(^archinstall/lib/disk/btrfs/btrfssubvolumeinfo\.py$ | ^archinstall/lib/installer\.py$ | ^archinstall/lib/systemd\.py$ | ^archinstall/lib/user_interaction/general_conf\.py$ - | ^archinstall/lib/user_interaction/locale_conf\.py$ - | ^archinstall/default_profiles/custom\.py$) + | ^archinstall/lib/user_interaction/locale_conf\.py$) files = archinstall/ -- cgit v1.2.3-54-g00ecf From 15199081e542caf38c9c0f163af764616d9420ef Mon Sep 17 00:00:00 2001 From: xenium <76689618+xeniumcode@users.noreply.github.com> Date: Thu, 4 May 2023 21:00:40 +0530 Subject: Adding Hyperland wm (#1785) --- archinstall/default_profiles/desktops/hyperland.py | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 archinstall/default_profiles/desktops/hyperland.py (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/hyperland.py b/archinstall/default_profiles/desktops/hyperland.py new file mode 100644 index 00000000..e55dd7c4 --- /dev/null +++ b/archinstall/default_profiles/desktops/hyperland.py @@ -0,0 +1,31 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class HyperlandProfile(XorgProfile): + def __init__(self): + super().__init__('Hyperland', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + "hyprland", + "dunst", + "xdg-desktop-portal-hyprland", + "kitty", + "qt5-wayland", + "qt6-wayland" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Sddm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() -- cgit v1.2.3-54-g00ecf From 235c1190b003b8f69ff344d6e2ac31f4bd1d0ba2 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sat, 6 May 2023 16:09:58 +1000 Subject: Fix 1763 (#1790) * Fix 1763 --------- Co-authored-by: Daniel Girtler --- archinstall/default_profiles/desktops/sway.py | 37 +++++++++++++++++++-------- archinstall/default_profiles/profile.py | 25 ++++++++---------- archinstall/lib/profile/profiles_handler.py | 8 ++++++ 3 files changed, 44 insertions(+), 26 deletions(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/sway.py b/archinstall/default_profiles/desktops/sway.py index 519f5bbb..ae814e46 100644 --- a/archinstall/default_profiles/desktops/sway.py +++ b/archinstall/default_profiles/desktops/sway.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import List, Optional, TYPE_CHECKING, Any from archinstall.default_profiles.profile import ProfileType, GreeterType @@ -9,6 +10,11 @@ if TYPE_CHECKING: _: Any +class SeatAccess(Enum): + seatd = 'seatd' + polkit = 'polkit' + + class SwayProfile(XorgProfile): def __init__(self): super().__init__( @@ -16,10 +22,15 @@ class SwayProfile(XorgProfile): ProfileType.WindowMgr, description='' ) - self._control_preference = [] + + self.custom_settings = {'seat_access': None} @property def packages(self) -> List[str]: + additional = [] + if seat := self.custom_settings.get('seat_access', None): + additional = [seat] + return [ "sway", "swaybg", @@ -33,7 +44,7 @@ class SwayProfile(XorgProfile): "pavucontrol", "foot", "xorg-xwayland" - ] + self._control_preference + ] + additional @property def default_greeter_type(self) -> Optional[GreeterType]: @@ -41,22 +52,26 @@ class SwayProfile(XorgProfile): @property def services(self) -> List[str]: - if "seatd" in self._control_preference: - return ['seatd'] - elif "polkit" in self._control_preference: - return ['polkit'] - + if pref := self.custom_settings.get('seat_access', None): + return [pref.value] return [] - def _get_system_privelege_control_preference(self): + def _ask_seat_access(self): # need to activate seat service and add to seat group title = str(_('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')) title += str(_('\n\nChoose an option to give Sway access to your hardware')) - choice = Menu(title, ["polkit", "seatd"], skip=False).run() - self._control_preference = [choice.value] + + options = [e.value for e in SeatAccess] + default = None + + if seat := self.custom_settings.get('seat_access', None): + default = seat + + choice = Menu(title, options, skip=False, preset_values=default).run() + self.custom_settings['seat_access'] = choice.single_value def do_on_select(self): - self._get_system_privelege_control_preference() + self._ask_seat_access() def preview_text(self) -> Optional[str]: text = str(_('Environment type: {}')).format(self.profile_type.value) diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index c7d6b3dc..b1ad1f50 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -1,6 +1,5 @@ from __future__ import annotations -from dataclasses import dataclass from enum import Enum, auto from typing import List, Optional, Any, Dict, TYPE_CHECKING, TypeVar @@ -43,20 +42,6 @@ class SelectResult(Enum): ResetCurrent = auto() -@dataclass -class ProfileInfo: - name: str - details: Optional[str] - gfx_driver: Optional[str] = None - greeter: Optional[str] = None - - @property - def absolute_name(self) -> str: - if self.details is not None: - return self.details - return self.name - - class Profile: def __init__( self, @@ -72,6 +57,8 @@ class Profile: self.name = name self.description = description self.profile_type = profile_type + self.custom_settings: Dict[str, Any] = {} + self._support_gfx_driver = support_gfx_driver self._support_greeter = support_greeter @@ -135,6 +122,14 @@ class Profile: """ return SelectResult.NewSelection + def set_custom_settings(self, settings: Dict[str, Any]): + """ + 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 + """ + self.custom_settings = settings + def current_selection_names(self) -> List[str]: if self._current_selection: return [s.name for s in self._current_selection] diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 824849c3..6ed95f8e 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -43,6 +43,7 @@ class ProfileHandler: data = { 'main': profile.name, 'details': [profile.name for profile in profile.current_selection], + 'custom_settings': {profile.name: profile.custom_settings for profile in profile.current_selection} } if self._url_path is not None: @@ -98,6 +99,7 @@ class ProfileHandler: profile = self.get_profile_by_name(main) if main else None valid: List[Profile] = [] + if details := profile_config.get('details', []): resolved = {detail: self.get_profile_by_name(detail) for detail in details if detail} valid = [p for p in resolved.values() if p is not None] @@ -106,6 +108,12 @@ class ProfileHandler: if invalid: log(f'No profile definition found: {invalid}') + custom_settings = profile_config.get('custom_settings', {}) + for profile in valid: + profile.set_custom_settings( + custom_settings.get(profile.name, {}) + ) + if profile is not None: profile.set_current_selection(valid) -- cgit v1.2.3-54-g00ecf From c0a7d45a83a8a6a8a7497362364d899a7ae8843d Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sun, 7 May 2023 16:40:44 +1000 Subject: Fix 1792 (#1795) Co-authored-by: Daniel Girtler --- archinstall/default_profiles/applications/pipewire.py | 2 +- archinstall/default_profiles/servers/docker.py | 2 +- archinstall/lib/configuration.py | 2 +- archinstall/lib/global_menu.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/applications/pipewire.py b/archinstall/default_profiles/applications/pipewire.py index 5d2e5ea3..4cb75968 100644 --- a/archinstall/default_profiles/applications/pipewire.py +++ b/archinstall/default_profiles/applications/pipewire.py @@ -27,7 +27,7 @@ class PipewireProfile(Profile): ] def _enable_pipewire_for_all(self, install_session: 'Installer'): - users: Union[User, List[User]] = archinstall.arguments.get('!users', None) + users: Union[User, List[User]] = archinstall.arguments.get('!users', []) if not isinstance(users, list): users = [users] diff --git a/archinstall/default_profiles/servers/docker.py b/archinstall/default_profiles/servers/docker.py index e6e17831..f4800916 100644 --- a/archinstall/default_profiles/servers/docker.py +++ b/archinstall/default_profiles/servers/docker.py @@ -25,7 +25,7 @@ class DockerProfile(Profile): return ['docker'] def post_install(self, install_session: 'Installer'): - users: Union[User, List[User]] = archinstall.arguments.get('!users', None) + users: Union[User, List[User]] = archinstall.arguments.get('!users', []) if not isinstance(users, list): users = [users] diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index 77fed755..22c41c0d 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -29,7 +29,7 @@ class ConfigurationOutput: self._user_config_file = 'user_configuration.json' self._user_creds_file = "user_credentials.json" - self._sensitive = ['!users'] + self._sensitive = ['!users', '!root-password'] self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run'] self._process_config() diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index db7af63b..a969d93f 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -107,7 +107,7 @@ class GlobalMenu(AbstractMenu): Selector( _('User account'), lambda x: self._create_user_account(x), - default={}, + default=[], display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else None, preview_func=self._prev_users) self._menu_options['profile_config'] = \ -- 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/default_profiles') 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 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/default_profiles') 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 1b0c8f3ce3bc7887632f0d08b7afe8073f7a3958 Mon Sep 17 00:00:00 2001 From: Nikita Triblya <69369034+Tr3bleee@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:54:38 +0200 Subject: Removed comma at the end of package listing in i3.py (#1839) Removed comma at the end of package listing in i3.py --- archinstall/default_profiles/desktops/i3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/i3.py b/archinstall/default_profiles/desktops/i3.py index 7c6f24ca..93c38906 100644 --- a/archinstall/default_profiles/desktops/i3.py +++ b/archinstall/default_profiles/desktops/i3.py @@ -21,7 +21,7 @@ class I3wmProfile(XorgProfile): 'xterm', 'lightdm-gtk-greeter', 'lightdm', - 'dmenu', + 'dmenu' ] @property -- cgit v1.2.3-54-g00ecf From 64a0d097f047b8923881daa1d4c2516aa6538dae Mon Sep 17 00:00:00 2001 From: Nikita Triblya <69369034+Tr3bleee@users.noreply.github.com> Date: Wed, 21 Jun 2023 09:48:48 +0200 Subject: Update budgie.py (#1877) Removed the comma after the "papirus-icon-theme" package, since it is the final package --- archinstall/default_profiles/desktops/budgie.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/budgie.py b/archinstall/default_profiles/desktops/budgie.py index 32bd718d..93be320f 100644 --- a/archinstall/default_profiles/desktops/budgie.py +++ b/archinstall/default_profiles/desktops/budgie.py @@ -18,7 +18,7 @@ class BudgieProfile(XorgProfile): "budgie", "mate-terminal", "nemo", - "papirus-icon-theme", + "papirus-icon-theme" ] @property -- cgit v1.2.3-54-g00ecf From 96cb6cc943758664ca4b91ea8adc4c4e29f124d2 Mon Sep 17 00:00:00 2001 From: Nikita Triblya <69369034+Tr3bleee@users.noreply.github.com> Date: Wed, 21 Jun 2023 09:49:06 +0200 Subject: Update lxqt.py (#1878) Removed the comma after the "slock" package, since it is the final package --- archinstall/default_profiles/desktops/lxqt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/lxqt.py b/archinstall/default_profiles/desktops/lxqt.py index 146e168e..60bf47f2 100644 --- a/archinstall/default_profiles/desktops/lxqt.py +++ b/archinstall/default_profiles/desktops/lxqt.py @@ -23,7 +23,7 @@ class LxqtProfile(XorgProfile): "xdg-utils", "ttf-freefont", "leafpad", - "slock", + "slock" ] @property -- cgit v1.2.3-54-g00ecf From 2450c7bdff093aafba4a227156234ceed1887c31 Mon Sep 17 00:00:00 2001 From: Sumit Patel <89197795+Dark-Kernel@users.noreply.github.com> Date: Wed, 28 Jun 2023 23:34:32 +0530 Subject: Created dwm.py (#1902) Created dwm.py for suckless users. --- archinstall/default_profiles/desktops/dwm.py | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 archinstall/default_profiles/desktops/dwm.py (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/dwm.py b/archinstall/default_profiles/desktops/dwm.py new file mode 100644 index 00000000..3a7c0348 --- /dev/null +++ b/archinstall/default_profiles/desktops/dwm.py @@ -0,0 +1,35 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + + +class BspwmProfile(XorgProfile): + def __init__(self): + super().__init__('Bspwm', ProfileType.WindowMgr, description='') + + @property + def packages(self) -> List[str]: + return [ + 'dwm', + 'st', + 'dmenu', + 'picom', + 'xorg-xsetroot', + 'xorg-xset', + 'pywal', + 'xdo', + 'dunst', + 'slock' + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Lightdm + + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() -- cgit v1.2.3-54-g00ecf From 025ee8f064f6a142aa3de702c42c52101e8da9f6 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Wed, 28 Jun 2023 23:07:25 +0200 Subject: Fixed indentation issues from #1902 (#1904) --- archinstall/default_profiles/desktops/dwm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/dwm.py b/archinstall/default_profiles/desktops/dwm.py index 3a7c0348..6b9ca6bc 100644 --- a/archinstall/default_profiles/desktops/dwm.py +++ b/archinstall/default_profiles/desktops/dwm.py @@ -19,11 +19,11 @@ class BspwmProfile(XorgProfile): 'dmenu', 'picom', 'xorg-xsetroot', - 'xorg-xset', - 'pywal', - 'xdo', - 'dunst', - 'slock' + 'xorg-xset', + 'pywal', + 'xdo', + 'dunst', + 'slock' ] @property -- cgit v1.2.3-54-g00ecf From 58bccccd5d2048dd002ec0f2efeb185e480a72bd Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Thu, 29 Jun 2023 09:49:22 +0200 Subject: Revert "Created dwm.py (#1902)" - This reverts commit 2450c7bdff093aafba4a227156234ceed1887c31. (#1905) --- archinstall/default_profiles/desktops/dwm.py | 35 ---------------------------- 1 file changed, 35 deletions(-) delete mode 100644 archinstall/default_profiles/desktops/dwm.py (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/dwm.py b/archinstall/default_profiles/desktops/dwm.py deleted file mode 100644 index 6b9ca6bc..00000000 --- a/archinstall/default_profiles/desktops/dwm.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import List, Optional, Any, TYPE_CHECKING - -from archinstall.default_profiles.profile import ProfileType, GreeterType -from archinstall.default_profiles.xorg import XorgProfile - -if TYPE_CHECKING: - _: Any - - -class BspwmProfile(XorgProfile): - def __init__(self): - super().__init__('Bspwm', ProfileType.WindowMgr, description='') - - @property - def packages(self) -> List[str]: - return [ - 'dwm', - 'st', - 'dmenu', - 'picom', - 'xorg-xsetroot', - 'xorg-xset', - 'pywal', - 'xdo', - 'dunst', - 'slock' - ] - - @property - def default_greeter_type(self) -> Optional[GreeterType]: - return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() -- cgit v1.2.3-54-g00ecf From 29b528072551f32a0075b3f5c9d91d5ded019534 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Sat, 29 Jul 2023 06:20:55 -0400 Subject: Add `awesome` package to Awesome profile (#1954) --- archinstall/default_profiles/desktops/awesome.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/awesome.py b/archinstall/default_profiles/desktops/awesome.py index bb481914..371e51db 100644 --- a/archinstall/default_profiles/desktops/awesome.py +++ b/archinstall/default_profiles/desktops/awesome.py @@ -14,7 +14,10 @@ class AwesomeProfile(XorgProfile): @property def packages(self) -> List[str]: - return ['alacritty'] + return [ + 'awesome', + 'alacritty' + ] def preview_text(self) -> Optional[str]: text = str(_('Environment type: {}')).format(self.profile_type.value) -- cgit v1.2.3-54-g00ecf From a43344c5ae2249ddb70e535637915b30563c5038 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sun, 30 Jul 2023 00:28:21 +0200 Subject: Adding pack packages to profiles after they were removed (#1956) * Added back xinit for awesome, since it can be used without a greeter, as well as other useful tools we've had in previous releases * Fixing xinitrc for awesome profile * Attempting to grab xorg packages when installing the desktop profile * Spelling error on xorg-server * Fixed sway value error on seat selection --- archinstall/__init__.py | 4 +-- archinstall/default_profiles/desktops/awesome.py | 32 ++++++++++++++++++++++-- archinstall/default_profiles/desktops/bspwm.py | 22 ++++++++++++++++ archinstall/default_profiles/desktops/sway.py | 2 +- archinstall/default_profiles/xorg.py | 8 +++++- archinstall/lib/general.py | 2 +- 6 files changed, 63 insertions(+), 7 deletions(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index cfaecd16..56f0b278 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -145,12 +145,12 @@ 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) """ - if type(args) == Namespace: + if type(args) is Namespace: args = vars(args) clean_args = {} for key, val in args.items(): - if type(val) == dict: + if isinstance(val, dict): val = cleanup_empty_args(val) if val is not None: diff --git a/archinstall/default_profiles/desktops/awesome.py b/archinstall/default_profiles/desktops/awesome.py index 371e51db..79e0eb71 100644 --- a/archinstall/default_profiles/desktops/awesome.py +++ b/archinstall/default_profiles/desktops/awesome.py @@ -14,9 +14,18 @@ class AwesomeProfile(XorgProfile): @property def packages(self) -> List[str]: - return [ + return super().packages + [ 'awesome', - 'alacritty' + 'alacritty', + 'xorg-xinit', + 'xorg-xrandr', + 'xterm', + 'feh', + 'slock', + 'terminus-font', + 'gnu-free-fonts', + 'ttf-liberation', + 'xsel', ] def preview_text(self) -> Optional[str]: @@ -37,3 +46,22 @@ class AwesomeProfile(XorgProfile): fh.write(awesome_lua) # TODO: Configure the right-click-menu to contain the above packages that were installed. (as a user config) + + # TODO: check if we selected a greeter, + # but for now, awesome is intended to run without one. + with open(f"{install_session.target}/etc/X11/xinit/xinitrc", 'r') as xinitrc: + xinitrc_data = xinitrc.read() + + for line in xinitrc_data.split('\n'): + if "twm &" in line: + xinitrc_data = xinitrc_data.replace(line, f"# {line}") + if "xclock" in line: + xinitrc_data = xinitrc_data.replace(line, f"# {line}") + if "xterm" in line: + xinitrc_data = xinitrc_data.replace(line, f"# {line}") + + xinitrc_data += '\n' + xinitrc_data += 'exec awesome\n' + + with open(f"{install_session.target}/etc/X11/xinit/xinitrc", 'w') as xinitrc: + xinitrc.write(xinitrc_data) \ No newline at end of file diff --git a/archinstall/default_profiles/desktops/bspwm.py b/archinstall/default_profiles/desktops/bspwm.py index f3bc982d..2a29f41b 100644 --- a/archinstall/default_profiles/desktops/bspwm.py +++ b/archinstall/default_profiles/desktops/bspwm.py @@ -13,6 +13,7 @@ class BspwmProfile(XorgProfile): @property def packages(self) -> List[str]: + # return super().packages + [ return [ 'bspwm', 'sxhkd', @@ -28,3 +29,24 @@ class BspwmProfile(XorgProfile): def preview_text(self) -> Optional[str]: text = str(_('Environment type: {}')).format(self.profile_type.value) return text + '\n' + self.packages_text() + + # The wiki specified xinit, but we already use greeter? + # https://wiki.archlinux.org/title/Bspwm#Starting + # + # # TODO: check if we selected a greeter, else run this: + # with open(f"{install_session.target}/etc/X11/xinit/xinitrc", 'r') as xinitrc: + # xinitrc_data = xinitrc.read() + + # for line in xinitrc_data.split('\n'): + # if "twm &" in line: + # xinitrc_data = xinitrc_data.replace(line, f"# {line}") + # if "xclock" in line: + # xinitrc_data = xinitrc_data.replace(line, f"# {line}") + # if "xterm" in line: + # xinitrc_data = xinitrc_data.replace(line, f"# {line}") + + # xinitrc_data += '\n' + # xinitrc_data += 'exec bspwn\n' + + # with open(f"{install_session.target}/etc/X11/xinit/xinitrc", 'w') as xinitrc: + # xinitrc.write(xinitrc_data) diff --git a/archinstall/default_profiles/desktops/sway.py b/archinstall/default_profiles/desktops/sway.py index ae814e46..25d74a88 100644 --- a/archinstall/default_profiles/desktops/sway.py +++ b/archinstall/default_profiles/desktops/sway.py @@ -53,7 +53,7 @@ class SwayProfile(XorgProfile): @property def services(self) -> List[str]: if pref := self.custom_settings.get('seat_access', None): - return [pref.value] + return [pref] return [] def _ask_seat_access(self): diff --git a/archinstall/default_profiles/xorg.py b/archinstall/default_profiles/xorg.py index 553421a4..13154818 100644 --- a/archinstall/default_profiles/xorg.py +++ b/archinstall/default_profiles/xorg.py @@ -1,4 +1,4 @@ -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, List from archinstall.default_profiles.profile import Profile, ProfileType @@ -19,3 +19,9 @@ class XorgProfile(Profile): description=description, support_gfx_driver=True ) + + @property + def packages(self) -> List[str]: + return [ + 'xorg-server' + ] diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 473f85a4..90af25ed 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -211,7 +211,7 @@ class SysCommandWorker: return False def write(self, data: bytes, line_ending :bool = True) -> int: - assert type(data) == bytes # TODO: Maybe we can support str as well and encode it + assert isinstance(data, bytes) # TODO: Maybe we can support str as well and encode it self.make_sure_we_are_executing() -- cgit v1.2.3-54-g00ecf From f8d269092033f49b9f9621580fa1862e0c3b6cdb Mon Sep 17 00:00:00 2001 From: Himadri Bhattacharjee <107522312+lavafroth@users.noreply.github.com> Date: Mon, 31 Jul 2023 07:40:45 +0000 Subject: move preview text method for profiles into xorg superclass, override when needed (#1918) Co-authored-by: Anton Hvornum --- archinstall/default_profiles/desktops/awesome.py | 6 +----- archinstall/default_profiles/desktops/budgie.py | 4 ---- archinstall/default_profiles/desktops/cinnamon.py | 4 ---- archinstall/default_profiles/desktops/cutefish.py | 4 ---- archinstall/default_profiles/desktops/deepin.py | 4 ---- archinstall/default_profiles/desktops/enlightenment.py | 4 ---- archinstall/default_profiles/desktops/gnome.py | 4 ---- archinstall/default_profiles/desktops/hyperland.py | 4 ---- archinstall/default_profiles/desktops/i3.py | 4 ---- archinstall/default_profiles/desktops/kde.py | 4 ---- archinstall/default_profiles/desktops/lxqt.py | 4 ---- archinstall/default_profiles/desktops/mate.py | 4 ---- archinstall/default_profiles/desktops/qtile.py | 4 ---- archinstall/default_profiles/desktops/sway.py | 4 ---- archinstall/default_profiles/desktops/xfce4.py | 4 ---- archinstall/default_profiles/xorg.py | 6 +++++- 16 files changed, 6 insertions(+), 62 deletions(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/awesome.py b/archinstall/default_profiles/desktops/awesome.py index 79e0eb71..3833ce71 100644 --- a/archinstall/default_profiles/desktops/awesome.py +++ b/archinstall/default_profiles/desktops/awesome.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Any, TYPE_CHECKING +from typing import List, Any, TYPE_CHECKING from archinstall.default_profiles.profile import ProfileType from archinstall.default_profiles.xorg import XorgProfile @@ -28,10 +28,6 @@ class AwesomeProfile(XorgProfile): 'xsel', ] - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() - def install(self, install_session: 'Installer'): super().install(install_session) diff --git a/archinstall/default_profiles/desktops/budgie.py b/archinstall/default_profiles/desktops/budgie.py index 93be320f..68506d45 100644 --- a/archinstall/default_profiles/desktops/budgie.py +++ b/archinstall/default_profiles/desktops/budgie.py @@ -24,7 +24,3 @@ class BudgieProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/cinnamon.py b/archinstall/default_profiles/desktops/cinnamon.py index 22fd0d9d..a819b4d1 100644 --- a/archinstall/default_profiles/desktops/cinnamon.py +++ b/archinstall/default_profiles/desktops/cinnamon.py @@ -25,7 +25,3 @@ class CinnamonProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/cutefish.py b/archinstall/default_profiles/desktops/cutefish.py index 6f88c47a..c4202920 100644 --- a/archinstall/default_profiles/desktops/cutefish.py +++ b/archinstall/default_profiles/desktops/cutefish.py @@ -23,9 +23,5 @@ class CutefishProfile(XorgProfile): def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Sddm - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() - def install(self, install_session: 'Installer'): super().install(install_session) diff --git a/archinstall/default_profiles/desktops/deepin.py b/archinstall/default_profiles/desktops/deepin.py index 054c8fdf..e6a9f6b5 100644 --- a/archinstall/default_profiles/desktops/deepin.py +++ b/archinstall/default_profiles/desktops/deepin.py @@ -22,7 +22,3 @@ class DeepinProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/enlightenment.py b/archinstall/default_profiles/desktops/enlightenment.py index 164f64fe..7dd7822a 100644 --- a/archinstall/default_profiles/desktops/enlightenment.py +++ b/archinstall/default_profiles/desktops/enlightenment.py @@ -21,7 +21,3 @@ class EnlighenmentProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/gnome.py b/archinstall/default_profiles/desktops/gnome.py index 3cbd49bd..24ade437 100644 --- a/archinstall/default_profiles/desktops/gnome.py +++ b/archinstall/default_profiles/desktops/gnome.py @@ -21,7 +21,3 @@ class GnomeProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Gdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/hyperland.py b/archinstall/default_profiles/desktops/hyperland.py index e55dd7c4..58ee8cca 100644 --- a/archinstall/default_profiles/desktops/hyperland.py +++ b/archinstall/default_profiles/desktops/hyperland.py @@ -25,7 +25,3 @@ class HyperlandProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Sddm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/i3.py b/archinstall/default_profiles/desktops/i3.py index 93c38906..9c2994de 100644 --- a/archinstall/default_profiles/desktops/i3.py +++ b/archinstall/default_profiles/desktops/i3.py @@ -27,7 +27,3 @@ class I3wmProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/kde.py b/archinstall/default_profiles/desktops/kde.py index cd02e069..b65a9620 100644 --- a/archinstall/default_profiles/desktops/kde.py +++ b/archinstall/default_profiles/desktops/kde.py @@ -26,7 +26,3 @@ class KdeProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Sddm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/lxqt.py b/archinstall/default_profiles/desktops/lxqt.py index 60bf47f2..5d75e08d 100644 --- a/archinstall/default_profiles/desktops/lxqt.py +++ b/archinstall/default_profiles/desktops/lxqt.py @@ -29,7 +29,3 @@ class LxqtProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Sddm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/mate.py b/archinstall/default_profiles/desktops/mate.py index 0ddaaaab..d3c4a6e1 100644 --- a/archinstall/default_profiles/desktops/mate.py +++ b/archinstall/default_profiles/desktops/mate.py @@ -21,7 +21,3 @@ class MateProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/qtile.py b/archinstall/default_profiles/desktops/qtile.py index 66c6fa1b..96e93b1d 100644 --- a/archinstall/default_profiles/desktops/qtile.py +++ b/archinstall/default_profiles/desktops/qtile.py @@ -21,7 +21,3 @@ class QtileProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/desktops/sway.py b/archinstall/default_profiles/desktops/sway.py index 25d74a88..c757797d 100644 --- a/archinstall/default_profiles/desktops/sway.py +++ b/archinstall/default_profiles/desktops/sway.py @@ -73,9 +73,5 @@ class SwayProfile(XorgProfile): def do_on_select(self): self._ask_seat_access() - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() - def install(self, install_session: 'Installer'): super().install(install_session) diff --git a/archinstall/default_profiles/desktops/xfce4.py b/archinstall/default_profiles/desktops/xfce4.py index bd6c3038..a7f0a7e6 100644 --- a/archinstall/default_profiles/desktops/xfce4.py +++ b/archinstall/default_profiles/desktops/xfce4.py @@ -24,7 +24,3 @@ class Xfce4Profile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() diff --git a/archinstall/default_profiles/xorg.py b/archinstall/default_profiles/xorg.py index 13154818..c9abf4da 100644 --- a/archinstall/default_profiles/xorg.py +++ b/archinstall/default_profiles/xorg.py @@ -1,4 +1,4 @@ -from typing import Any, TYPE_CHECKING, List +from typing import Any, Optional, TYPE_CHECKING, List from archinstall.default_profiles.profile import Profile, ProfileType @@ -20,6 +20,10 @@ class XorgProfile(Profile): support_gfx_driver=True ) + def preview_text(self) -> Optional[str]: + text = str(_('Environment type: {}')).format(self.profile_type.value) + return text + '\n' + self.packages_text() + @property def packages(self) -> List[str]: return [ -- cgit v1.2.3-54-g00ecf From f270a47f27de7176199293a3777d116bf8101cf8 Mon Sep 17 00:00:00 2001 From: Christian Heusel Date: Thu, 14 Sep 2023 14:10:26 +0200 Subject: add the option to use ly as display manager (#2004) Signed-off-by: Christian Heusel --- archinstall/default_profiles/profile.py | 1 + archinstall/lib/profile/profiles_handler.py | 3 +++ 2 files changed, 4 insertions(+) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index ce07c286..c7683013 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -34,6 +34,7 @@ class GreeterType(Enum): Lightdm = 'lightdm' Sddm = 'sddm' Gdm = 'gdm' + Ly = 'ly' class SelectResult(Enum): diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 7e6af3d1..8ea646b6 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -182,6 +182,9 @@ class ProfileHandler: case GreeterType.Gdm: packages = ['gdm'] service = ['gdm'] + case GreeterType.Ly: + packages = ['ly'] + service = ['ly'] if packages: install_session.add_additional_packages(packages) -- cgit v1.2.3-54-g00ecf From 06d46ac762f58d051b2dba1aea50a2e3314ab403 Mon Sep 17 00:00:00 2001 From: Campbell Jones Date: Fri, 15 Sep 2023 07:51:36 -0400 Subject: Add option to use slick-greeter with lightdm (#1970) --- archinstall/default_profiles/desktops/budgie.py | 2 +- archinstall/default_profiles/profile.py | 3 ++- archinstall/lib/profile/profiles_handler.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/budgie.py b/archinstall/default_profiles/desktops/budgie.py index 68506d45..28c05f45 100644 --- a/archinstall/default_profiles/desktops/budgie.py +++ b/archinstall/default_profiles/desktops/budgie.py @@ -23,4 +23,4 @@ class BudgieProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: - return GreeterType.Lightdm + return GreeterType.LightdmSlick diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index c7683013..982bd5a3 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -31,7 +31,8 @@ class ProfileType(Enum): class GreeterType(Enum): - Lightdm = 'lightdm' + Lightdm = 'lightdm-gtk-greeter' + LightdmSlick = 'lightdm-slick-greeter' Sddm = 'sddm' Gdm = 'gdm' Ly = 'ly' diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 8ea646b6..07910b08 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -173,6 +173,9 @@ class ProfileHandler: service = None match greeter: + case GreeterType.LightdmSlick: + packages = ['lightdm', 'lightdm-slick-greeter'] + service = ['lightdm'] case GreeterType.Lightdm: packages = ['lightdm', 'lightdm-gtk-greeter'] service = ['lightdm'] @@ -191,6 +194,17 @@ class ProfileHandler: if service: install_session.enable_service(service) + # slick-greeter requires a config change + if greeter == GreeterType.LightdmSlick: + path = install_session.target.joinpath('etc/lightdm/lightdm.conf') + with open(path, 'r') as file: + filedata = file.read() + + filedata = filedata.replace('#greeter-session=example-gtk-gnome', 'greeter-session=lightdm-slick-greeter') + + with open(path, 'w') as file: + file.write(filedata) + def install_gfx_driver(self, install_session: 'Installer', driver: Optional[GfxDriver]): try: -- 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/default_profiles') 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 d6e3a4651f2ff944a00e8acfb316db8be2fbcf3e Mon Sep 17 00:00:00 2001 From: Sxmourai <49468969+Sxmourai@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:46:59 +0200 Subject: Renamed hyperland to hyprland, fixed seatd via post_installation and added waybar-hyprland (#1824) * Renamed hyperland to hyprland, fixed seatd via post_installation and installed waybar * Removed the launching of seatd on the installation process * Starting to add nvidia support, and automatic configuring of hyprland * Starting to add auto configuration of hyprland... But this will need maintenance * Added hyprpaper auto config Gonna make waybar auto config next * Waybar auto config is starting... I can't test rn I'm on vacation and my connection is quite bad (68 days for arch iso) * Added wlogout support (and swaylock) * Fixed file managers printing * Starting to add a shell config... Definitely don't push this * Reverted custom-shell config (create a separate PR) * Removed systemd-logind, as that was just for testing the selector * Added polkit as an option for the seat. As it's a dependency of the hyprland package * Flake8 fix * The name change wasn't propegated to the menu * Added newline at the end of general_conf.py to not alter it * Removed newline at the end of general_conf.py to not alter it * Renamed the Hyprland class --------- Co-authored-by: Anton Hvornum Co-authored-by: Anton Hvornum --- archinstall/default_profiles/desktops/hyperland.py | 27 ---------- archinstall/default_profiles/desktops/hyprland.py | 62 ++++++++++++++++++++++ archinstall/lib/interactions/general_conf.py | 2 +- 3 files changed, 63 insertions(+), 28 deletions(-) delete mode 100644 archinstall/default_profiles/desktops/hyperland.py create mode 100644 archinstall/default_profiles/desktops/hyprland.py (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/hyperland.py b/archinstall/default_profiles/desktops/hyperland.py deleted file mode 100644 index 58ee8cca..00000000 --- a/archinstall/default_profiles/desktops/hyperland.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import List, Optional, Any, TYPE_CHECKING - -from archinstall.default_profiles.profile import ProfileType, GreeterType -from archinstall.default_profiles.xorg import XorgProfile - -if TYPE_CHECKING: - _: Any - - -class HyperlandProfile(XorgProfile): - def __init__(self): - super().__init__('Hyperland', ProfileType.DesktopEnv, description='') - - @property - def packages(self) -> List[str]: - return [ - "hyprland", - "dunst", - "xdg-desktop-portal-hyprland", - "kitty", - "qt5-wayland", - "qt6-wayland" - ] - - @property - def default_greeter_type(self) -> Optional[GreeterType]: - return GreeterType.Sddm diff --git a/archinstall/default_profiles/desktops/hyprland.py b/archinstall/default_profiles/desktops/hyprland.py new file mode 100644 index 00000000..f464c828 --- /dev/null +++ b/archinstall/default_profiles/desktops/hyprland.py @@ -0,0 +1,62 @@ +from enum import Enum +from typing import List, Optional, TYPE_CHECKING, Any + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile +from archinstall.lib.menu import Menu + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + _: Any + + +class SeatAccess(Enum): + seatd = 'seatd' + polkit = 'polkit' + + +class HyprlandProfile(XorgProfile): + def __init__(self): + super().__init__('Hyprland', ProfileType.DesktopEnv, description='') + + self.custom_settings = {'seat_access': None} + + @property + def packages(self) -> List[str]: + return [ + "hyprland", + "dunst", + "xdg-desktop-portal-hyprland", + "qt5-wayland", + "qt6-wayland" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Sddm + + @property + def services(self) -> List[str]: + if pref := self.custom_settings.get('seat_access', None): + return [pref] + return [] + + def _ask_seat_access(self): + # need to activate seat service and add to seat group + title = str(_('Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')) + title += str(_('\n\nChoose an option to give Hyprland access to your hardware')) + + options = [e.value for e in SeatAccess] + default = None + + if seat := self.custom_settings.get('seat_access', None): + default = seat + + choice = Menu(title, options, skip=False, preset_values=default).run() + self.custom_settings['seat_access'] = choice.single_value + + def do_on_select(self): + self._ask_seat_access() + + def install(self, install_session: 'Installer'): + super().install(install_session) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 8dd6e94f..56598e25 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -218,4 +218,4 @@ def select_additional_repositories(preset: List[str]) -> List[str]: case MenuSelectionType.Reset: return [] case MenuSelectionType.Selection: return choice.single_value - return [] + return [] \ No newline at end of file -- cgit v1.2.3-54-g00ecf From ca3051ebf3ddd45f90a3bd3244df4640fb0b1c58 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Fri, 22 Sep 2023 16:47:50 +0200 Subject: Added kitty, dolphin, wofi to hyprland (#2092) --- archinstall/default_profiles/desktops/hyprland.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/hyprland.py b/archinstall/default_profiles/desktops/hyprland.py index f464c828..065ca6f2 100644 --- a/archinstall/default_profiles/desktops/hyprland.py +++ b/archinstall/default_profiles/desktops/hyprland.py @@ -26,6 +26,9 @@ class HyprlandProfile(XorgProfile): return [ "hyprland", "dunst", + "kitty", + "dolphin", + "wofi", "xdg-desktop-portal-hyprland", "qt5-wayland", "qt6-wayland" -- cgit v1.2.3-54-g00ecf From 8e25f2e9926e5da2532fd0d52cdac1722b6d766d Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 7 Mar 2024 01:49:51 -0500 Subject: plasma-workspace replaces plasma-wayland-session (#2386) --- archinstall/default_profiles/desktops/kde.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/kde.py b/archinstall/default_profiles/desktops/kde.py index b65a9620..62274d51 100644 --- a/archinstall/default_profiles/desktops/kde.py +++ b/archinstall/default_profiles/desktops/kde.py @@ -19,7 +19,7 @@ class KdeProfile(XorgProfile): "kwrite", "dolphin", "ark", - "plasma-wayland-session", + "plasma-workspace", "egl-wayland" ] -- cgit v1.2.3-54-g00ecf From 08a6d402c4792cae95aec196460dc67aadd86f3c Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 8 Mar 2024 00:43:51 +1100 Subject: Fix 2215 | Display installed packages for all profile submenus (#2355) * Display all packages to be installed * Display all packages to be installed --- archinstall/default_profiles/desktops/bspwm.py | 25 --------------------- archinstall/default_profiles/profile.py | 31 ++++++++++++++++++-------- archinstall/default_profiles/xorg.py | 5 ++++- archinstall/lib/hardware.py | 12 +++++++++- archinstall/lib/interactions/system_conf.py | 7 +++--- archinstall/lib/menu/menu.py | 4 +++- archinstall/lib/profile/profile_menu.py | 22 ++++++++++++++++-- archinstall/lib/utils/util.py | 8 +++---- 8 files changed, 68 insertions(+), 46 deletions(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/bspwm.py b/archinstall/default_profiles/desktops/bspwm.py index 2a29f41b..61eeba43 100644 --- a/archinstall/default_profiles/desktops/bspwm.py +++ b/archinstall/default_profiles/desktops/bspwm.py @@ -25,28 +25,3 @@ class BspwmProfile(XorgProfile): @property def default_greeter_type(self) -> Optional[GreeterType]: return GreeterType.Lightdm - - def preview_text(self) -> Optional[str]: - text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() - - # The wiki specified xinit, but we already use greeter? - # https://wiki.archlinux.org/title/Bspwm#Starting - # - # # TODO: check if we selected a greeter, else run this: - # with open(f"{install_session.target}/etc/X11/xinit/xinitrc", 'r') as xinitrc: - # xinitrc_data = xinitrc.read() - - # for line in xinitrc_data.split('\n'): - # if "twm &" in line: - # xinitrc_data = xinitrc_data.replace(line, f"# {line}") - # if "xclock" in line: - # xinitrc_data = xinitrc_data.replace(line, f"# {line}") - # if "xterm" in line: - # xinitrc_data = xinitrc_data.replace(line, f"# {line}") - - # xinitrc_data += '\n' - # xinitrc_data += 'exec bspwn\n' - - # with open(f"{install_session.target}/etc/X11/xinit/xinitrc", 'w') as xinitrc: - # xinitrc.write(xinitrc_data) diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index 49a9c19d..4c85b0c7 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -178,15 +178,28 @@ class Profile: def preview_text(self) -> Optional[str]: """ - Used for preview text in profiles_bck. If a description is set for a - profile it will automatically display that one in the preview. - If no preview or a different text should be displayed just + Override this method to provide a preview text for the profile """ - if self.description: - return self.description - return None + return self.packages_text() - def packages_text(self) -> str: + def packages_text(self, include_sub_packages: bool = False) -> Optional[str]: header = str(_('Installed packages')) - output = format_cols(self.packages, header) - return output + + text = '' + packages = [] + + if self.packages: + packages = self.packages + + if include_sub_packages: + for p in self.current_selection: + if p.packages: + packages += p.packages + + text += format_cols(sorted(set(packages))) + + if text: + text = f'{header}: \n{text}' + return text + + return None diff --git a/archinstall/default_profiles/xorg.py b/archinstall/default_profiles/xorg.py index c9abf4da..88ba55a6 100644 --- a/archinstall/default_profiles/xorg.py +++ b/archinstall/default_profiles/xorg.py @@ -22,7 +22,10 @@ class XorgProfile(Profile): def preview_text(self) -> Optional[str]: text = str(_('Environment type: {}')).format(self.profile_type.value) - return text + '\n' + self.packages_text() + if packages := self.packages_text(): + text += f'\n{packages}' + + return text @property def packages(self) -> List[str]: diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index efdae430..c8001c19 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -2,12 +2,16 @@ import os from enum import Enum from functools import cached_property from pathlib import Path -from typing import Optional, Dict, List +from typing import Optional, Dict, List, TYPE_CHECKING, Any from .exceptions import SysCallError from .general import SysCommand from .networking import list_interfaces, enrich_iface_types from .output import debug +from .utils.util import format_cols + +if TYPE_CHECKING: + _: Any class CpuVendor(Enum): @@ -73,6 +77,12 @@ class GfxDriver(Enum): case _: return False + def packages_text(self) -> str: + text = str(_('Installed packages')) + ':\n' + pkg_names = [p.value for p in self.gfx_packages()] + text += format_cols(sorted(pkg_names)) + return text + def gfx_packages(self) -> List[GfxPackage]: packages = [GfxPackage.XorgServer, GfxPackage.XorgXinit] diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py index aa72748e..35ba5a8b 100644 --- a/archinstall/lib/interactions/system_conf.py +++ b/archinstall/lib/interactions/system_conf.py @@ -103,14 +103,15 @@ def select_driver(options: List[GfxDriver] = [], current_value: Optional[GfxDriv if SysInfo.has_nvidia_graphics(): title += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')) - title += str(_('\nSelect a graphics driver or leave blank to install all open-source drivers')) - preset = current_value.value if current_value else None + choice = Menu( title, drivers, preset_values=preset, - default_option=GfxDriver.AllOpenSource.value + default_option=GfxDriver.AllOpenSource.value, + preview_command=lambda x: GfxDriver(x).packages_text(), + preview_size=0.3 ).run() if choice.type_ != MenuSelectionType.Selection: diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 3bd31b88..f14b855d 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -235,7 +235,9 @@ class Menu(TerminalMenu): if preview_command: if self._default_option is not None and self._default_menu_value == selection: selection = self._default_option - return preview_command(selection) + + if res := preview_command(selection): + return res.rstrip('\n') return None diff --git a/archinstall/lib/profile/profile_menu.py b/archinstall/lib/profile/profile_menu.py index d9e47190..aba75a88 100644 --- a/archinstall/lib/profile/profile_menu.py +++ b/archinstall/lib/profile/profile_menu.py @@ -40,6 +40,7 @@ class ProfileMenu(AbstractSubMenu): lambda preset: self._select_gfx_driver(preset), display_func=lambda x: x.value if x else None, dependencies=['profile'], + preview_func=self._preview_gfx, default=self._preset.gfx_driver if self._preset.profile and self._preset.profile.is_graphic_driver_supported() else None, enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False ) @@ -67,6 +68,7 @@ class ProfileMenu(AbstractSubMenu): def _select_profile(self, preset: Optional[Profile]) -> Optional[Profile]: profile = select_profile(preset) + if profile is not None: if not profile.is_graphic_driver_supported(): self._menu_options['gfx_driver'].set_enabled(False) @@ -105,12 +107,28 @@ class ProfileMenu(AbstractSubMenu): return driver + def _preview_gfx(self) -> Optional[str]: + driver: Optional[GfxDriver] = self._menu_options['gfx_driver'].current_selection + + if driver: + return driver.packages_text() + + return None + def _preview_profile(self) -> Optional[str]: profile: Optional[Profile] = self._menu_options['profile'].current_selection + text = '' if profile: - names = profile.current_selection_names() - return '\n'.join(names) + if (sub_profiles := profile.current_selection) is not None: + text += str(_('Selected profiles: ')) + text += ', '.join([p.name for p in sub_profiles]) + '\n' + + if packages := profile.packages_text(include_sub_packages=True): + text += f'{packages}' + + if text: + return text return None diff --git a/archinstall/lib/utils/util.py b/archinstall/lib/utils/util.py index 8df75ab1..2e42b3cf 100644 --- a/archinstall/lib/utils/util.py +++ b/archinstall/lib/utils/util.py @@ -31,18 +31,18 @@ def is_subpath(first: Path, second: Path): return False -def format_cols(items: List[str], header: Optional[str]) -> str: +def format_cols(items: List[str], header: Optional[str] = None) -> str: if header: text = f'{header}:\n' else: text = '' nr_items = len(items) - if nr_items <= 5: + if nr_items <= 4: col = 1 - elif nr_items <= 10: + elif nr_items <= 8: col = 2 - elif nr_items <= 15: + elif nr_items <= 12: col = 3 else: col = 4 -- cgit v1.2.3-54-g00ecf From 0ea6dbbd7677b94e863b2ab333431716886b5f84 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 19 Apr 2024 14:47:18 +0200 Subject: Rename "Kde" profile to the correct "KDE Plasma" / "Plasma" (#2421) * schema.json: Remove dead misspelled i3-gasp profile * schema.json: Rename KDE Plasma profile to the correct "Plasma" shorthand * Rename to KDE Plasma in user facing parts and keep the old "Kde" profile for now * Add back an accidental deleted character * Backwards compat v2 --- archinstall/default_profiles/desktop.py | 2 +- archinstall/default_profiles/desktops/kde.py | 28 ------------------------- archinstall/default_profiles/desktops/plasma.py | 27 ++++++++++++++++++++++++ archinstall/lib/models/network_configuration.py | 2 +- archinstall/lib/profile/profiles_handler.py | 5 +++++ examples/config-sample.json | 2 +- mypy-strict.ini | 4 ++-- schema.json | 5 ++--- 8 files changed, 39 insertions(+), 36 deletions(-) delete mode 100644 archinstall/default_profiles/desktops/kde.py create mode 100644 archinstall/default_profiles/desktops/plasma.py (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktop.py b/archinstall/default_profiles/desktop.py index 9d92f822..417d86d6 100644 --- a/archinstall/default_profiles/desktop.py +++ b/archinstall/default_profiles/desktop.py @@ -15,7 +15,7 @@ class DesktopProfile(Profile): super().__init__( 'Desktop', ProfileType.Desktop, - description=str(_('Provides a selection of desktop environments and tiling window managers, e.g. gnome, kde, sway')), + description=str(_('Provides a selection of desktop environments and tiling window managers, e.g. GNOME, KDE Plasma, Sway')), current_selection=current_selection, support_greeter=True ) diff --git a/archinstall/default_profiles/desktops/kde.py b/archinstall/default_profiles/desktops/kde.py deleted file mode 100644 index 62274d51..00000000 --- a/archinstall/default_profiles/desktops/kde.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import List, Optional, Any, TYPE_CHECKING - -from archinstall.default_profiles.profile import ProfileType, GreeterType -from archinstall.default_profiles.xorg import XorgProfile - -if TYPE_CHECKING: - _: Any - - -class KdeProfile(XorgProfile): - def __init__(self): - super().__init__('Kde', ProfileType.DesktopEnv, description='') - - @property - def packages(self) -> List[str]: - return [ - "plasma-meta", - "konsole", - "kwrite", - "dolphin", - "ark", - "plasma-workspace", - "egl-wayland" - ] - - @property - def default_greeter_type(self) -> Optional[GreeterType]: - return GreeterType.Sddm diff --git a/archinstall/default_profiles/desktops/plasma.py b/archinstall/default_profiles/desktops/plasma.py new file mode 100644 index 00000000..bcc1ea1b --- /dev/null +++ b/archinstall/default_profiles/desktops/plasma.py @@ -0,0 +1,27 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + +class PlasmaProfile(XorgProfile): + def __init__(self): + super().__init__('KDE Plasma', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + "plasma-meta", + "konsole", + "kwrite", + "dolphin", + "ark", + "plasma-workspace", + "egl-wayland" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Sddm diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index b726bb73..dfd8b8cb 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -20,7 +20,7 @@ class NicType(Enum): 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)')) + return str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE Plasma)')) case NicType.MANUAL: return str(_('Manual configuration')) diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 12dcee3f..b9acb4fe 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -107,6 +107,11 @@ class ProfileHandler: if details: for detail in filter(None, details): + # [2024-04-19] TODO: Backwards compatibility after naming change: https://github.com/archlinux/archinstall/pull/2421 + # 'Kde' is deprecated, remove this block in a future version + if detail == 'Kde': + detail = 'KDE Plasma' + if sub_profile := self.get_profile_by_name(detail): valid_sub_profiles.append(sub_profile) else: diff --git a/examples/config-sample.json b/examples/config-sample.json index d43f7ea6..47a4e2e0 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -106,7 +106,7 @@ "greeter": "sddm", "profile": { "details": [ - "Kde" + "KDE Plasma" ], "main": "Desktop" } diff --git a/mypy-strict.ini b/mypy-strict.ini index be7ddf57..c670bbaa 100644 --- a/mypy-strict.ini +++ b/mypy-strict.ini @@ -66,10 +66,10 @@ exclude = (?x)( | ^archinstall/profiles/enlightenment\.py$ | ^archinstall/profiles/gnome\.py$ | ^archinstall/profiles/i3\.py$ - | ^archinstall/profiles/kde\.py$ | ^archinstall/profiles/lxqt\.py$ | ^archinstall/profiles/mate\.py$ | ^archinstall/profiles/minimal\.py$ + | ^archinstall/profiles/plasma\.py$ | ^archinstall/profiles/qtile\.py$ | ^archinstall/profiles/server\.py$ | ^archinstall/profiles/sway\.py$ @@ -86,10 +86,10 @@ exclude = (?x)( | ^profiles/enlightenment\.py$ | ^profiles/gnome\.py$ | ^profiles/i3\.py$ - | ^profiles/kde\.py$ | ^profiles/lxqt\.py$ | ^profiles/mate\.py$ | ^profiles/minimal\.py$ + | ^profiles/plasma\.py$ | ^profiles/qtile\.py$ | ^profiles/server\.py$ | ^profiles/sway\.py$ diff --git a/schema.json b/schema.json index 74ecc4d3..97cb42e1 100644 --- a/schema.json +++ b/schema.json @@ -129,7 +129,7 @@ ] }, "details": { - "description": "Specific profile to be installed based on the 'main' selection; these profiles are present in profiles_v2/, use the name of a profile to install it (case insensitive)", + "description": "Specific profile to be installed based on the 'main' selection; these profiles are present in default_profiles/, use the file name of a profile without the extension to install it (case insensitive)", "type": "string", "enum": [ "awesome", @@ -142,8 +142,7 @@ "enlightenment", "gnome", "i3-wm", - "i3-gasp", - "kde", + "plasma", "lxqt", "mate", "sway", -- cgit v1.2.3-54-g00ecf From e5b6f46032283e722c449ca8ad7b68dc8c0b350f Mon Sep 17 00:00:00 2001 From: Uday Jadhav <110811053+udqy@users.noreply.github.com> Date: Sun, 5 May 2024 10:38:03 +0530 Subject: Add applications to Hyprland desktop profile (#2481) --- archinstall/default_profiles/desktops/hyprland.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'archinstall/default_profiles') diff --git a/archinstall/default_profiles/desktops/hyprland.py b/archinstall/default_profiles/desktops/hyprland.py index 065ca6f2..0c5452eb 100644 --- a/archinstall/default_profiles/desktops/hyprland.py +++ b/archinstall/default_profiles/desktops/hyprland.py @@ -31,7 +31,10 @@ class HyprlandProfile(XorgProfile): "wofi", "xdg-desktop-portal-hyprland", "qt5-wayland", - "qt6-wayland" + "qt6-wayland", + "polkit-kde-agent", + "grim", + "slurp" ] @property -- cgit v1.2.3-54-g00ecf