From c241203ded21676354f8d3f589d2f8b5fa5e0c36 Mon Sep 17 00:00:00 2001 From: 牧瀬紅莉栖 Date: Sun, 16 Apr 2023 15:36:01 +0800 Subject: Add runtime dependency for dmenu (#1514) --- profiles/sway.py | 1 + 1 file changed, 1 insertion(+) (limited to 'profiles') diff --git a/profiles/sway.py b/profiles/sway.py index 5fbd3365..4716fe78 100644 --- a/profiles/sway.py +++ b/profiles/sway.py @@ -21,6 +21,7 @@ __packages__ = [ "slurp", "pavucontrol", "foot", + "xorg-xwayland", ] -- cgit v1.2.3-70-g09d2 From 98f13ece6e99e32604e915c1e5e43527a7578346 Mon Sep 17 00:00:00 2001 From: Callum Andrew Date: Sun, 16 Apr 2023 17:36:56 +1000 Subject: profiles/sway: update packages (#1745) --- profiles/sway.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'profiles') diff --git a/profiles/sway.py b/profiles/sway.py index 4716fe78..f69f73ce 100644 --- a/profiles/sway.py +++ b/profiles/sway.py @@ -12,11 +12,12 @@ is_top_level_profile = False __packages__ = [ "sway", + "swaybg", "swaylock", "swayidle", "waybar", "dmenu", - "light", + "brightnessctl", "grim", "slurp", "pavucontrol", -- cgit v1.2.3-70-g09d2 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> --- .github/workflows/iso-build.yaml | 12 +- .github/workflows/mypy.yaml | 6 +- .github/workflows/python-build.yml | 27 +- .gitignore | 3 +- .pre-commit-config.yaml | 42 + PKGBUILD | 2 +- README.md | 201 +- archinstall/__init__.py | 143 +- archinstall/__main__.py | 6 +- 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 + archinstall/lib/configuration.py | 69 +- archinstall/lib/disk/__init__.py | 47 +- archinstall/lib/disk/blockdevice.py | 301 --- archinstall/lib/disk/btrfs/__init__.py | 56 - archinstall/lib/disk/btrfs/btrfs_helpers.py | 136 -- archinstall/lib/disk/btrfs/btrfspartition.py | 109 -- archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py | 192 -- archinstall/lib/disk/device_handler.py | 599 ++++++ archinstall/lib/disk/device_model.py | 1033 ++++++++++ archinstall/lib/disk/diskinfo.py | 40 - archinstall/lib/disk/dmcryptdev.py | 48 - archinstall/lib/disk/encryption.py | 174 -- archinstall/lib/disk/encryption_menu.py | 179 ++ archinstall/lib/disk/fido.py | 94 + archinstall/lib/disk/filesystem.py | 343 +--- archinstall/lib/disk/helpers.py | 556 ------ archinstall/lib/disk/mapperdev.py | 92 - archinstall/lib/disk/partition.py | 661 ------- archinstall/lib/disk/partitioning_menu.py | 335 ++++ archinstall/lib/disk/subvolume_menu.py | 101 + archinstall/lib/disk/user_guides.py | 240 --- archinstall/lib/disk/validators.py | 48 - archinstall/lib/general.py | 31 +- archinstall/lib/global_menu.py | 364 ++++ archinstall/lib/hsm/__init__.py | 1 - archinstall/lib/hsm/fido.py | 109 -- archinstall/lib/installer.py | 670 +++---- archinstall/lib/locale_helpers.py | 20 +- archinstall/lib/luks.py | 331 ++-- archinstall/lib/menu/__init__.py | 11 +- archinstall/lib/menu/abstract_menu.py | 200 +- archinstall/lib/menu/global_menu.py | 429 ----- archinstall/lib/menu/list_manager.py | 46 +- archinstall/lib/menu/menu.py | 114 +- archinstall/lib/menu/simple_menu.py | 2002 -------------------- archinstall/lib/menu/table_selection_menu.py | 68 +- archinstall/lib/mirrors.py | 4 +- archinstall/lib/models/__init__.py | 5 +- archinstall/lib/models/bootloader.py | 40 + archinstall/lib/models/dataclasses.py | 136 -- archinstall/lib/models/disk_encryption.py | 90 - archinstall/lib/models/gen.py | 146 ++ archinstall/lib/models/network_configuration.py | 2 +- archinstall/lib/models/password_strength.py | 85 - archinstall/lib/models/pydantic.py | 134 -- archinstall/lib/models/subvolume.py | 68 - archinstall/lib/models/users.py | 94 +- archinstall/lib/networking.py | 33 +- archinstall/lib/output.py | 47 +- archinstall/lib/packages/__init__.py | 4 + archinstall/lib/packages/packages.py | 4 +- archinstall/lib/pacman.py | 4 + archinstall/lib/profile/__init__.py | 0 archinstall/lib/profile/profile_menu.py | 203 ++ archinstall/lib/profile/profile_model.py | 35 + archinstall/lib/profile/profiles_handler.py | 391 ++++ archinstall/lib/profiles.py | 340 ---- archinstall/lib/storage.py | 19 +- archinstall/lib/udev/__init__.py | 1 - archinstall/lib/udev/udevadm.py | 17 - archinstall/lib/user_interaction/__init__.py | 16 +- .../user_interaction/backwards_compatible_conf.py | 95 - archinstall/lib/user_interaction/disk_conf.py | 403 +++- archinstall/lib/user_interaction/general_conf.py | 59 +- archinstall/lib/user_interaction/locale_conf.py | 3 +- .../lib/user_interaction/manage_users_conf.py | 21 +- archinstall/lib/user_interaction/network_conf.py | 6 +- .../lib/user_interaction/partitioning_conf.py | 362 ---- archinstall/lib/user_interaction/save_conf.py | 74 +- .../lib/user_interaction/subvolume_config.py | 98 - archinstall/lib/user_interaction/system_conf.py | 133 +- archinstall/lib/user_interaction/utils.py | 47 +- archinstall/lib/utils/__init__.py | 0 archinstall/lib/utils/singleton.py | 15 + archinstall/lib/utils/util.py | 30 + archinstall/locales/ar/LC_MESSAGES/base.po | 209 +- archinstall/locales/base.pot | 225 ++- archinstall/locales/cs/LC_MESSAGES/base.po | 246 ++- archinstall/locales/de/LC_MESSAGES/base.po | 243 ++- archinstall/locales/el/LC_MESSAGES/base.po | 246 ++- archinstall/locales/en/LC_MESSAGES/base.po | 203 +- archinstall/locales/es/LC_MESSAGES/base.po | 246 ++- archinstall/locales/fr/LC_MESSAGES/base.mo | Bin 27591 -> 27562 bytes archinstall/locales/fr/LC_MESSAGES/base.po | 300 ++- archinstall/locales/id/LC_MESSAGES/base.mo | Bin 27433 -> 26392 bytes archinstall/locales/id/LC_MESSAGES/base.po | 274 ++- archinstall/locales/it/LC_MESSAGES/base.po | 246 ++- archinstall/locales/ka/LC_MESSAGES/base.po | 250 ++- archinstall/locales/ko/LC_MESSAGES/base.po | 246 ++- archinstall/locales/languages.json | 2 +- archinstall/locales/nl/LC_MESSAGES/base.po | 242 ++- archinstall/locales/pl/LC_MESSAGES/base.po | 246 ++- archinstall/locales/pt/LC_MESSAGES/base.po | 242 ++- archinstall/locales/pt_BR/LC_MESSAGES/base.mo | Bin 27361 -> 27157 bytes archinstall/locales/pt_BR/LC_MESSAGES/base.po | 248 ++- archinstall/locales/ru/LC_MESSAGES/base.mo | Bin 35760 -> 36023 bytes archinstall/locales/ru/LC_MESSAGES/base.po | 249 ++- archinstall/locales/sv/LC_MESSAGES/base.po | 245 ++- archinstall/locales/ta/LC_MESSAGES/base.po | 246 ++- archinstall/locales/tr/LC_MESSAGES/base.po | 245 ++- archinstall/locales/uk/LC_MESSAGES/base.po | 250 ++- archinstall/locales/ur/LC_MESSAGES/base.po | 243 ++- archinstall/locales/zh-CN/LC_MESSAGES/base.po | 246 ++- archinstall/profiles | 1 - archinstall/scripts/__init__.py | 0 archinstall/scripts/guided.py | 276 +++ archinstall/scripts/minimal.py | 104 + archinstall/scripts/only_hd.py | 104 + archinstall/scripts/swiss.py | 353 ++++ archinstall/scripts/unattended.py | 18 + build_iso.sh | 31 + docs/examples/python.rst | 4 +- examples/__init__.py | 0 examples/auto_discovery_mounted.py | 13 + examples/config-sample.json | 126 +- examples/creds-sample.json | 19 +- examples/full_automated_installation.py | 95 + examples/guided.py | 306 --- examples/interactive_installation.py | 220 +++ examples/mac_address_installation.py | 18 + examples/minimal.py | 75 - examples/minimal_installation.py | 85 + examples/only_hd.py | 151 -- examples/only_hd_installation.py | 63 + examples/swiss.py | 526 ----- examples/unattended.py | 21 - mypy-strict.ini | 102 + mypy.ini | 15 + profiles/52-54-00-12-34-56.py | 62 - profiles/__init__.py | 0 profiles/applications/__init__.py | 0 profiles/applications/awesome.py | 34 - profiles/applications/cockpit.py | 13 - profiles/applications/docker.py | 9 - profiles/applications/httpd.py | 9 - profiles/applications/lighttpd.py | 9 - profiles/applications/mariadb.py | 11 - profiles/applications/nginx.py | 9 - profiles/applications/pipewire.py | 14 - profiles/applications/postgresql.py | 11 - profiles/applications/sshd.py | 9 - profiles/applications/tomcat.py | 12 - profiles/awesome.py | 51 - profiles/bspwm.py | 43 - profiles/budgie.py | 45 - profiles/cinnamon.py | 46 - profiles/cutefish.py | 41 - profiles/deepin.py | 44 - profiles/desktop.py | 97 - profiles/enlightenment.py | 43 - profiles/gnome.py | 45 - profiles/i3.py | 59 - profiles/kde.py | 58 - profiles/lxqt.py | 50 - profiles/mate.py | 42 - profiles/minimal.py | 24 - profiles/qtile.py | 42 - profiles/server.py | 63 - profiles/sway.py | 100 - profiles/xfce4.py | 45 - profiles/xorg.py | 68 - pyproject.toml | 32 +- schema.json | 111 +- 208 files changed, 13870 insertions(+), 10753 deletions(-) create mode 100644 .pre-commit-config.yaml 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 delete mode 100644 archinstall/lib/disk/blockdevice.py delete mode 100644 archinstall/lib/disk/btrfs/__init__.py delete mode 100644 archinstall/lib/disk/btrfs/btrfs_helpers.py delete mode 100644 archinstall/lib/disk/btrfs/btrfspartition.py delete mode 100644 archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py create mode 100644 archinstall/lib/disk/device_handler.py create mode 100644 archinstall/lib/disk/device_model.py delete mode 100644 archinstall/lib/disk/diskinfo.py delete mode 100644 archinstall/lib/disk/dmcryptdev.py delete mode 100644 archinstall/lib/disk/encryption.py create mode 100644 archinstall/lib/disk/encryption_menu.py create mode 100644 archinstall/lib/disk/fido.py delete mode 100644 archinstall/lib/disk/helpers.py delete mode 100644 archinstall/lib/disk/mapperdev.py delete mode 100644 archinstall/lib/disk/partition.py create mode 100644 archinstall/lib/disk/partitioning_menu.py create mode 100644 archinstall/lib/disk/subvolume_menu.py delete mode 100644 archinstall/lib/disk/user_guides.py delete mode 100644 archinstall/lib/disk/validators.py create mode 100644 archinstall/lib/global_menu.py delete mode 100644 archinstall/lib/hsm/__init__.py delete mode 100644 archinstall/lib/hsm/fido.py delete mode 100644 archinstall/lib/menu/global_menu.py delete mode 100644 archinstall/lib/menu/simple_menu.py create mode 100644 archinstall/lib/models/bootloader.py delete mode 100644 archinstall/lib/models/dataclasses.py delete mode 100644 archinstall/lib/models/disk_encryption.py create mode 100644 archinstall/lib/models/gen.py delete mode 100644 archinstall/lib/models/password_strength.py delete mode 100644 archinstall/lib/models/pydantic.py delete mode 100644 archinstall/lib/models/subvolume.py create mode 100644 archinstall/lib/profile/__init__.py create mode 100644 archinstall/lib/profile/profile_menu.py create mode 100644 archinstall/lib/profile/profile_model.py create mode 100644 archinstall/lib/profile/profiles_handler.py delete mode 100644 archinstall/lib/profiles.py delete mode 100644 archinstall/lib/udev/__init__.py delete mode 100644 archinstall/lib/udev/udevadm.py delete mode 100644 archinstall/lib/user_interaction/backwards_compatible_conf.py delete mode 100644 archinstall/lib/user_interaction/partitioning_conf.py delete mode 100644 archinstall/lib/user_interaction/subvolume_config.py create mode 100644 archinstall/lib/utils/__init__.py create mode 100644 archinstall/lib/utils/singleton.py create mode 100644 archinstall/lib/utils/util.py delete mode 120000 archinstall/profiles create mode 100644 archinstall/scripts/__init__.py create mode 100644 archinstall/scripts/guided.py create mode 100644 archinstall/scripts/minimal.py create mode 100644 archinstall/scripts/only_hd.py create mode 100644 archinstall/scripts/swiss.py create mode 100644 archinstall/scripts/unattended.py create mode 100755 build_iso.sh delete mode 100644 examples/__init__.py create mode 100644 examples/auto_discovery_mounted.py create mode 100644 examples/full_automated_installation.py delete mode 100644 examples/guided.py create mode 100644 examples/interactive_installation.py create mode 100644 examples/mac_address_installation.py delete mode 100644 examples/minimal.py create mode 100644 examples/minimal_installation.py delete mode 100644 examples/only_hd.py create mode 100644 examples/only_hd_installation.py delete mode 100644 examples/swiss.py delete mode 100644 examples/unattended.py create mode 100644 mypy-strict.ini create mode 100644 mypy.ini delete mode 100644 profiles/52-54-00-12-34-56.py delete mode 100644 profiles/__init__.py delete mode 100644 profiles/applications/__init__.py delete mode 100644 profiles/applications/awesome.py delete mode 100644 profiles/applications/cockpit.py delete mode 100644 profiles/applications/docker.py delete mode 100644 profiles/applications/httpd.py delete mode 100644 profiles/applications/lighttpd.py delete mode 100644 profiles/applications/mariadb.py delete mode 100644 profiles/applications/nginx.py delete mode 100644 profiles/applications/pipewire.py delete mode 100644 profiles/applications/postgresql.py delete mode 100644 profiles/applications/sshd.py delete mode 100644 profiles/applications/tomcat.py delete mode 100644 profiles/awesome.py delete mode 100644 profiles/bspwm.py delete mode 100644 profiles/budgie.py delete mode 100644 profiles/cinnamon.py delete mode 100644 profiles/cutefish.py delete mode 100644 profiles/deepin.py delete mode 100644 profiles/desktop.py delete mode 100644 profiles/enlightenment.py delete mode 100644 profiles/gnome.py delete mode 100644 profiles/i3.py delete mode 100644 profiles/kde.py delete mode 100644 profiles/lxqt.py delete mode 100644 profiles/mate.py delete mode 100644 profiles/minimal.py delete mode 100644 profiles/qtile.py delete mode 100644 profiles/server.py delete mode 100644 profiles/sway.py delete mode 100644 profiles/xfce4.py delete mode 100644 profiles/xorg.py (limited to 'profiles') diff --git a/.github/workflows/iso-build.yaml b/.github/workflows/iso-build.yaml index ab4e6f5f..00e2c13f 100644 --- a/.github/workflows/iso-build.yaml +++ b/.github/workflows/iso-build.yaml @@ -32,17 +32,7 @@ jobs: - run: cat /etc/os-release - run: pacman-key --init - run: pacman --noconfirm -Sy archlinux-keyring - - run: mkdir -p /tmp/archlive/airootfs/root/archinstall-git; cp -r . /tmp/archlive/airootfs/root/archinstall-git - - run: echo "pip uninstall archinstall -y; cd archinstall-git; rm -rf dist; python -m build -n; pip install dist/archinstall*.whl" > /tmp/archlive/airootfs/root/.zprofile - - run: echo "echo \"This is an unofficial ISO for development and testing of archinstall. No support will be provided.\"" >> /tmp/archlive/airootfs/root/.zprofile - - run: echo "echo \"This ISO was built from Git SHA $GITHUB_SHA\"" >> /tmp/archlive/airootfs/root/.zprofile - - run: echo "echo \"Type archinstall to launch the installer.\"" >> /tmp/archlive/airootfs/root/.zprofile - - run: cat /tmp/archlive/airootfs/root/.zprofile - - run: pacman -Sy; pacman --noconfirm -S git archiso - - run: cp -r /usr/share/archiso/configs/releng/* /tmp/archlive - - run: echo -e "git\npython\npython-pip\npython-build\npython-flit\npython-setuptools\npython-wheel" >> /tmp/archlive/packages.x86_64 - - run: find /tmp/archlive - - run: cd /tmp/archlive; mkarchiso -v -w work/ -o out/ ./ + - run: ./build_iso.sh - uses: actions/upload-artifact@v3 with: name: Arch Live ISO diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index 20c98f3b..8689570f 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -15,8 +15,4 @@ jobs: # one day this will be enabled # run: mypy --strict --module archinstall || exit 0 - name: run mypy - run: mypy --follow-imports=silent archinstall/lib/menu/abstract_menu.py archinstall/lib/menu/global_menu.py - archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py - archinstall/lib/disk/blockdevice.py archinstall/lib/user_interaction/subvolume_config.py archinstall/lib/disk/btrfs/btrfs_helpers.py - archinstall/lib/translationhandler.py archinstall/lib/disk/diskinfo.py archinstall/lib/menu/table_selection_menu.py archinstall/lib/hsm - archinstall/lib/disk/encryption.py archinstall/lib/models/disk_encryption.py + run: mypy --config-file mypy.ini diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index 647ad70e..f98ce160 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -7,20 +7,37 @@ on: [ push, pull_request ] jobs: deploy: runs-on: ubuntu-latest + container: + image: archlinux:latest + options: --privileged steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.x' - - name: Install dependencies + python-version: '3.10' + - name: Prepare arch + run: | + pacman-key --init + pacman --noconfirm -Sy archlinux-keyring + pacman --noconfirm -Sy python-pyparted pkgconfig gcc + - name: Install build dependencies run: | python -m pip install --upgrade pip - pip install build twine + pip install --upgrade build twine wheel setuptools installer + pip uninstall archinstall -y + - name: Install package dependencies + run: | + pip install --upgrade simple-term-menu pyparted - name: Build archinstall + run: python -m build --wheel --no-isolation + - name: Install archinstall + run: python -m installer dist/*.whl + - name: Run archinstall run: | - python -m build . --wheel + python -V + archinstall -v - uses: actions/upload-artifact@v3 with: name: archinstall - path: dist/* \ No newline at end of file + path: dist/* diff --git a/.gitignore b/.gitignore index 40e00e87..18173914 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ venv **/cmd_history.txt **/*.*~ /*.sig -/*.json \ No newline at end of file +/*.json +requirements.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..87128289 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +default_stages: ['commit'] +repos: + - repo: https://github.com/pycqa/autoflake + rev: v2.0.2 + hooks: + - id: autoflake + args: [ + '--in-place', + '--remove-all-unused-imports', + '--ignore-init-module-imports' + ] + files: \.py$ + require_serial: true + fail_fast: true + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + # general hooks: + - id: check-added-large-files # Prevent giant files from being committed + args: ['--maxkb=5000'] + - id: check-merge-conflict # Check for files that contain merge conflict strings + - id: check-symlinks # Checks for symlinks which do not point to anything + - id: check-yaml # Attempts to load all yaml files to verify syntax + - id: destroyed-symlinks # Detects symlinks which are changed to regular files + - id: detect-private-key # Checks for the existence of private keys + - id: end-of-file-fixer # Makes sure files end in a newline and only a newline + - id: trailing-whitespace # Trims trailing whitespace + # Python specific hooks: + - id: check-ast # Simply check whether files parse as valid python + - id: check-docstring-first # Checks for a common error of placing code before the docstring + - repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + args: [--config=.flake8] + fail_fast: true + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.1.1 + hooks: + - id: mypy + args: [--config=mypy.ini] + fail_fast: true diff --git a/PKGBUILD b/PKGBUILD index 08c2001b..69f81f49 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -13,6 +13,7 @@ license=(GPL3) depends=( 'python' 'systemd' + 'python-pyparted' ) makedepends=( 'python-setuptools' @@ -48,7 +49,6 @@ prepare() { # use real directories for examples and profiles, as symlinks do not work rm -fv $pkgname/{examples,profiles} - mv -v examples profiles $pkgname/ } build() { diff --git a/README.md b/README.md index d83a8901..517621bc 100644 --- a/README.md +++ b/README.md @@ -26,44 +26,60 @@ Assuming you are on an Arch Linux live-ISO: # archinstall +#### Advanced Some additional options that are not needed by most users are hidden behind the `--advanced` flag. ## Running from a declarative configuration file or URL -Prerequisites: - 1. Edit the [configuration file](https://github.com/archlinux/archinstall/blob/master/examples/config-sample.json) according to your requirements. +`archinstall` can be run with a JSON configuration file. There are 2 different configuration files to consider, +the `user_configuration.json` contains all general installation configuration, whereas the `user_credentials.json` +contains the sensitive user configuration such as user password, root password and encryption password. -Assuming you are on a Arch Linux live-ISO and booted into EFI mode. +An example of the user configuration file can be found here +[configuration file](https://github.com/archlinux/archinstall/blob/master/examples/config-sample.json) +and example of the credentials configuration here +[credentials file](https://github.com/archlinux/archinstall/blob/master/examples/creds-sample.json). - # archinstall --config --disk-layout --creds +**HINT:** The configuration files can be auto-generated by starting `archisntall`, configuring all desired menu +points and then going to `Save configuration`. + +To load the configuration file into `archinstall` run the following command +``` +archinstall --config --creds +``` # Available Languages -Archinstall is available in different languages which have been contributed and are maintained by the community. +Archinstall is available in different languages which have been contributed and are maintained by the community. Current translations are listed below and vary in the amount of translations per language ``` English -Deutsch -Español -Français +Arabic +Brazilian Portuguese +Czech +Dutch +French +Georgian +German Indonesian -Italiano -Nederlands -Polskie -Português do Brasil -Português -Svenska -Türkçe -čeština -Русский -اردو -Ελληνικά -தமிழ் +Italian +Korean +Modern Greek +Polish +Portuguese +Russian +Spanish +Swedish +Tamil +Turkish +Ukrainian +Urdu ``` -Any contributions to the translations are more than welcome, and to get started please follow [the guide](https://github.com/archlinux/archinstall/blob/master/archinstall/locales/README.md) +Any contributions to the translations are more than welcome, +to get started please follow [the guide](https://github.com/archlinux/archinstall/blob/master/archinstall/locales/README.md) -# Help? +# Help or Issues Submit an issue here on GitHub, or submit a post in the discord help channel.
When doing so, attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you! @@ -86,73 +102,111 @@ Therefore, Archinstall will try its best to not introduce any breaking changes e # Scripting your own installation -You could just copy [guided.py](https://github.com/archlinux/archinstall/blob/master/examples/guided.py) as a starting point. - -However, assuming you're building your own ISO and want to create an automated installation process, or you want to install virtual machines onto local disk images, here is a [minimal example](https://github.com/archlinux/archinstall/blob/master/examples/minimal.py) of how to install using archinstall as a Python library:
- -```python -import archinstall, getpass - -# Select a harddrive and a disk password -harddrive = archinstall.select_disk(archinstall.all_blockdevices(partitions=False)) -disk_password = getpass.getpass(prompt='Disk password (won\'t echo): ') - -# We disable safety precautions in the library that protects the partitions -harddrive.keep_partitions = False - -# First, we configure the basic filesystem layout -with archinstall.Filesystem(harddrive, archinstall.GPT) as fs: - # We create a filesystem layout that will use the entire drive - # (this is a helper function, you can partition manually as well) - fs.use_entire_disk(root_filesystem_type='btrfs') - - boot = fs.find_partition('/boot') - root = fs.find_partition('/') - - boot.format('vfat') - - # Set the flag for encrypted to allow for encryption and then encrypt - root.encrypted = True - root.encrypt(password=disk_password) +## Scripting interactive installation -with archinstall.luks2(root, 'luksloop', disk_password) as unlocked_root: - unlocked_root.format(root.filesystem) - unlocked_root.mount('/mnt') +There are some examples in the `examples/` directory that should serve as a starting point. - boot.mount('/mnt/boot') +The following is a small example of how to script your own *interative* installation: -with archinstall.Installer('/mnt') as installation: - if installation.minimal_installation(hostname='minimal-arch'): - installation.add_bootloader() - - installation.add_additional_packages(['nano', 'wget', 'git']) - - # Optionally, install a profile of choice. - # In this case, we install a minimal profile that is empty - installation.install_profile('minimal') - - user = User('devel', 'devel', False) - installation.create_users(user) - installation.user_set_pw('root', 'airoot') +```python +from pathlib import Path + +from archinstall import Installer, ProfileConfiguration, profile_handler, User +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 + +fs_type = FilesystemType('ext4') + +# Select a device to use for the installation +disk_config = select_disk_config() + +# Optional: ask for disk encryption configuration +data_store = {} +disk_encryption = DiskEncryptionMenu(disk_config.device_modifications, data_store).run() + +# initiate file handler with the disk config and the optional disk encryption config +fs_handler = FilesystemHandler(disk_config, disk_encryption) + +# perform all file operations +# WARNING: this will potentially format the filesystem and delete all data +fs_handler.perform_filesystem_operations() + +mountpoint = Path('/tmp') + +with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=['linux'] +) as installation: + installation.mount_ordered_layout() + installation.minimal_installation(hostname='minimal-arch') + installation.add_additional_packages(['nano', 'wget', 'git']) + + # 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) + + user = User('archinstall', 'password', True) + installation.create_users(user) ``` This installer will perform the following: -* Prompt the user to select a disk and disk-password -* Proceed to wipe the selected disk with a `GPT` partition table on a UEFI system and MBR on a BIOS system. -* Sets up a default 100% used disk with encryption. +* Prompt the user to configurate the disk partitioning +* Prompt the user to setup disk encryption +* Create a file handler instance for the configured disk and the optional disk encryption +* Perform the disk operations (WARNING: this will potentially format the disks and erase all data) * Installs a basic instance of Arch Linux *(base base-devel linux linux-firmware btrfs-progs efibootmgr)* * Installs and configures a bootloader to partition 0 on uefi. On BIOS, it sets the root to partition 0. * Install additional packages *(nano, wget, git)* +* Create a new user > **Creating your own ISO with this script on it:** Follow [ArchISO](https://wiki.archlinux.org/index.php/archiso)'s guide on how to create your own ISO. +## Script non-interactive automated installation + +For an example of a fully scripted, automated installation please see the example +[full_automated_installation.py](https://github.com/archlinux/archinstall/blob/master/examples/full_automated_installation.py) + ## Unattended installation based on MAC address -Archinstall comes with an [unattended](https://github.com/archlinux/archinstall/blob/master/examples/unattended.py) example which will look for a matching profile for the machine it is being run on, based on any local MAC address. -For instance, if the machine that [unattended](https://github.com/archlinux/archinstall/blob/master/examples/unattended.py) is run on has the MAC address `52:54:00:12:34:56` it will look for a profile called [profiles/52-54-00-12-34-56.py](https://github.com/archlinux/archinstall/blob/master/profiles/52-54-00-12-34-56.py). +Archinstall comes with an [unattended](https://github.com/archlinux/archinstall/blob/master/examples/mac_address_installation.py) +example which will look for a matching profile for the machine it is being run on, based on any local MAC address. +For instance, if the machine the code is executed on has the MAC address `52:54:00:12:34:56` it will look for a profile called +[52-54-00-12-34-56.py](https://github.com/archlinux/archinstall/default_profiles/tailored.py). If it's found, the unattended installation will commence and source that profile as its installation procedure. +# Profiles + +`archinstall` ships with a set of pre-defined profiles that can be chosen during the installation process. + +| *Desktop* | *Server* | +|---------------|------------| +| Awesome | Cockpit | +| Bspwm | Docker | +| Budgie | Lighttpd | +| Cinnamon | Mariadb | +| Cutefish | Nginx | +| Deepin | Postgresql | +| Enlightenment | Tomcat | +| Gnome | httpd | +| Kde | sshd | +| Lxqt | | +| Mate | | +| Qtile | | +| Sway | | +| Xfce4 | | +| i3-wm | | + +The definitions of the profiles and what packages they will install can be seen directly in the menu or +[default profiles](https://github.com/archlinux/archinstall/default_profiles) + + # Testing ## Using a Live ISO Image @@ -167,8 +221,7 @@ you can replace the version of archinstall with a new version and run that with 4. Now clone the latest repository with `git clone https://github.com/archlinux/archinstall` 5. Enter the repository with `cd archinstall` *At this stage, you can choose to check out a feature branch for instance with `git checkout v2.3.1-rc1`* -6. Build the project and install it using `python setup.py install` - *If you get a 'No Module named setuptools' error, run `pacman -S python-setuptools`* +6. Build the project and install it using `pip install` After this, running archinstall with `python -m archinstall` will run against whatever branch you chose in step 5. diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 9de4a3ec..8cb6ced9 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -1,8 +1,12 @@ """Arch Linux installer - guided, templates etc.""" -import typing +import importlib from argparse import ArgumentParser, Namespace -from .lib.disk import * +from .lib import disk +from .lib import menu +from .lib import models as models +from .lib import packages + from .lib.exceptions import * from .lib.general import * from .lib.hardware import * @@ -10,41 +14,20 @@ 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.models.network_configuration import NetworkConfigurationHandler -from .lib.models.users import User from .lib.networking import * from .lib.output import * -from .lib.models.dataclasses import ( - VersionDef, - PackageSearchResult, - PackageSearch, - LocalPackage -) -from .lib.packages.packages import ( - group_search, - package_search, - find_package, - find_packages, - installed_package, - validate_package_list, -) -from .lib.profiles 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.menu import Menu -from .lib.menu.list_manager import ListManager -from .lib.menu.text_input import TextInput -from .lib.menu.global_menu import GlobalMenu -from .lib.menu.abstract_menu import ( - Selector, - AbstractMenu -) +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.plugins import plugins, load_plugin # This initiates the plugin loading ceremony from .lib.configuration import * -from .lib.udev import udevadm_info + + parser = ArgumentParser() __version__ = "2.5.4" @@ -66,8 +49,6 @@ def define_arguments(): parser.add_argument("-v", "--version", action="version", version="%(prog)s " + __version__) parser.add_argument("--config", nargs="?", help="JSON configuration file or URL") parser.add_argument("--creds", nargs="?", help="JSON credentials configuration file") - parser.add_argument("--disk_layouts","--disk_layout","--disk-layouts","--disk-layout",nargs="?", - help="JSON disk layout file") parser.add_argument("--silent", action="store_true", help="WARNING: Disables all prompts for input and confirmation. If no configuration is provided, this is ignored") parser.add_argument("--dry-run", "--dry_run", action="store_true", @@ -79,6 +60,7 @@ def define_arguments(): parser.add_argument("--no-pkg-lookups", action="store_true", default=False, help="Disabled package validation specifically prior to starting installation.") parser.add_argument("--plugin", nargs="?", type=str) + def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, error :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. @@ -129,7 +111,8 @@ def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, erro print(f" We ignore the entry {element} as it isn't related to any argument") return config -def cleanup_empty_args(args :typing.Union[Namespace, dict]) -> dict: + +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) @@ -161,7 +144,7 @@ def get_arguments() -> Dict[str, Any]: 3) Amend Change whatever is needed on the configuration dictionary (it could be done in post_process_arguments but this ougth to be left to changes anywhere else in the code, not in the arguments dictionary """ - config = {} + config: Dict[str, Any] = {} args, unknowns = parser.parse_known_args() # preprocess the JSON files. # TODO Expand the url access to the other JSON file arguments ? @@ -174,15 +157,15 @@ def get_arguments() -> Dict[str, Any]: exit(1) # load the parameters. first the known, then the unknowns - args = cleanup_empty_args(args) - config.update(args) + clean_args = cleanup_empty_args(args) + config.update(clean_args) config.update(parse_unspecified_argument_list(unknowns)) # amend the parameters (check internal consistency) # Installation can't be silent if config is not passed - if args.get('config') is None: + if clean_args.get('config') is None: config["silent"] = False else: - config["silent"] = args.get('silent') + config["silent"] = clean_args.get('silent') # avoiding a compatibility issue if 'dry-run' in config: @@ -190,29 +173,24 @@ def get_arguments() -> Dict[str, Any]: return config + def load_config(): """ refine and set some arguments. Formerly at the scripts """ from .lib.models import NetworkConfiguration + arguments.setdefault('sys-language', 'en_US') + arguments.setdefault('sys-encoding', 'utf-8') + if (archinstall_lang := arguments.get('archinstall-language', None)) is not None: arguments['archinstall-language'] = TranslationHandler().get_language_by_name(archinstall_lang) - if arguments.get('harddrives', None) is not None: - if type(arguments['harddrives']) is str: - arguments['harddrives'] = arguments['harddrives'].split(',') - arguments['harddrives'] = [BlockDevice(BlockDev) for BlockDev in arguments['harddrives']] - # Temporarily disabling keep_partitions if config file is loaded - # Temporary workaround to make Desktop Environments work - - if arguments.get('profile', None) is not None: - if type(arguments.get('profile', None)) is dict: - arguments['profile'] = Profile(None, arguments.get('profile', None)['path']) - else: - arguments['profile'] = Profile(None, arguments.get('profile', None)) + if disk_config := arguments.get('disk_config', {}): + arguments['disk_config'] = disk.DiskLayoutConfiguration.parse_arg(disk_config) - storage['_desktop_profile'] = arguments.get('desktop-environment', None) + if profile_config := arguments.get('profile_config', None): + arguments['profile_config'] = ProfileConfiguration.parse_arg(profile_config) if arguments.get('mirror-region', None) is not None: if type(arguments.get('mirror-region', None)) is dict: @@ -221,12 +199,6 @@ def load_config(): selected_region = arguments.get('mirror-region', None) arguments['mirror-region'] = {selected_region: list_mirrors()[selected_region]} - arguments.setdefault('sys-language', 'en_US') - arguments.setdefault('sys-encoding', 'utf-8') - - if arguments.get('gfx_driver', None) is not None: - storage['gfx_driver_packages'] = AVAILABLE_GFX_DRIVERS.get(arguments.get('gfx_driver', None), None) - if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) @@ -240,10 +212,13 @@ def load_config(): superusers = arguments.get('!superusers', None) arguments['!users'] = User.parse_arguments(users, superusers) - if arguments.get('disk_encryption', None) is not None and arguments.get('disk_layouts', None) is not None: + if arguments.get('bootloader', None) is not None: + arguments['bootloader'] = 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', '') - arguments['disk_encryption'] = DiskEncryption.parse_arg( - arguments['disk_layouts'], + arguments['disk_encryption'] = disk.DiskEncryption.parse_arg( + arguments['disk_config'], arguments['disk_encryption'], password ) @@ -251,8 +226,8 @@ def load_config(): def post_process_arguments(arguments): storage['arguments'] = arguments - if arguments.get('mount_point'): - storage['MOUNT_POINT'] = arguments['mount_point'] + if mountpoint := arguments.get('mount_point', None): + 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) @@ -260,53 +235,31 @@ def post_process_arguments(arguments): if arguments.get('plugin', None): load_plugin(arguments['plugin']) - if arguments.get('disk_layouts', None) is not None: - layout_storage = {} - if not json_stream_to_structure('--disk_layouts',arguments['disk_layouts'],layout_storage): - exit(1) - else: - if arguments.get('harddrives') is None: - arguments['harddrives'] = [disk for disk in layout_storage] - # backward compatibility. Change partition.format for partition.wipe - for disk in layout_storage: - for i, partition in enumerate(layout_storage[disk].get('partitions',[])): - if 'format' in partition: - partition['wipe'] = partition['format'] - del partition['format'] - elif 'btrfs' in partition: - partition['btrfs']['subvolumes'] = Subvolume.parse_arguments(partition['btrfs']['subvolumes']) - arguments['disk_layouts'] = layout_storage - load_config() define_arguments() -arguments = get_arguments() +arguments: Dict[str, Any] = get_arguments() post_process_arguments(arguments) + # @archinstall.plugin decorator hook to programmatically add -# plugins in runtime. Useful in profiles and other things. +# plugins in runtime. Useful in profiles_bck and other things. def plugin(f, *args, **kwargs): plugins[f.__name__] = f def run_as_a_module(): """ - Since we're running this as a 'python -m archinstall' module OR - a nuitka3 compiled version of the project. - This function and the file __main__ acts as a entry point. + This can either be run as the compiled and installed application: python setup.py install + OR straight as a module: python -m archinstall + In any case we will be attempting to load the provided script to be run from the scripts/ folder """ + script = arguments.get('script', None) - # Add another path for finding profiles, so that list_profiles() in Script() can find guided.py, unattended.py etc. - storage['PROFILE_PATH'].append(os.path.abspath(f'{os.path.dirname(__file__)}/examples')) - try: - script = Script(arguments.get('script', None)) - except ProfileNotFound as err: - print(f"Couldn't find file: {err}") - sys.exit(1) - - os.chdir(os.path.abspath(os.path.dirname(__file__))) + if script is None: + print('No script to run provided') - # Remove the example directory from the PROFILE_PATH, to avoid guided.py etc shows up in user input questions. - storage['PROFILE_PATH'].pop() - script.execute() + mod_name = f'archinstall.scripts.{script}' + # by loading the module we'll automatically run the script + importlib.import_module(mod_name) diff --git a/archinstall/__main__.py b/archinstall/__main__.py index e125930f..69a79855 100644 --- a/archinstall/__main__.py +++ b/archinstall/__main__.py @@ -5,9 +5,13 @@ import pathlib # Load .git version before the builtin version if pathlib.Path('./archinstall/__init__.py').absolute().exists(): spec = importlib.util.spec_from_file_location("archinstall", "./archinstall/__init__.py") + + if spec is None or spec.loader is None: + raise ValueError('Could not retrieve spec from file: archinstall/__init__.py') + archinstall = importlib.util.module_from_spec(spec) sys.modules["archinstall"] = archinstall - spec.loader.exec_module(sys.modules["archinstall"]) + spec.loader.exec_module(archinstall) else: import archinstall 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 + ) diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index c036783f..77fed755 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -2,27 +2,15 @@ import os import json import stat import logging -import pathlib -from typing import Optional, Dict +from pathlib import Path +from typing import Optional, Dict, Any, TYPE_CHECKING -from .hsm.fido import Fido2 -from .models.disk_encryption import DiskEncryption from .storage import storage from .general import JSON, UNSAFE_JSON from .output import log -from .exceptions import RequirementError - - -def configuration_sanity_check(): - disk_encryption: DiskEncryption = storage['arguments'].get('disk_encryption') - if disk_encryption is not None and disk_encryption.hsm_device: - if not Fido2.get_fido2_devices(): - raise RequirementError( - f"In order to use HSM to pair with the disk encryption," - + f" one needs to be accessible through /dev/hidraw* and support" - + f" the FIDO2 protocol. You can check this by running" - + f" 'systemd-cryptenroll --fido2-device=list'." - ) + +if TYPE_CHECKING: + _: Any class ConfigurationOutput: @@ -35,13 +23,11 @@ class ConfigurationOutput: :type config: Dict """ self._config = config - self._user_credentials = {} - self._disk_layout = None - self._user_config = {} - self._default_save_path = pathlib.Path(storage.get('LOG_PATH', '.')) + self._user_credentials: Dict[str, Any] = {} + self._user_config: Dict[str, Any] = {} + self._default_save_path = Path(storage.get('LOG_PATH', '.')) self._user_config_file = 'user_configuration.json' self._user_creds_file = "user_credentials.json" - self._disk_layout_file = "user_disk_layout.json" self._sensitive = ['!users'] self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run'] @@ -56,23 +42,18 @@ class ConfigurationOutput: def user_configuration_file(self): return self._user_config_file - @property - def disk_layout_file(self): - return self._disk_layout_file - def _process_config(self): for key in self._config: if key in self._sensitive: self._user_credentials[key] = self._config[key] - elif key == 'disk_layouts': - self._disk_layout = self._config[key] elif key in self._ignore: pass else: self._user_config[key] = self._config[key] - if key == 'disk_encryption' and self._config[key]: # special handling for encryption password - self._user_credentials['encryption_password'] = self._config[key].encryption_password + # special handling for encryption password + if key == 'disk_encryption' and self._config[key] is not None: + self._user_credentials['encryption_password'] = self._config[key].encryption_password def user_config_to_json(self) -> str: return json.dumps({ @@ -81,11 +62,6 @@ class ConfigurationOutput: 'version': storage['__version__'] }, indent=4, sort_keys=True, cls=JSON) - def disk_layout_to_json(self) -> Optional[str]: - if self._disk_layout: - return json.dumps(self._disk_layout, indent=4, sort_keys=True, cls=JSON) - return None - def user_credentials_to_json(self) -> Optional[str]: if self._user_credentials: return json.dumps(self._user_credentials, indent=4, sort_keys=True, cls=UNSAFE_JSON) @@ -96,15 +72,11 @@ class ConfigurationOutput: log(" -- Chosen configuration --", level=logging.DEBUG) user_conig = self.user_config_to_json() - disk_layout = self.disk_layout_to_json() log(user_conig, level=logging.INFO) - if disk_layout: - log(disk_layout, level=logging.INFO) - print() - def _is_valid_path(self, dest_path :pathlib.Path) -> bool: + 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()), @@ -113,7 +85,7 @@ class ConfigurationOutput: return False return True - def save_user_config(self, dest_path :pathlib.Path = None): + def save_user_config(self, dest_path: Path): if self._is_valid_path(dest_path): target = dest_path / self._user_config_file @@ -122,7 +94,7 @@ class ConfigurationOutput: os.chmod(str(dest_path / self._user_config_file), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) - def save_user_creds(self, dest_path :pathlib.Path = None): + def save_user_creds(self, dest_path: Path): if self._is_valid_path(dest_path): if user_creds := self.user_credentials_to_json(): target = dest_path / self._user_creds_file @@ -132,21 +104,10 @@ class ConfigurationOutput: os.chmod(str(target), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) - def save_disk_layout(self, dest_path :pathlib.Path = None): - if self._is_valid_path(dest_path): - if disk_layout := self.disk_layout_to_json(): - target = dest_path / self._disk_layout_file - - with target.open('w') as config_file: - config_file.write(disk_layout) - - os.chmod(str(target), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) - - def save(self, dest_path :pathlib.Path = None): + def save(self, dest_path: Optional[Path] = None): if not dest_path: dest_path = self._default_save_path if self._is_valid_path(dest_path): self.save_user_config(dest_path) self.save_user_creds(dest_path) - self.save_disk_layout(dest_path) diff --git a/archinstall/lib/disk/__init__.py b/archinstall/lib/disk/__init__.py index 352d04b9..cdc96373 100644 --- a/archinstall/lib/disk/__init__.py +++ b/archinstall/lib/disk/__init__.py @@ -1,7 +1,40 @@ -from .btrfs import * -from .helpers import * -from .blockdevice import BlockDevice -from .filesystem import Filesystem, MBR, GPT -from .partition import * -from .user_guides import * -from .validators import * \ No newline at end of file +from .device_handler import device_handler, disk_layouts +from .fido import Fido2 +from .filesystem import FilesystemHandler +from .subvolume_menu import SubvolumeMenu +from .partitioning_menu import ( + manual_partitioning, + PartitioningList +) +from .device_model import ( + _DeviceInfo, + BDevice, + DiskLayoutType, + DiskLayoutConfiguration, + PartitionTable, + Unit, + Size, + SubvolumeModification, + DeviceGeometry, + PartitionType, + PartitionFlag, + FilesystemType, + ModificationStatus, + PartitionModification, + DeviceModification, + EncryptionType, + DiskEncryption, + Fido2Device, + LsblkInfo, + CleanType, + get_lsblk_info, + get_all_lsblk_info, + get_lsblk_by_mountpoint +) +from .encryption_menu import ( + select_encryption_type, + select_encrypted_password, + select_hsm, + select_partitions_to_encrypt, + DiskEncryptionMenu, +) diff --git a/archinstall/lib/disk/blockdevice.py b/archinstall/lib/disk/blockdevice.py deleted file mode 100644 index 178b786a..00000000 --- a/archinstall/lib/disk/blockdevice.py +++ /dev/null @@ -1,301 +0,0 @@ -from __future__ import annotations -import json -import logging -import time - -from collections import OrderedDict -from dataclasses import dataclass -from typing import Optional, Dict, Any, Iterator, List, TYPE_CHECKING - -from ..exceptions import DiskError, SysCallError -from ..output import log -from ..general import SysCommand -from ..storage import storage - - -if TYPE_CHECKING: - from .partition import Partition - _: Any - - -@dataclass -class BlockSizeInfo: - start: str - end: str - size: str - - -@dataclass -class BlockInfo: - pttype: str - ptuuid: str - size: int - tran: Optional[str] - rota: bool - free_space: Optional[List[BlockSizeInfo]] - - -class BlockDevice: - def __init__(self, path :str, info :Optional[Dict[str, Any]] = None): - if not info: - from .helpers import all_blockdevices - # If we don't give any information, we need to auto-fill it. - # Otherwise any subsequent usage will break. - self.info = all_blockdevices(partitions=False)[path].info - else: - self.info = info - - self._path = path - self.keep_partitions = True - self._block_info = self._fetch_information() - self._partitions: Dict[str, 'Partition'] = {} - - self._load_partitions() - - # TODO: Currently disk encryption is a BIT misleading. - # It's actually partition-encryption, but for future-proofing this - # I'm placing the encryption password on a BlockDevice level. - - def __repr__(self, *args :str, **kwargs :str) -> str: - return self._str_repr - - @property - def path(self) -> str: - return self._path - - @property - def _str_repr(self) -> str: - return f"BlockDevice({self._device_or_backfile}, size={self.size}GB, free_space={self._safe_free_space()}, bus_type={self.bus_type})" - - def as_json(self) -> Dict[str, Any]: - return { - str(_('Device')): self._device_or_backfile, - str(_('Size')): f'{self.size}GB', - str(_('Free space')): f'{self._safe_free_space()}', - str(_('Bus-type')): f'{self.bus_type}' - } - - def __iter__(self) -> Iterator['Partition']: - for partition in self.partitions: - yield self.partitions[partition] - - def __getitem__(self, key :str, *args :str, **kwargs :str) -> Any: - if hasattr(self, key): - return getattr(self, key) - - if self.info and key in self.info: - return self.info[key] - - raise KeyError(f'{self.info} does not contain information: "{key}"') - - def __lt__(self, left_comparitor :'BlockDevice') -> bool: - return self._path < left_comparitor.path - - def json(self) -> str: - """ - json() has precedence over __dump__, so this is a way - to give less/partial information for user readability. - """ - return self._path - - def __dump__(self) -> Dict[str, Dict[str, Any]]: - return { - self._path: { - 'partuuid': self.uuid, - 'wipe': self.info.get('wipe', None), - 'partitions': [part.__dump__() for part in self.partitions.values()] - } - } - - def _call_lsblk(self, path: str) -> Dict[str, Any]: - output = SysCommand(f'lsblk --json -b -o+SIZE,PTTYPE,ROTA,TRAN,PTUUID {self._path}').decode('UTF-8') - if output: - lsblk_info = json.loads(output) - return lsblk_info - - raise DiskError(f'Failed to read disk "{self.path}" with lsblk') - - def _load_partitions(self): - from .partition import Partition - - self._partitions.clear() - - lsblk_info = self._call_lsblk(self._path) - device = lsblk_info['blockdevices'][0] - self._partitions.clear() - - if children := device.get('children', None): - root = f'/dev/{device["name"]}' - for child in children: - part_id = child['name'].removeprefix(device['name']) - self._partitions[part_id] = Partition(root + part_id, block_device=self, part_id=part_id) - - def _get_free_space(self) -> Optional[List[BlockSizeInfo]]: - # NOTE: parted -s will default to `cancel` on prompt, skipping any partition - # that is "outside" the disk. in /dev/sr0 this is usually the case with Archiso, - # so the free will ignore the ESP partition and just give the "free" space. - # Doesn't harm us, but worth noting in case something weird happens. - try: - output = SysCommand(f"parted -s --machine {self._path} print free").decode('utf-8') - if output: - free_lines = [line for line in output.split('\n') if 'free' in line] - sizes = [] - for free_space in free_lines: - _, start, end, size, *_ = free_space.strip('\r\n;').split(':') - sizes.append(BlockSizeInfo(start, end, size)) - - return sizes - except SysCallError as error: - log(f"Could not get free space on {self._path}: {error}", level=logging.DEBUG) - - return None - - def _fetch_information(self) -> BlockInfo: - lsblk_info = self._call_lsblk(self._path) - device = lsblk_info['blockdevices'][0] - free_space = self._get_free_space() - - return BlockInfo( - pttype=device['pttype'], - ptuuid=device['ptuuid'], - size=device['size'], - tran=device['tran'], - rota=device['rota'], - free_space=free_space - ) - - @property - def _device_or_backfile(self) -> Optional[str]: - """ - Returns the actual device-endpoint of the BlockDevice. - If it's a loop-back-device it returns the back-file, - For other types it return self.device - """ - if self.info.get('type') == 'loop': - return self.info['back-file'] - else: - return self.device - - @property - def mountpoint(self) -> None: - """ - A dummy function to enable transparent comparisons of mountpoints. - As blockdevices can't be mounted directly, this will always be None - """ - return None - - @property - def device(self) -> Optional[str]: - """ - Returns the device file of the BlockDevice. - If it's a loop-back-device it returns the /dev/X device, - If it's a ATA-drive it returns the /dev/X device - And if it's a crypto-device it returns the parent device - """ - if "DEVTYPE" not in self.info: - raise DiskError(f'Could not locate backplane info for "{self._path}"') - - if self.info['DEVTYPE'] in ['disk','loop']: - return self._path - elif self.info['DEVTYPE'][:4] == 'raid': - # This should catch /dev/md## raid devices - return self._path - elif self.info['DEVTYPE'] == 'crypt': - if 'pkname' not in self.info: - raise DiskError(f'A crypt device ({self._path}) without a parent kernel device name.') - return f"/dev/{self.info['pkname']}" - else: - log(f"Unknown blockdevice type for {self._path}: {self.info['DEVTYPE']}", level=logging.DEBUG) - - return None - - @property - def partition_type(self) -> str: - return self._block_info.pttype - - @property - def uuid(self) -> str: - return self._block_info.ptuuid - - @property - def size(self) -> float: - from .helpers import convert_size_to_gb - return convert_size_to_gb(self._block_info.size) - - @property - def bus_type(self) -> Optional[str]: - return self._block_info.tran - - @property - def spinning(self) -> bool: - return self._block_info.rota - - @property - def partitions(self) -> Dict[str, 'Partition']: - return OrderedDict(sorted(self._partitions.items())) - - @property - def partition(self) -> List['Partition']: - return list(self.partitions.values()) - - @property - def first_free_sector(self) -> str: - if block_size := self._largest_free_space(): - return block_size.start - else: - return '512MB' - - @property - def first_end_sector(self) -> str: - if block_size := self._largest_free_space(): - return block_size.end - else: - return f"{self.size}GB" - - def _safe_free_space(self) -> str: - if self._block_info.free_space: - sizes = [free_space.size for free_space in self._block_info.free_space] - return '+'.join(sizes) - return '?' - - def _largest_free_space(self) -> Optional[BlockSizeInfo]: - if self._block_info.free_space: - sorted_sizes = sorted(self._block_info.free_space, key=lambda x: x.size, reverse=True) - return sorted_sizes[0] - return None - - def _partprobe(self) -> bool: - return SysCommand(['partprobe', self._path]).exit_code == 0 - - def flush_cache(self) -> None: - self._load_partitions() - - def get_partition(self, uuid :Optional[str] = None, partuuid :Optional[str] = None) -> Partition: - if not uuid and not partuuid: - raise ValueError(f"BlockDevice.get_partition() requires either a UUID or a PARTUUID for lookups.") - - log(f"Retrieving partition PARTUUID={partuuid} or UUID={uuid}", level=logging.DEBUG, fg="gray") - - for count in range(storage.get('DISK_RETRY_ATTEMPTS', 5)): - for partition_index, partition in self.partitions.items(): - try: - if uuid and partition.uuid and partition.uuid.lower() == uuid.lower(): - log(f"Matched UUID={uuid} against {partition.uuid}", level=logging.DEBUG, fg="gray") - return partition - elif partuuid and partition.part_uuid and partition.part_uuid.lower() == partuuid.lower(): - log(f"Matched PARTUUID={partuuid} against {partition.part_uuid}", level=logging.DEBUG, fg="gray") - return partition - except DiskError as error: - # Most likely a blockdevice that doesn't support or use UUID's - # (like Microsoft recovery partition) - log(f"Could not get UUID/PARTUUID of {partition}: {error}", level=logging.DEBUG, fg="gray") - pass - - log(f"uuid {uuid} or {partuuid} not found. Waiting {storage.get('DISK_TIMEOUTS', 1) * count}s for next attempt",level=logging.DEBUG) - self.flush_cache() - time.sleep(storage.get('DISK_TIMEOUTS', 1) * count) - - log(f"Could not find {uuid}/{partuuid} in disk after 5 retries", level=logging.INFO) - log(f"Cache: {self._partitions}") - log(f"Partitions: {self.partitions.items()}") - raise DiskError(f"Partition {uuid}/{partuuid} was never found on {self} despite several attempts.") diff --git a/archinstall/lib/disk/btrfs/__init__.py b/archinstall/lib/disk/btrfs/__init__.py deleted file mode 100644 index a26e0160..00000000 --- a/archinstall/lib/disk/btrfs/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations -import pathlib -import glob -import logging -from typing import Union, Dict, TYPE_CHECKING - -# https://stackoverflow.com/a/39757388/929999 -if TYPE_CHECKING: - from ...installer import Installer - -from .btrfs_helpers import ( - subvolume_info_from_path as subvolume_info_from_path, - find_parent_subvolume as find_parent_subvolume, - setup_subvolumes as setup_subvolumes, - mount_subvolume as mount_subvolume -) -from .btrfssubvolumeinfo import BtrfsSubvolumeInfo as BtrfsSubvolume -from .btrfspartition import BTRFSPartition as BTRFSPartition - -from ...exceptions import DiskError, Deprecated -from ...general import SysCommand -from ...output import log - - -def create_subvolume(installation: Installer, subvolume_location :Union[pathlib.Path, str]) -> bool: - """ - This function uses btrfs to create a subvolume. - - @installation: archinstall.Installer instance - @subvolume_location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot - """ - - installation_mountpoint = installation.target - if type(installation_mountpoint) == str: - installation_mountpoint = pathlib.Path(installation_mountpoint) - # Set up the required physical structure - if type(subvolume_location) == str: - subvolume_location = pathlib.Path(subvolume_location) - - target = installation_mountpoint / subvolume_location.relative_to(subvolume_location.anchor) - - # Difference from mount_subvolume: - # We only check if the parent exists, since we'll run in to "target path already exists" otherwise - if not target.parent.exists(): - target.parent.mkdir(parents=True) - - if glob.glob(str(target / '*')): - raise DiskError(f"Cannot create subvolume at {target} because it contains data (non-empty folder target)") - - # Remove the target if it exists - if target.exists(): - target.rmdir() - - log(f"Creating a subvolume on {target}", level=logging.INFO) - if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0: - raise DiskError(f"Could not create a subvolume at {target}: {cmd}") diff --git a/archinstall/lib/disk/btrfs/btrfs_helpers.py b/archinstall/lib/disk/btrfs/btrfs_helpers.py deleted file mode 100644 index f6d2734a..00000000 --- a/archinstall/lib/disk/btrfs/btrfs_helpers.py +++ /dev/null @@ -1,136 +0,0 @@ -import logging -import re -from pathlib import Path -from typing import Optional, Dict, Any, TYPE_CHECKING - -from ...models.subvolume import Subvolume -from ...exceptions import SysCallError, DiskError -from ...general import SysCommand -from ...output import log -from ...plugins import plugins -from ..helpers import get_mount_info -from .btrfssubvolumeinfo import BtrfsSubvolumeInfo - -if TYPE_CHECKING: - from .btrfspartition import BTRFSPartition - from ...installer import Installer - - -class fstab_btrfs_compression_plugin(): - def __init__(self, partition_dict): - self.partition_dict = partition_dict - - def on_genfstab(self, installation): - with open(f"{installation.target}/etc/fstab", 'r') as fh: - fstab = fh.read() - - # Replace the {installation}/etc/fstab with entries - # using the compress=zstd where the mountpoint has compression set. - with open(f"{installation.target}/etc/fstab", 'w') as fh: - for line in fstab.split('\n'): - # So first we grab the mount options by using subvol=.*? as a locator. - # And we also grab the mountpoint for the entry, for instance /var/log - if (subvoldef := re.findall(',.*?subvol=.*?[\t ]', line)) and (mountpoint := re.findall('[\t ]/.*?[\t ]', line)): - for subvolume in self.partition_dict.get('btrfs', {}).get('subvolumes', []): - # We then locate the correct subvolume and check if it's compressed - if subvolume.compress and subvolume.mountpoint == mountpoint[0].strip(): - # We then sneak in the compress=zstd option if it doesn't already exist: - # We skip entries where compression is already defined - if ',compress=zstd,' not in line: - line = line.replace(subvoldef[0], f",compress=zstd{subvoldef[0]}") - break - - fh.write(f"{line}\n") - - return True - - -def mount_subvolume(installation: 'Installer', device: 'BTRFSPartition', subvolume: Subvolume): - # we normalize the subvolume name (getting rid of slash at the start if exists. - # In our implementation has no semantic load. - # Every subvolume is created from the top of the hierarchy- and simplifies its further use - name = subvolume.name.lstrip('/') - mountpoint = Path(subvolume.mountpoint) - installation_target = Path(installation.target) - - mountpoint = installation_target / mountpoint.relative_to(mountpoint.anchor) - mountpoint.mkdir(parents=True, exist_ok=True) - mount_options = subvolume.options + [f'subvol={name}'] - - log(f"Mounting subvolume {name} on {device} to {mountpoint}", level=logging.INFO, fg="gray") - SysCommand(f"mount {device.path} {mountpoint} -o {','.join(mount_options)}") - - -def setup_subvolumes(installation: 'Installer', partition_dict: Dict[str, Any]): - log(f"Setting up subvolumes: {partition_dict['btrfs']['subvolumes']}", level=logging.INFO, fg="gray") - - for subvolume in partition_dict['btrfs']['subvolumes']: - # we normalize the subvolume name (getting rid of slash at the start if exists. In our implementation has no semantic load. - # Every subvolume is created from the top of the hierarchy- and simplifies its further use - name = subvolume.name.lstrip('/') - - # We create the subvolume using the BTRFSPartition instance. - # That way we ensure not only easy access, but also accurate mount locations etc. - partition_dict['device_instance'].create_subvolume(name, installation=installation) - - # Make the nodatacow processing now - # It will be the main cause of creation of subvolumes which are not to be mounted - # it is not an options which can be established by subvolume (but for whole file systems), and can be - # set up via a simple attribute change in a directory (if empty). And here the directories are brand new - if subvolume.nodatacow: - if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0: - raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}") - - # Make the compress processing now - # it is not an options which can be established by subvolume (but for whole file systems), and can be - # set up via a simple attribute change in a directory (if empty). And here the directories are brand new - # in this way only zstd compression is activaded - # TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated - - if subvolume.compress: - if not any(['compress' in filesystem_option for filesystem_option in partition_dict.get('filesystem', {}).get('mount_options', [])]): - if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0: - raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}") - - if 'fstab_btrfs_compression_plugin' not in plugins: - plugins['fstab_btrfs_compression_plugin'] = fstab_btrfs_compression_plugin(partition_dict) - - -def subvolume_info_from_path(path: Path) -> Optional[BtrfsSubvolumeInfo]: - try: - subvolume_name = '' - result = {} - for index, line in enumerate(SysCommand(f"btrfs subvolume show {path}")): - if index == 0: - subvolume_name = line.strip().decode('UTF-8') - continue - - if b':' in line: - key, value = line.strip().decode('UTF-8').split(':', 1) - - # A bit of a hack, until I figure out how @dataclass - # allows for hooking in a pre-processor to do this we have to do it here: - result[key.lower().replace(' ', '_').replace('(s)', 's')] = value.strip() - - return BtrfsSubvolumeInfo(**{'full_path' : path, 'name' : subvolume_name, **result}) # type: ignore - except SysCallError as error: - log(f"Could not retrieve subvolume information from {path}: {error}", level=logging.WARNING, fg="orange") - - return None - - -def find_parent_subvolume(path: Path, filters=[]) -> Optional[BtrfsSubvolumeInfo]: - # A root path cannot have a parent - if str(path) == '/': - return None - - if found_mount := get_mount_info(str(path.parent), traverse=True, ignore=filters): - if not (subvolume := subvolume_info_from_path(found_mount['target'])): - if found_mount['target'] == '/': - return None - - return find_parent_subvolume(path.parent, filters=[*filters, found_mount['target']]) - - return subvolume - - return None diff --git a/archinstall/lib/disk/btrfs/btrfspartition.py b/archinstall/lib/disk/btrfs/btrfspartition.py deleted file mode 100644 index d04c9b98..00000000 --- a/archinstall/lib/disk/btrfs/btrfspartition.py +++ /dev/null @@ -1,109 +0,0 @@ -import glob -import pathlib -import logging -from typing import Optional, TYPE_CHECKING - -from ...exceptions import DiskError -from ...storage import storage -from ...output import log -from ...general import SysCommand -from ..partition import Partition -from ..helpers import findmnt -from .btrfs_helpers import ( - subvolume_info_from_path -) - -if TYPE_CHECKING: - from ...installer import Installer - from .btrfssubvolumeinfo import BtrfsSubvolumeInfo - - -class BTRFSPartition(Partition): - def __init__(self, *args, **kwargs): - Partition.__init__(self, *args, **kwargs) - - @property - def subvolumes(self): - for filesystem in findmnt(pathlib.Path(self.path), recurse=True).get('filesystems', []): - if '[' in filesystem.get('source', ''): - yield subvolume_info_from_path(filesystem['target']) - - def iterate_children(struct): - for c in struct.get('children', []): - if '[' in child.get('source', ''): - yield subvolume_info_from_path(c['target']) - - for sub_child in iterate_children(c): - yield sub_child - - for child in iterate_children(filesystem): - yield child - - def create_subvolume(self, subvolume :pathlib.Path, installation :Optional['Installer'] = None) -> 'BtrfsSubvolumeInfo': - """ - Subvolumes have to be created within a mountpoint. - This means we need to get the current installation target. - After we get it, we need to verify it is a btrfs subvolume filesystem. - Finally, the destination must be empty. - """ - - # Allow users to override the installation session - if not installation: - installation = storage.get('installation_session') - - # Determain if the path given, is an absolute path or a relative path. - # We do this by checking if the path contains a known mountpoint. - if str(subvolume)[0] == '/': - if filesystems := findmnt(subvolume, traverse=True).get('filesystems'): - if (target := filesystems[0].get('target')) and target != '/' and str(subvolume).startswith(target): - # Path starts with a known mountpoint which isn't / - # Which means it's an absolute path to a mounted location. - pass - else: - # Since it's not an absolute position with a known start. - # We omit the anchor ('/' basically) and make sure it's appendable - # to the installation.target later - subvolume = subvolume.relative_to(subvolume.anchor) - # else: We don't need to do anything about relative paths, they should be appendable to installation.target as-is. - - # If the subvolume is not absolute, then we do two checks: - # 1. Check if the partition itself is mounted somewhere, and use that as a root - # 2. Use an active Installer().target as the root, assuming it's filesystem is btrfs - # If both above fail, we need to warn the user that such setup is not supported. - if str(subvolume)[0] != '/': - if self.mountpoint is None and installation is None: - raise DiskError("When creating a subvolume on BTRFSPartition()'s, you need to either initiate a archinstall.Installer() or give absolute paths when creating the subvoulme.") - elif self.mountpoint: - subvolume = self.mountpoint / subvolume - elif installation: - ongoing_installation_destination = installation.target - if type(ongoing_installation_destination) == str: - ongoing_installation_destination = pathlib.Path(ongoing_installation_destination) - - subvolume = ongoing_installation_destination / subvolume - - subvolume.parent.mkdir(parents=True, exist_ok=True) - - # - - log(f'Attempting to create subvolume at {subvolume}', level=logging.DEBUG, fg="grey") - - if glob.glob(str(subvolume / '*')): - raise DiskError(f"Cannot create subvolume at {subvolume} because it contains data (non-empty folder target is not supported by BTRFS)") - # Ideally we would like to check if the destination is already a subvolume. - # But then we would need the mount-point at this stage as well. - # So we'll comment out this check: - # elif subvolinfo := subvolume_info_from_path(subvolume): - # raise DiskError(f"Destination {subvolume} is already a subvolume: {subvolinfo}") - - # And deal with it here: - SysCommand(f"btrfs subvolume create {subvolume}") - - return subvolume_info_from_path(subvolume) diff --git a/archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py b/archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py deleted file mode 100644 index 5f5bdea6..00000000 --- a/archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py +++ /dev/null @@ -1,192 +0,0 @@ -import pathlib -import datetime -import logging -import string -import random -import shutil -from dataclasses import dataclass -from typing import Optional, List# , TYPE_CHECKING -from functools import cached_property - -# if TYPE_CHECKING: -# from ..blockdevice import BlockDevice - -from ...exceptions import DiskError -from ...general import SysCommand -from ...output import log -from ...storage import storage - - -@dataclass -class BtrfsSubvolumeInfo: - full_path :pathlib.Path - name :str - uuid :str - parent_uuid :str - creation_time :datetime.datetime - subvolume_id :int - generation :int - gen_at_creation :int - parent_id :int - top_level_id :int - send_transid :int - send_time :datetime.datetime - receive_transid :int - received_uuid :Optional[str] = None - flags :Optional[str] = None - receive_time :Optional[datetime.datetime] = None - snapshots :Optional[List] = None - - def __post_init__(self): - self.full_path = pathlib.Path(self.full_path) - - # Convert "-" entries to `None` - if self.parent_uuid == "-": - self.parent_uuid = None - if self.received_uuid == "-": - self.received_uuid = None - if self.flags == "-": - self.flags = None - if self.receive_time == "-": - self.receive_time = None - if self.snapshots == "": - self.snapshots = [] - - # Convert timestamps into datetime workable objects (and preserve timezone by using ISO formats) - self.creation_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.creation_time)) - self.send_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.send_time)) - if self.receive_time: - self.receive_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.receive_time)) - - @property - def parent_subvolume(self): - from .btrfs_helpers import find_parent_subvolume - - return find_parent_subvolume(self.full_path) - - @property - def root(self) -> bool: - from .btrfs_helpers import subvolume_info_from_path - - # TODO: Make this function traverse storage['MOUNT_POINT'] and find the first - # occurrence of a mountpoint that is a btrfs volume instead of lazy assume / is a subvolume. - # It would also be nice if it could use findmnt(self.full_path) and traverse backwards - # finding the last occurrence of a subvolume which 'self' belongs to. - if volume := subvolume_info_from_path(storage['MOUNT_POINT']): - return self.full_path == volume.full_path - - return False - - @cached_property - def partition(self): - from ..helpers import findmnt, get_parent_of_partition, all_blockdevices - from ..partition import Partition - from ..blockdevice import BlockDevice - from ..mapperdev import MapperDev - from .btrfspartition import BTRFSPartition - from .btrfs_helpers import subvolume_info_from_path - - try: - # If the subvolume is mounted, it's pretty trivial to lookup the partition (parent) device. - if filesystem := findmnt(self.full_path).get('filesystems', []): - if source := filesystem[0].get('source', None): - # Strip away subvolume definitions from findmnt - if '[' in source: - source = source[:source.find('[')] - - if filesystem[0].get('fstype', '') == 'btrfs': - return BTRFSPartition(source, BlockDevice(get_parent_of_partition(pathlib.Path(source)))) - elif filesystem[0].get('source', '').startswith('/dev/mapper'): - return MapperDev(source) - else: - return Partition(source, BlockDevice(get_parent_of_partition(pathlib.Path(source)))) - except DiskError: - # Subvolume has never been mounted, we have no reliable way of finding where it is. - # But we have the UUID of the partition, and can begin looking for it by mounting - # all blockdevices that we can reliably support.. This is taxing tho and won't cover all devices. - - log(f"Looking up {self}, this might take time.", fg="orange", level=logging.WARNING) - for blockdevice, instance in all_blockdevices(mappers=True, partitions=True, error=True).items(): - if type(instance) in (Partition, MapperDev): - we_mounted_it = False - detection_mountpoint = instance.mountpoint - if not detection_mountpoint: - if type(instance) == Partition and instance.encrypted: - # TODO: Perhaps support unlocking encrypted volumes? - # This will cause a lot of potential user interactions tho. - log(f"Ignoring {blockdevice} because it's encrypted.", fg="gray", level=logging.DEBUG) - continue - - detection_mountpoint = pathlib.Path(f"/tmp/{''.join([random.choice(string.ascii_letters) for x in range(20)])}") - detection_mountpoint.mkdir(parents=True, exist_ok=True) - - instance.mount(str(detection_mountpoint)) - we_mounted_it = True - - if (filesystem := findmnt(detection_mountpoint)) and (filesystem := filesystem.get('filesystems', [])): - if subvolume := subvolume_info_from_path(filesystem[0]['target']): - if subvolume.uuid == self.uuid: - # The top level subvolume matched of ourselves, - # which means the instance we're iterating has the subvol we're looking for. - log(f"Found the subvolume on device {instance}", level=logging.DEBUG, fg="gray") - return instance - - def iterate_children(struct): - for child in struct.get('children', []): - if '[' in child.get('source', ''): - yield subvolume_info_from_path(child['target']) - - for sub_child in iterate_children(child): - yield sub_child - - for child in iterate_children(filesystem[0]): - if child.uuid == self.uuid: - # We found a child within the instance that has the subvol we're looking for. - log(f"Found the subvolume on device {instance}", level=logging.DEBUG, fg="gray") - return instance - - if we_mounted_it: - instance.unmount() - shutil.rmtree(detection_mountpoint) - - @cached_property - def mount_options(self) -> Optional[List[str]]: - from ..helpers import findmnt - - if filesystem := findmnt(self.full_path).get('filesystems', []): - return filesystem[0].get('options').split(',') - - def convert_to_ISO_format(self, time_string): - time_string_almost_done = time_string.replace(' ', 'T', 1).replace(' ', '') - iso_string = f"{time_string_almost_done[:-2]}:{time_string_almost_done[-2:]}" - return iso_string - - def mount(self, mountpoint :pathlib.Path, options=None, include_previously_known_options=True): - from ..helpers import findmnt - - try: - if mnt_info := findmnt(pathlib.Path(mountpoint), traverse=False): - log(f"Unmounting {mountpoint} as it was already mounted using {mnt_info}") - SysCommand(f"umount {mountpoint}") - except DiskError: - # No previously mounted device at the mountpoint - pass - - if not options: - options = [] - - try: - if include_previously_known_options and (cached_options := self.mount_options): - options += cached_options - except DiskError: - pass - - if not any('subvol=' in x for x in options): - options += f'subvol={self.name}' - - SysCommand(f"mount {self.partition.path} {mountpoint} -o {','.join(options)}") - log(f"{self} has successfully been mounted to {mountpoint}", level=logging.INFO, fg="gray") - - def unmount(self, recurse :bool = True): - SysCommand(f"umount {'-R' if recurse else ''} {self.full_path}") - log(f"Successfully unmounted {self}", level=logging.INFO, fg="gray") diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py new file mode 100644 index 00000000..12cf18ea --- /dev/null +++ b/archinstall/lib/disk/device_handler.py @@ -0,0 +1,599 @@ +from __future__ import annotations + +import json +import logging +import os +import time +from pathlib import Path +from typing import List, Dict, Any, Optional, TYPE_CHECKING + +from parted import ( # type: ignore + Disk, Geometry, FileSystem, + PartitionException, DiskLabelException, + getAllDevices, freshDisk, Partition, +) + +from .device_model import ( + DeviceModification, PartitionModification, + BDevice, _DeviceInfo, _PartitionInfo, + FilesystemType, Unit, PartitionTable, + ModificationStatus, get_lsblk_info, LsblkInfo, + _BtrfsSubvolumeInfo, get_all_lsblk_info, DiskEncryption +) + +from ..exceptions import DiskError, UnknownFilesystemFormat +from ..general import SysCommand, SysCallError, JSON +from ..luks import Luks2 +from ..output import log +from ..utils.util import is_subpath + +if TYPE_CHECKING: + _: Any + + +class DeviceHandler(object): + _TMP_BTRFS_MOUNT = Path('/mnt/arch_btrfs') + + def __init__(self): + self._devices: Dict[Path, BDevice] = {} + self.load_devices() + + @property + def devices(self) -> List[BDevice]: + return list(self._devices.values()) + + def load_devices(self): + block_devices = {} + + for device in getAllDevices(): + try: + disk = Disk(device) + except DiskLabelException as error: + if 'unrecognised disk label' in getattr(error, 'message', str(error)): + disk = freshDisk(device, PartitionTable.GPT.value) + else: + log(f'Unable to get disk from device: {device}', level=logging.DEBUG) + continue + + device_info = _DeviceInfo.from_disk(disk) + partition_infos = [] + + for partition in disk.partitions: + lsblk_info = get_lsblk_info(partition.path) + fs_type = self._determine_fs_type(partition, lsblk_info) + subvol_infos = [] + + if fs_type == FilesystemType.Btrfs: + subvol_infos = self.get_btrfs_info(partition.path) + + partition_infos.append( + _PartitionInfo.from_partition( + partition, + fs_type, + lsblk_info.partuuid, + lsblk_info.mountpoints, + subvol_infos + ) + ) + + block_device = BDevice(disk, device_info, partition_infos) + block_devices[block_device.device_info.path] = block_device + + self._devices = block_devices + + def _determine_fs_type( + self, + partition: Partition, + lsblk_info: Optional[LsblkInfo] = None + ) -> Optional[FilesystemType]: + try: + if partition.fileSystem: + return FilesystemType(partition.fileSystem.type) + elif lsblk_info is not None: + 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) + + return None + + def get_device(self, path: Path) -> Optional[BDevice]: + return self._devices.get(path, None) + + def get_device_by_partition_path(self, partition_path: Path) -> Optional[BDevice]: + partition = self.find_partition(partition_path) + if partition: + return partition.disk.device + return None + + def find_partition(self, path: Path) -> Optional[_PartitionInfo]: + for device in self._devices.values(): + part = next(filter(lambda x: str(x.path) == str(path), device.partition_infos), None) + if part is not None: + return part + return None + + def get_uuid_for_path(self, path: Path) -> Optional[str]: + partition = self.find_partition(path) + return partition.partuuid if partition else None + + def get_btrfs_info(self, dev_path: Path) -> List[_BtrfsSubvolumeInfo]: + lsblk_info = get_lsblk_info(dev_path) + subvol_infos: List[_BtrfsSubvolumeInfo] = [] + + if not lsblk_info.mountpoint: + self.mount(dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + mountpoint = self._TMP_BTRFS_MOUNT + else: + # when multiple subvolumes are mounted then the lsblk output may look like + # "mountpoint": "/mnt/archinstall/.snapshots" + # "mountpoints": ["/mnt/archinstall/.snapshots", "/mnt/archinstall/home", ..] + # so we'll determine the minimum common path and assume that's the root + path_strings = [str(m) for m in lsblk_info.mountpoints] + common_prefix = os.path.commonprefix(path_strings) + mountpoint = Path(common_prefix) + + try: + result = SysCommand(f'btrfs subvolume list {mountpoint}') + except SysCallError as err: + log(f'Failed to read btrfs subvolume information: {err}', level=logging.DEBUG) + return subvol_infos + + if result.exit_code == 0: + try: + if decoded := result.decode('utf-8'): + # ID 256 gen 16 top level 5 path @ + for line in decoded.splitlines(): + # expected output format: + # ID 257 gen 8 top level 5 path @home + name = Path(line.split(' ')[-1]) + 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) + raise err + + if not lsblk_info.mountpoint: + self.umount(dev_path) + + return subvol_infos + + def _perform_formatting( + self, + fs_type: FilesystemType, + path: Path, + additional_parted_options: List[str] = [] + ): + options = [] + command = '' + + match fs_type: + case FilesystemType.Btrfs: + options += ['-f'] + command += 'mkfs.btrfs' + case FilesystemType.Fat16: + options += ['-F16'] + command += 'mkfs.fat' + case FilesystemType.Fat32: + options += ['-F32'] + command += 'mkfs.fat' + case FilesystemType.Ext2: + options += ['-F'] + command += 'mkfs.ext2' + case FilesystemType.Ext3: + options += ['-F'] + command += 'mkfs.ext3' + case FilesystemType.Ext4: + options += ['-F'] + command += 'mkfs.ext4' + case FilesystemType.Xfs: + options += ['-f'] + command += 'mkfs.xfs' + case FilesystemType.F2fs: + options += ['-f'] + command += 'mkfs.f2fs' + case FilesystemType.Ntfs: + options += ['-f', '-Q'] + command += 'mkfs.ntfs' + case FilesystemType.Reiserfs: + command += 'mkfs.reiserfs' + case _: + raise UnknownFilesystemFormat(f'Filetype "{fs_type.value}" is not supported') + + options += additional_parted_options + options_str = ' '.join(options) + + log(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') + + try: + if (handle := SysCommand(f"/usr/bin/{command} {options_str} {path}")).exit_code != 0: + mkfs_error = handle.decode() + raise DiskError(f'Could not format {path} with {fs_type.value}: {mkfs_error}') + 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 + + def _perform_enc_formatting( + self, + dev_path: Path, + mapper_name: Optional[str], + fs_type: FilesystemType, + enc_conf: DiskEncryption + ): + luks_handler = Luks2( + dev_path, + mapper_name=mapper_name, + password=enc_conf.encryption_password + ) + + key_file = luks_handler.encrypt() + + log(f'Unlocking luks2 device: {dev_path}', level=logging.DEBUG) + 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) + self._perform_formatting(fs_type, luks_handler.mapper_dev) + + log(f'luks2 locking device: {dev_path}', level=logging.INFO) + luks_handler.lock() + + def format( + self, + modification: DeviceModification, + enc_conf: Optional['DiskEncryption'] = None + ): + """ + Format can be given an overriding path, for instance /dev/null to test + the formatting functionality and in essence the support for the given filesystem. + """ + + # verify that all partitions have a path set (which implies that they have been created) + missing_path = next(filter(lambda x: x.dev_path is None, modification.partitions), None) + if missing_path is not None: + raise ValueError('When formatting, all partitions must have a path set') + + # crypto luks is not known to parted and can therefore not + # be used as a filesystem type in that sense; + invalid_fs_type = next(filter(lambda x: x.fs_type is FilesystemType.Crypto_luks, modification.partitions), None) + if invalid_fs_type is not None: + raise ValueError('Crypto luks cannot be set as a filesystem type') + + # make sure all devices are unmounted + self._umount_all_existing(modification) + + for part_mod in modification.partitions: + # partition will be encrypted + if enc_conf is not None and part_mod in enc_conf.partitions: + self._perform_enc_formatting( + part_mod.real_dev_path, + part_mod.mapper_name, + part_mod.fs_type, + enc_conf + ) + else: + self._perform_formatting(part_mod.fs_type, part_mod.real_dev_path) + + def _perform_partitioning( + self, + part_mod: PartitionModification, + block_device: BDevice, + disk: Disk, + requires_delete: bool + ): + # when we require a delete and the partition to be (re)created + # already exists then we have to delete it first + if requires_delete and part_mod.status in [ModificationStatus.Modify, ModificationStatus.Delete]: + log(f'Delete existing partition: {part_mod.real_dev_path}', level=logging.INFO) + part_info = self.find_partition(part_mod.real_dev_path) + + if not part_info: + raise DiskError(f'No partition for dev path found: {part_mod.real_dev_path}') + + disk.deletePartition(part_info.partition) + disk.commit() + + if part_mod.status == ModificationStatus.Delete: + return + + start_sector = part_mod.start.convert( + Unit.sectors, + block_device.device_info.sector_size + ) + + length_sector = part_mod.length.convert( + Unit.sectors, + block_device.device_info.sector_size + ) + + geometry = Geometry( + device=block_device.disk.device, + start=start_sector.value, + length=length_sector.value + ) + + filesystem = FileSystem(type=part_mod.fs_type.value, geometry=geometry) + + partition = Partition( + disk=disk, + type=part_mod.type.get_partition_code(), + fs=filesystem, + geometry=geometry + ) + + 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) + + try: + disk.addPartition(partition=partition, constraint=disk.device.optimalAlignedConstraint) + disk.commit() + + # the creation will take a bit of time + time.sleep(3) + + # the partition has a real path now as it was created + part_mod.dev_path = Path(partition.path) + + info = get_lsblk_info(part_mod.dev_path) + + if not info.partuuid: + raise DiskError(f'Unable to determine new partition uuid: {part_mod.dev_path}') + + part_mod.partuuid = info.partuuid + part_mod.uuid = info.uuid + except PartitionException as ex: + raise DiskError(f'Unable to add partition, most likely due to overlapping sectors: {ex}') from ex + + def create_btrfs_volumes( + self, + part_mod: PartitionModification, + enc_conf: Optional['DiskEncryption'] = None + ): + log(f'Creating subvolumes: {part_mod.real_dev_path}', level=logging.INFO) + + luks_handler = None + + # unlock the partition first if it's encrypted + if enc_conf is not None and part_mod in enc_conf.partitions: + if not part_mod.mapper_name: + raise ValueError('No device path specified for modification') + + luks_handler = self.unlock_luks2_dev( + part_mod.real_dev_path, + part_mod.mapper_name, + enc_conf.encryption_password + ) + + if not luks_handler.mapper_dev: + raise DiskError('Failed to unlock luks device') + + self.mount(luks_handler.mapper_dev, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + else: + self.mount(part_mod.real_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) + + if luks_handler is not None: + subvol_path = self._TMP_BTRFS_MOUNT / sub_vol.name + else: + subvol_path = self._TMP_BTRFS_MOUNT / sub_vol.name + + SysCommand(f"btrfs subvolume create {subvol_path}") + + if sub_vol.nodatacow: + if (result := SysCommand(f'chattr +C {subvol_path}')).exit_code != 0: + raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {result.decode()}') + + if sub_vol.compress: + if (result := SysCommand(f'chattr +c {subvol_path}')).exit_code != 0: + raise DiskError(f'Could not set compress attribute at {subvol_path}: {result}') + + if luks_handler is not None and luks_handler.mapper_dev is not None: + self.umount(luks_handler.mapper_dev) + luks_handler.lock() + else: + self.umount(part_mod.real_dev_path) + + def unlock_luks2_dev(self, dev_path: Path, mapper_name: str, enc_password: str) -> Luks2: + luks_handler = Luks2(dev_path, mapper_name=mapper_name, password=enc_password) + + if not luks_handler.is_unlocked(): + luks_handler.unlock() + + if not luks_handler.is_unlocked(): + raise DiskError(f'Failed to unlock luks2 device: {dev_path}') + + return luks_handler + + def _umount_all_existing(self, modification: DeviceModification): + log(f'Unmounting all partitions: {modification.device_path}', level=logging.INFO) + + existing_partitions = self._devices[modification.device_path].partition_infos + + for partition in existing_partitions: + log(f'Unmounting: {partition.path}', level=logging.DEBUG) + + # un-mount for existing encrypted partitions + if partition.fs_type == FilesystemType.Crypto_luks: + Luks2(partition.path).lock() + else: + self.umount(partition.path, recursive=True) + + def partition( + self, + modification: DeviceModification, + partition_table: Optional[PartitionTable] = None + ): + """ + Create a partition table on the block device and create all partitions. + """ + if modification.wipe: + if partition_table is None: + raise ValueError('Modification is marked as wipe but no partitioning table was provided') + + if partition_table.MBR and len(modification.partitions) > 3: + raise DiskError('Too many partitions on disk, MBR disks can only have 3 primary partitions') + + # make sure all devices are unmounted + self._umount_all_existing(modification) + + # WARNING: the entire device will be wiped and all data lost + if modification.wipe: + self.wipe_dev(modification.device) + 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}') + disk = modification.device.disk + + log(f'Creating partitions: {modification.device_path}') + + # TODO sort by delete first + + for part_mod in modification.partitions: + # don't touch existing partitions + if part_mod.exists(): + continue + + # if the entire disk got nuked then we don't have to delete + # any existing partitions anymore because they're all gone already + requires_delete = modification.wipe is False + self._perform_partitioning(part_mod, modification.device, disk, requires_delete=requires_delete) + + self.partprobe(modification.device.device_info.path) + + def mount( + self, + dev_path: Path, + target_mountpoint: Path, + mount_fs: Optional[str] = None, + create_target_mountpoint: bool = True, + options: List[str] = [] + ): + if create_target_mountpoint and not target_mountpoint.exists(): + target_mountpoint.mkdir(parents=True, exist_ok=True) + + if not target_mountpoint.exists(): + raise ValueError('Target mountpoint does not exist') + + lsblk_info = get_lsblk_info(dev_path) + if target_mountpoint in lsblk_info.mountpoints: + log(f'Device already mounted at {target_mountpoint}') + return + + str_options = ','.join(options) + str_options = f'-o {str_options}' if str_options else '' + + mount_fs = f'-t {mount_fs}' if mount_fs else '' + + command = f'mount {mount_fs} {str_options} {dev_path} {target_mountpoint}' + + log(f'Mounting {dev_path}: command', level=logging.DEBUG) + + try: + result = SysCommand(command) + if result.exit_code != 0: + raise DiskError(f'Could not mount {dev_path}: {command}\n{result.decode()}') + except SysCallError as err: + raise DiskError(f'Could not mount {dev_path}: {command}\n{err.message}') + + def umount(self, mountpoint: Path, recursive: bool = False): + try: + lsblk_info = get_lsblk_info(mountpoint) + except SysCallError as ex: + # this could happen if before partitioning the device contained 3 partitions + # and after partitioning only 2 partitions were created, then the modifications object + # will have a reference to /dev/sX3 which is being tried to umount here now + if 'not a block device' in ex.message: + return + 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) + + for mountpoint in lsblk_info.mountpoints: + log(f'Unmounting mountpoint: {mountpoint}', level=logging.DEBUG) + + command = 'umount' + + if recursive: + command += ' -R' + + SysCommand(f'{command} {mountpoint}') + + def detect_pre_mounted_mods(self, base_mountpoint: Path) -> List[DeviceModification]: + part_mods: Dict[Path, List[PartitionModification]] = {} + + for device in self.devices: + for part_info in device.partition_infos: + for mountpoint in part_info.mountpoints: + if is_subpath(mountpoint, base_mountpoint): + path = Path(part_info.disk.device.path) + part_mods.setdefault(path, []) + part_mods[path].append(PartitionModification.from_existing_partition(part_info)) + break + + device_mods: List[DeviceModification] = [] + for device_path, mods in part_mods.items(): + device_mod = DeviceModification(self._devices[device_path], False, mods) + device_mods.append(device_mod) + + return device_mods + + def partprobe(self, path: Optional[Path] = None): + if path is not None: + command = f'partprobe {path}' + else: + command = 'partprobe' + + try: + result = SysCommand(command) + if result.exit_code != 0: + log(f'Error calling partprobe: {result.decode()}', level=logging.DEBUG) + raise DiskError(f'Could not perform partprobe on {path}: {result.decode()}') + except SysCallError as error: + log(f"partprobe experienced an error with {path}: {error}", level=logging.DEBUG) + + def _wipe(self, dev_path: Path): + """ + Wipe a device (partition or otherwise) of meta-data, be it file system, LVM, etc. + @param dev_path: Device path of the partition to be wiped. + @type dev_path: str + """ + with open(dev_path, 'wb') as p: + p.write(bytearray(1024)) + + def wipe_dev(self, block_device: BDevice): + """ + Wipe the block device of meta-data, be it file system, LVM, etc. + 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}') + for partition in block_device.partition_infos: + self._wipe(partition.path) + + self._wipe(block_device.device_info.path) + + +device_handler = DeviceHandler() + + +def disk_layouts() -> str: + try: + 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") + return '' + except json.decoder.JSONDecodeError as err: + log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") + return '' diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py new file mode 100644 index 00000000..0270a4dd --- /dev/null +++ b/archinstall/lib/disk/device_model.py @@ -0,0 +1,1033 @@ +from __future__ import annotations + +import dataclasses +import json +import logging +import math +import time +import uuid +from dataclasses import dataclass, field +from enum import Enum +from enum import auto +from pathlib import Path +from typing import Optional, List, Dict, TYPE_CHECKING, Any +from typing import Union + +import parted # type: ignore +from parted import Disk, Geometry, Partition + +from ..exceptions import DiskError, SysCallError +from ..general import SysCommand +from ..output import log +from ..storage import storage + +if TYPE_CHECKING: + _: Any + + +class DiskLayoutType(Enum): + Default = 'default_layout' + Manual = 'manual_partitioning' + Pre_mount = 'pre_mounted_config' + + def display_msg(self) -> str: + match self: + case DiskLayoutType.Default: return str(_('Use a best-effort default partition layout')) + case DiskLayoutType.Manual: return str(_('Manual Partitioning')) + case DiskLayoutType.Pre_mount: return str(_('Pre-mounted configuration')) + + +@dataclass +class DiskLayoutConfiguration: + config_type: DiskLayoutType + device_modifications: List[DeviceModification] = field(default_factory=list) + # used for pre-mounted config + relative_mountpoint: Optional[Path] = None + + def __post_init__(self): + if self.config_type == DiskLayoutType.Pre_mount and self.relative_mountpoint is None: + raise ValueError('Must set a relative mountpoint when layout type is pre-mount"') + + def __dump__(self) -> Dict[str, Any]: + return { + 'config_type': self.config_type.value, + 'device_modifications': [mod.__dump__() for mod in self.device_modifications] + } + + @classmethod + def parse_arg(cls, disk_config: Dict[str, List[Dict[str, Any]]]) -> Optional[DiskLayoutConfiguration]: + from .device_handler import device_handler + + device_modifications: List[DeviceModification] = [] + config_type = disk_config.get('config_type', None) + + if not config_type: + raise ValueError('Missing disk layout configuration: config_type') + + config = DiskLayoutConfiguration( + config_type=DiskLayoutType(config_type), + device_modifications=device_modifications + ) + + for entry in disk_config.get('device_modifications', []): + device_path = Path(entry.get('device', None)) if entry.get('device', None) else None + + if not device_path: + continue + + device = device_handler.get_device(device_path) + + if not device: + continue + + device_modification = DeviceModification( + wipe=entry.get('wipe', False), + device=device + ) + + device_partitions: List[PartitionModification] = [] + + for partition in entry.get('partitions', []): + device_partition = PartitionModification( + status=ModificationStatus(partition['status']), + fs_type=FilesystemType(partition['fs_type']), + start=Size.parse_args(partition['start']), + length=Size.parse_args(partition['length']), + mount_options=partition['mount_options'], + mountpoint=Path(partition['mountpoint']) if partition['mountpoint'] else None, + type=PartitionType(partition['type']), + flags=[PartitionFlag[f] for f in partition.get('flags', [])], + btrfs_subvols=SubvolumeModification.parse_args(partition.get('btrfs', [])), + ) + # special 'invisible attr to internally identify the part mod + setattr(device_partition, '_obj_id', partition['obj_id']) + device_partitions.append(device_partition) + + device_modification.partitions = device_partitions + device_modifications.append(device_modification) + + return config + + +class PartitionTable(Enum): + GPT = 'gpt' + MBR = 'msdos' + + +class Unit(Enum): + B = 1 # byte + kB = 1000**1 # kilobyte + MB = 1000**2 # megabyte + GB = 1000**3 # gigabyte + TB = 1000**4 # terabyte + PB = 1000**5 # petabyte + EB = 1000**6 # exabyte + ZB = 1000**7 # zettabyte + YB = 1000**8 # yottabyte + + KiB = 1024**1 # kibibyte + MiB = 1024**2 # mebibyte + GiB = 1024**3 # gibibyte + TiB = 1024**4 # tebibyte + PiB = 1024**5 # pebibyte + EiB = 1024**6 # exbibyte + ZiB = 1024**7 # zebibyte + YiB = 1024**8 # yobibyte + + sectors = 'sectors' # size in sector + + Percent = '%' # size in percentile + + +@dataclass +class Size: + value: int + unit: Unit + sector_size: Optional[Size] = None # only required when unit is sector + total_size: Optional[Size] = None # required when operating on percentages + + def __post_init__(self): + if self.unit == Unit.sectors and self.sector_size is None: + raise ValueError('Sector size is required when unit is sectors') + elif self.unit == Unit.Percent: + if self.value < 0 or self.value > 100: + raise ValueError('Percentage must be between 0 and 100') + elif self.total_size is None: + raise ValueError('Total size is required when unit is percentage') + + @property + def _total_size(self) -> Size: + """ + Save method to get the total size, mainly to satisfy mypy + This shouldn't happen as the Size object fails instantiation on missing total size + """ + if self.unit == Unit.Percent and self.total_size is None: + raise ValueError('Percent unit size must specify a total size') + return self.total_size # type: ignore + + def __dump__(self) -> Dict[str, Any]: + return { + 'value': self.value, + 'unit': self.unit.name, + 'sector_size': self.sector_size.__dump__() if self.sector_size else None, + 'total_size': self._total_size.__dump__() if self._total_size else None + } + + @classmethod + def parse_args(cls, size_arg: Dict[str, Any]) -> Size: + sector_size = size_arg['sector_size'] + total_size = size_arg['total_size'] + + return Size( + size_arg['value'], + Unit[size_arg['unit']], + Size.parse_args(sector_size) if sector_size else None, + Size.parse_args(total_size) if total_size else None + ) + + def convert( + self, + target_unit: Unit, + sector_size: Optional[Size] = None, + total_size: Optional[Size] = None + ) -> Size: + if target_unit == Unit.sectors and sector_size is None: + raise ValueError('If target has unit sector, a sector size must be provided') + + # not sure why we would ever wanna convert to percentages + if target_unit == Unit.Percent and total_size is None: + raise ValueError('Missing paramter total size to be able to convert to percentage') + + if self.unit == target_unit: + return self + elif self.unit == Unit.Percent: + amount = int(self._total_size._normalize() * (self.value / 100)) + return Size(amount, Unit.B) + elif self.unit == Unit.sectors: + norm = self._normalize() + return Size(norm, Unit.B).convert(target_unit, sector_size) + else: + if target_unit == Unit.sectors and sector_size is not None: + norm = self._normalize() + sectors = math.ceil(norm / sector_size.value) + return Size(sectors, Unit.sectors, sector_size) + else: + value = int(self._normalize() / target_unit.value) # type: ignore + return Size(value, target_unit) + + def format_size( + self, + target_unit: Unit, + sector_size: Optional[Size] = None + ) -> str: + if self.unit == Unit.Percent: + return f'{self.value}%' + else: + target_size = self.convert(target_unit, sector_size) + return f'{target_size.value} {target_unit.name}' + + def _normalize(self) -> int: + """ + will normalize the value of the unit to Byte + """ + if self.unit == Unit.Percent: + return self.convert(Unit.B).value + elif self.unit == Unit.sectors and self.sector_size is not None: + return self.value * self.sector_size._normalize() + return int(self.value * self.unit.value) # type: ignore + + def __sub__(self, other: Size) -> Size: + src_norm = self._normalize() + dest_norm = other._normalize() + return Size(abs(src_norm - dest_norm), Unit.B) + + def __lt__(self, other): + return self._normalize() < other._normalize() + + def __le__(self, other): + return self._normalize() <= other._normalize() + + def __eq__(self, other): + return self._normalize() == other._normalize() + + def __ne__(self, other): + return self._normalize() != other._normalize() + + def __gt__(self, other): + return self._normalize() > other._normalize() + + def __ge__(self, other): + return self._normalize() >= other._normalize() + + +@dataclass +class _BtrfsSubvolumeInfo: + name: Path + mountpoint: Optional[Path] + + +@dataclass +class _PartitionInfo: + partition: Partition + name: str + type: PartitionType + fs_type: FilesystemType + path: Path + start: Size + length: Size + flags: List[PartitionFlag] + partuuid: str + disk: Disk + mountpoints: List[Path] + btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list) + + def as_json(self) -> Dict[str, Any]: + info = { + 'Name': self.name, + 'Type': self.type.value, + 'Filesystem': self.fs_type.value if self.fs_type else str(_('Unknown')), + 'Path': str(self.path), + 'Start': self.start.format_size(Unit.MiB), + 'Length': self.length.format_size(Unit.MiB), + 'Flags': ', '.join([f.name for f in self.flags]) + } + + if self.btrfs_subvol_infos: + info['Btrfs vol.'] = f'{len(self.btrfs_subvol_infos)} subvolumes' + + return info + + @classmethod + def from_partition( + cls, + partition: Partition, + fs_type: FilesystemType, + partuuid: str, + mountpoints: List[Path], + btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = [] + ) -> _PartitionInfo: + partition_type = PartitionType.get_type_from_code(partition.type) + flags = [f for f in PartitionFlag if partition.getFlag(f.value)] + + start = Size( + partition.geometry.start, + Unit.sectors, + Size(partition.disk.device.sectorSize, Unit.B) + ) + + length = Size(int(partition.getLength(unit='B')), Unit.B) + + return _PartitionInfo( + partition=partition, + name=partition.get_name(), + type=partition_type, + fs_type=fs_type, + path=partition.path, + start=start, + length=length, + flags=flags, + partuuid=partuuid, + disk=partition.disk, + mountpoints=mountpoints, + btrfs_subvol_infos=btrfs_subvol_infos + ) + + +@dataclass +class _DeviceInfo: + model: str + path: Path + type: str + total_size: Size + free_space_regions: List[DeviceGeometry] + sector_size: Size + read_only: bool + dirty: bool + + def as_json(self) -> Dict[str, Any]: + total_free_space = sum([region.get_length(unit=Unit.MiB) for region in self.free_space_regions]) + return { + 'Model': self.model, + 'Path': str(self.path), + 'Type': self.type, + 'Size': self.total_size.format_size(Unit.MiB), + 'Free space': int(total_free_space), + 'Sector size': self.sector_size.value, + 'Read only': self.read_only + } + + @classmethod + def from_disk(cls, disk: Disk) -> _DeviceInfo: + device = disk.device + device_type = parted.devices[device.type] + + sector_size = Size(device.sectorSize, Unit.B) + free_space = [DeviceGeometry(g, sector_size) for g in disk.getFreeSpaceRegions()] + + return _DeviceInfo( + model=device.model.strip(), + path=Path(device.path), + type=device_type, + sector_size=sector_size, + total_size=Size(int(device.getLength(unit='B')), Unit.B), + free_space_regions=free_space, + read_only=device.readOnly, + dirty=device.dirty + ) + + +@dataclass +class SubvolumeModification: + name: Path + mountpoint: Optional[Path] = None + compress: bool = False + nodatacow: bool = False + + @classmethod + def from_existing_subvol_info(cls, info: _BtrfsSubvolumeInfo) -> SubvolumeModification: + return SubvolumeModification(info.name, mountpoint=info.mountpoint) + + @classmethod + def parse_args(cls, subvol_args: List[Dict[str, Any]]) -> List[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) + continue + + mountpoint = Path(entry['mountpoint']) if entry['mountpoint'] else None + + mods.append( + SubvolumeModification( + entry['name'], + mountpoint, + entry.get('compress', False), + entry.get('nodatacow', False) + ) + ) + + return mods + + @property + def mount_options(self) -> List[str]: + options = [] + options += ['compress'] if self.compress else [] + options += ['nodatacow'] if self.nodatacow else [] + return options + + @property + def relative_mountpoint(self) -> Path: + """ + Will return the relative path based on the anchor + e.g. Path('/mnt/test') -> Path('mnt/test') + """ + if self.mountpoint is not None: + return self.mountpoint.relative_to(self.mountpoint.anchor) + + raise ValueError('Mountpoint is not specified') + + def is_root(self, relative_mountpoint: Optional[Path] = None) -> bool: + if self.mountpoint: + if relative_mountpoint is not None: + return self.mountpoint.relative_to(relative_mountpoint) == Path('.') + return self.mountpoint == Path('/') + return False + + def __dump__(self) -> Dict[str, Any]: + return { + 'name': str(self.name), + 'mountpoint': str(self.mountpoint), + 'compress': self.compress, + 'nodatacow': self.nodatacow + } + + def as_json(self) -> Dict[str, Any]: + return { + 'name': str(self.name), + 'mountpoint': str(self.mountpoint), + 'compress': self.compress, + 'nodatacow': self.nodatacow + } + + +class DeviceGeometry: + def __init__(self, geometry: Geometry, sector_size: Size): + self._geometry = geometry + self._sector_size = sector_size + + @property + def start(self) -> int: + return self._geometry.start + + @property + def end(self) -> int: + return self._geometry.end + + def get_length(self, unit: Unit = Unit.sectors) -> int: + return self._geometry.getLength(unit.name) + + def as_json(self) -> Dict[str, Any]: + return { + 'Sector size': self._sector_size.value, + 'Start sector': self._geometry.start, + 'End sector': self._geometry.end, + 'Length': self._geometry.getLength() + } + + +@dataclass +class BDevice: + disk: Disk + device_info: _DeviceInfo + partition_infos: List[_PartitionInfo] + + def __hash__(self): + return hash(self.disk.device.path) + + +class PartitionType(Enum): + Boot = 'boot' + Primary = 'primary' + + @classmethod + def get_type_from_code(cls, code: int) -> PartitionType: + if code == parted.PARTITION_NORMAL: + return PartitionType.Primary + + raise DiskError(f'Partition code not supported: {code}') + + def get_partition_code(self) -> Optional[int]: + if self == PartitionType.Primary: + return parted.PARTITION_NORMAL + elif self == PartitionType.Boot: + return parted.PARTITION_BOOT + return None + + +class PartitionFlag(Enum): + Boot = 1 + + +class FilesystemType(Enum): + Btrfs = 'btrfs' + Ext2 = 'ext2' + Ext3 = 'ext3' + Ext4 = 'ext4' + F2fs = 'f2fs' + Fat16 = 'fat16' + Fat32 = 'fat32' + Ntfs = 'ntfs' + Reiserfs = 'reiserfs' + Xfs = 'xfs' + + # this is not a FS known to parted, so be careful + # with the usage from this enum + Crypto_luks = 'crypto_LUKS' + + def is_crypto(self) -> bool: + return self == FilesystemType.Crypto_luks + + @property + def fs_type_mount(self) -> str: + match self: + case FilesystemType.Ntfs: return 'ntfs3' + case FilesystemType.Fat32: return 'vfat' + case _: return self.value # type: ignore + + @property + def installation_pkg(self) -> Optional[str]: + match self: + case FilesystemType.Btrfs: return 'btrfs-progs' + case FilesystemType.Xfs: return 'xfsprogs' + case FilesystemType.F2fs: return 'f2fs-tools' + case _: return None + + @property + def installation_module(self) -> Optional[str]: + match self: + case FilesystemType.Btrfs: return 'btrfs' + case _: return None + + @property + def installation_binary(self) -> Optional[str]: + match self: + case FilesystemType.Btrfs: return '/usr/bin/btrfs' + case _: return None + + @property + def installation_hooks(self) -> Optional[str]: + match self: + case FilesystemType.Btrfs: return 'btrfs' + case _: return None + + +class ModificationStatus(Enum): + Exist = 'existing' + Modify = 'modify' + Delete = 'delete' + Create = 'create' + + +@dataclass +class PartitionModification: + status: ModificationStatus + type: PartitionType + start: Size + length: Size + fs_type: FilesystemType + mountpoint: Optional[Path] = None + mount_options: List[str] = field(default_factory=list) + flags: List[PartitionFlag] = field(default_factory=list) + btrfs_subvols: List[SubvolumeModification] = field(default_factory=list) + + # only set if the device was created or exists + dev_path: Optional[Path] = None + partuuid: Optional[str] = None + uuid: Optional[str] = None + + def __post_init__(self): + # needed to use the object as a dictionary key due to hash func + if not hasattr(self, '_obj_id'): + self._obj_id = uuid.uuid4() + + if self.is_exists_or_modify() and not self.dev_path: + raise ValueError('If partition marked as existing a path must be set') + + def __hash__(self): + return hash(self._obj_id) + + @property + def obj_id(self) -> str: + if hasattr(self, '_obj_id'): + return str(self._obj_id) + return '' + + @property + def real_dev_path(self) -> Path: + if self.dev_path is None: + raise ValueError('Device path was not set') + return self.dev_path + + @classmethod + def from_existing_partition(cls, partition_info: _PartitionInfo) -> PartitionModification: + if partition_info.btrfs_subvol_infos: + mountpoint = None + subvol_mods = [] + for info in partition_info.btrfs_subvol_infos: + subvol_mods.append( + SubvolumeModification.from_existing_subvol_info(info) + ) + else: + mountpoint = partition_info.mountpoints[0] if partition_info.mountpoints else None + subvol_mods = [] + + return PartitionModification( + status=ModificationStatus.Exist, + type=partition_info.type, + start=partition_info.start, + length=partition_info.length, + fs_type=partition_info.fs_type, + dev_path=partition_info.path, + flags=partition_info.flags, + mountpoint=mountpoint, + btrfs_subvols=subvol_mods + ) + + @property + def relative_mountpoint(self) -> Path: + """ + Will return the relative path based on the anchor + e.g. Path('/mnt/test') -> Path('mnt/test') + """ + if self.mountpoint: + return self.mountpoint.relative_to(self.mountpoint.anchor) + + raise ValueError('Mountpoint is not specified') + + def is_boot(self) -> bool: + return PartitionFlag.Boot in self.flags + + def is_root(self, relative_mountpoint: Optional[Path] = None) -> bool: + if relative_mountpoint is not None and self.mountpoint is not None: + return self.mountpoint.relative_to(relative_mountpoint) == Path('.') + elif self.mountpoint is not None: + return Path('/') == self.mountpoint + else: + for subvol in self.btrfs_subvols: + if subvol.is_root(relative_mountpoint): + return True + + return False + + def is_modify(self) -> bool: + return self.status == ModificationStatus.Modify + + def exists(self) -> bool: + return self.status == ModificationStatus.Exist + + def is_exists_or_modify(self) -> bool: + return self.status in [ModificationStatus.Exist, ModificationStatus.Modify] + + @property + def mapper_name(self) -> Optional[str]: + if self.dev_path: + return f'{storage.get("ENC_IDENTIFIER", "ai")}{self.dev_path.name}' + return None + + def set_flag(self, flag: PartitionFlag): + if flag not in self.flags: + self.flags.append(flag) + + def invert_flag(self, flag: PartitionFlag): + if flag in self.flags: + self.flags = [f for f in self.flags if f != flag] + else: + self.set_flag(flag) + + def json(self) -> Dict[str, Any]: + """ + Called for configuration settings + """ + return { + 'obj_id': self.obj_id, + 'status': self.status.value, + 'type': self.type.value, + 'start': self.start.__dump__(), + 'length': self.length.__dump__(), + 'fs_type': self.fs_type.value, + 'mountpoint': str(self.mountpoint) if self.mountpoint else None, + 'mount_options': self.mount_options, + 'flags': [f.name for f in self.flags], + 'btrfs': [vol.__dump__() for vol in self.btrfs_subvols] + } + + def as_json(self) -> Dict[str, Any]: + """ + Called for displaying data in table format + """ + info = { + 'Status': self.status.value, + 'Device': str(self.dev_path) if self.dev_path else '', + 'Type': self.type.value, + 'Start': self.start.format_size(Unit.MiB), + 'Length': self.length.format_size(Unit.MiB), + 'FS type': self.fs_type.value, + 'Mountpoint': self.mountpoint if self.mountpoint else '', + 'Mount options': ', '.join(self.mount_options), + 'Flags': ', '.join([f.name for f in self.flags]), + } + + if self.btrfs_subvols: + info['Btrfs vol.'] = f'{len(self.btrfs_subvols)} subvolumes' + + return info + + +@dataclass +class DeviceModification: + device: BDevice + wipe: bool + partitions: List[PartitionModification] = field(default_factory=list) + + @property + def device_path(self) -> Path: + return self.device.device_info.path + + def add_partition(self, partition: PartitionModification): + self.partitions.append(partition) + + def get_boot_partition(self) -> Optional[PartitionModification]: + liltered = filter(lambda x: x.is_boot(), self.partitions) + return next(liltered, None) + + def get_root_partition(self, relative_path: Optional[Path]) -> Optional[PartitionModification]: + filtered = filter(lambda x: x.is_root(relative_path), self.partitions) + return next(filtered, None) + + def __dump__(self) -> Dict[str, Any]: + """ + Called when generating configuration files + """ + return { + 'device': str(self.device.device_info.path), + 'wipe': self.wipe, + 'partitions': [p.json() for p in self.partitions] + } + + +class EncryptionType(Enum): + NoEncryption = "no_encryption" + Partition = "partition" + + @classmethod + def _encryption_type_mapper(cls) -> Dict[str, 'EncryptionType']: + return { + # str(_('Full disk encryption')): EncryptionType.FullDiskEncryption, + str(_('Partition encryption')): EncryptionType.Partition + } + + @classmethod + def text_to_type(cls, text: str) -> 'EncryptionType': + mapping = cls._encryption_type_mapper() + return mapping[text] + + @classmethod + def type_to_text(cls, type_: 'EncryptionType') -> str: + mapping = cls._encryption_type_mapper() + type_to_text = {type_: text for text, type_ in mapping.items()} + return type_to_text[type_] + + +@dataclass +class DiskEncryption: + encryption_type: EncryptionType = EncryptionType.Partition + encryption_password: str = '' + partitions: List[PartitionModification] = field(default_factory=list) + hsm_device: Optional[Fido2Device] = None + + def should_generate_encryption_file(self, part_mod: PartitionModification) -> bool: + return part_mod in self.partitions and part_mod.mountpoint != Path('/') + + def json(self) -> Dict[str, Any]: + obj: Dict[str, Any] = { + 'encryption_type': self.encryption_type.value, + 'partitions': [p.obj_id for p in self.partitions] + } + + if self.hsm_device: + obj['hsm_device'] = self.hsm_device.json() + + return obj + + @classmethod + def parse_arg( + cls, + disk_config: DiskLayoutConfiguration, + arg: Dict[str, Any], + password: str = '' + ) -> 'DiskEncryption': + enc_partitions = [] + for mod in disk_config.device_modifications: + for part in mod.partitions: + if part.obj_id in arg.get('partitions', []): + enc_partitions.append(part) + + enc = DiskEncryption( + EncryptionType(arg['encryption_type']), + password, + enc_partitions + ) + + if hsm := arg.get('hsm_device', None): + enc.hsm_device = Fido2Device.parse_arg(hsm) + + return enc + + +@dataclass +class Fido2Device: + path: Path + manufacturer: str + product: str + + def json(self) -> Dict[str, str]: + return { + 'path': str(self.path), + 'manufacturer': self.manufacturer, + 'product': self.product + } + + @classmethod + def parse_arg(cls, arg: Dict[str, str]) -> 'Fido2Device': + return Fido2Device( + Path(arg['path']), + arg['manufacturer'], + arg['product'] + ) + + +@dataclass +class LsblkInfo: + name: str = '' + path: Path = Path() + pkname: str = '' + size: Size = Size(0, Unit.B) + log_sec: int = 0 + pttype: str = '' + ptuuid: str = '' + rota: bool = False + tran: Optional[str] = None + partuuid: Optional[str] = None + uuid: Optional[str] = None + fstype: Optional[str] = None + fsver: Optional[str] = None + fsavail: Optional[str] = None + fsuse_percentage: Optional[str] = None + type: Optional[str] = None + mountpoint: Optional[Path] = None + mountpoints: List[Path] = field(default_factory=list) + fsroots: List[Path] = field(default_factory=list) + children: List[LsblkInfo] = field(default_factory=list) + + def json(self) -> Dict[str, Any]: + return { + 'name': self.name, + 'path': str(self.path), + 'pkname': self.pkname, + 'size': self.size.format_size(Unit.MiB), + 'log_sec': self.log_sec, + 'pttype': self.pttype, + 'ptuuid': self.ptuuid, + 'rota': self.rota, + 'tran': self.tran, + 'partuuid': self.partuuid, + 'uuid': self.uuid, + 'fstype': self.fstype, + 'fsver': self.fsver, + 'fsavail': self.fsavail, + 'fsuse_percentage': self.fsuse_percentage, + 'type': self.type, + 'mountpoint': self.mountpoint, + 'mountpoints': [str(m) for m in self.mountpoints], + 'fsroots': [str(r) for r in self.fsroots], + 'children': [c.json() for c in self.children] + } + + @property + def btrfs_subvol_info(self) -> Dict[Path, Path]: + """ + It is assumed that lsblk will contain the fields as + + "mountpoints": ["/mnt/archinstall/log", "/mnt/archinstall/home", "/mnt/archinstall", ...] + "fsroots": ["/@log", "/@home", "/@"...] + + we'll thereby map the fsroot, which are the mounted filesystem roots + to the corresponding mountpoints + """ + return dict(zip(self.fsroots, self.mountpoints)) + + @classmethod + def exclude(cls) -> List[str]: + return ['children'] + + @classmethod + def fields(cls) -> List[str]: + return [f.name for f in dataclasses.fields(LsblkInfo) if f.name not in cls.exclude()] + + @classmethod + def from_json(cls, blockdevice: Dict[str, Any]) -> LsblkInfo: + 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): + val = Path(blockdevice[lsblk_field]) + elif isinstance(getattr(info, data_field), Size): + val = Size(blockdevice[lsblk_field], Unit.B) + else: + val = blockdevice[lsblk_field] + + setattr(info, data_field, val) + + 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] + + fs_roots = [] + for r in 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 + + return info + + +class CleanType(Enum): + Blockdevice = auto() + Dataclass = auto() + Lsblk = auto() + + +def _clean_field(name: str, clean_type: CleanType) -> str: + match clean_type: + case CleanType.Blockdevice: + return name.replace('_percentage', '%').replace('_', '-') + case CleanType.Dataclass: + return name.lower().replace('-', '_').replace('%', '_percentage') + case CleanType.Lsblk: + return name.replace('_percentage', '%').replace('_', '-') + + +def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = 3) -> List[LsblkInfo]: + fields = [_clean_field(f, CleanType.Lsblk) for f in LsblkInfo.fields()] + lsblk_fields = ','.join(fields) + + if not dev_path: + dev_path = '' + + if retry == 0: + retry = 1 + + result = None + + for i in range(retry): + try: + result = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}') + except SysCallError as error: + # 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) + time.sleep(1) + else: + raise error + + if result and result.exit_code == 0: + try: + if decoded := result.decode('utf-8'): + block_devices = json.loads(decoded) + 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) + 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] + + raise DiskError(f'lsblk failed to retrieve information for "{dev_path}"') + + +def get_all_lsblk_info() -> List[LsblkInfo]: + return _fetch_lsblk_info() + + +def get_lsblk_by_mountpoint(mountpoint: Path, as_prefix: bool = False) -> List[LsblkInfo]: + def _check(infos: List[LsblkInfo]) -> List[LsblkInfo]: + devices = [] + for entry in infos: + if as_prefix: + matches = [m for m in entry.mountpoints if str(m).startswith(str(mountpoint))] + if matches: + devices += [entry] + elif mountpoint in entry.mountpoints: + devices += [entry] + + if len(entry.children) > 0: + if len(match := _check(entry.children)) > 0: + devices += match + + return devices + + all_info = get_all_lsblk_info() + return _check(all_info) diff --git a/archinstall/lib/disk/diskinfo.py b/archinstall/lib/disk/diskinfo.py deleted file mode 100644 index b56ba282..00000000 --- a/archinstall/lib/disk/diskinfo.py +++ /dev/null @@ -1,40 +0,0 @@ -import dataclasses -import json -from dataclasses import dataclass, field -from typing import Optional, List - -from ..general import SysCommand -from ..exceptions import DiskError - -@dataclass -class LsblkInfo: - size: int = 0 - log_sec: int = 0 - pttype: Optional[str] = None - rota: bool = False - tran: Optional[str] = None - ptuuid: Optional[str] = None - partuuid: Optional[str] = None - uuid: Optional[str] = None - fstype: Optional[str] = None - type: Optional[str] = None - mountpoints: List[str] = field(default_factory=list) - - -def get_lsblk_info(dev_path: str) -> LsblkInfo: - fields = [f.name for f in dataclasses.fields(LsblkInfo)] - lsblk_fields = ','.join([f.upper().replace('_', '-') for f in fields]) - - output = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}').decode('UTF-8') - - if output: - block_devices = json.loads(output) - info = block_devices['blockdevices'][0] - lsblk_info = LsblkInfo() - - for f in fields: - setattr(lsblk_info, f, info[f.replace('_', '-')]) - - return lsblk_info - - raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') diff --git a/archinstall/lib/disk/dmcryptdev.py b/archinstall/lib/disk/dmcryptdev.py deleted file mode 100644 index 63392ffb..00000000 --- a/archinstall/lib/disk/dmcryptdev.py +++ /dev/null @@ -1,48 +0,0 @@ -import pathlib -import logging -import json -from dataclasses import dataclass -from typing import Optional -from ..exceptions import SysCallError -from ..general import SysCommand -from ..output import log -from .mapperdev import MapperDev - -@dataclass -class DMCryptDev: - dev_path :pathlib.Path - - @property - def name(self): - with open(f"/sys/devices/virtual/block/{pathlib.Path(self.path).name}/dm/name", "r") as fh: - return fh.read().strip() - - @property - def path(self): - return f"/dev/mapper/{self.dev_path}" - - @property - def blockdev(self): - pass - - @property - def MapperDev(self): - return MapperDev(mappername=self.name) - - @property - def mountpoint(self) -> Optional[str]: - try: - data = json.loads(SysCommand(f"findmnt --json -R {self.dev_path}").decode()) - for filesystem in data['filesystems']: - return filesystem.get('target') - - except SysCallError as error: - # Not mounted anywhere most likely - log(f"Could not locate mount information for {self.dev_path}: {error}", level=logging.WARNING, fg="yellow") - pass - - return None - - @property - def filesystem(self) -> Optional[str]: - return self.MapperDev.filesystem \ No newline at end of file diff --git a/archinstall/lib/disk/encryption.py b/archinstall/lib/disk/encryption.py deleted file mode 100644 index c7496bfa..00000000 --- a/archinstall/lib/disk/encryption.py +++ /dev/null @@ -1,174 +0,0 @@ -from typing import Dict, Optional, Any, TYPE_CHECKING, List - -from ..menu.abstract_menu import Selector, AbstractSubMenu -from ..menu.menu import MenuSelectionType -from ..menu.table_selection_menu import TableMenu -from ..models.disk_encryption import EncryptionType, DiskEncryption -from ..user_interaction.partitioning_conf import current_partition_layout -from ..user_interaction.utils import get_password -from ..menu import Menu -from ..general import secret -from ..hsm.fido import Fido2Device, Fido2 - -if TYPE_CHECKING: - _: Any - - -class DiskEncryptionMenu(AbstractSubMenu): - def __init__(self, data_store: Dict[str, Any], preset: Optional[DiskEncryption], disk_layouts: Dict[str, Any]): - if preset: - self._preset = preset - else: - self._preset = DiskEncryption() - - self._disk_layouts = disk_layouts - super().__init__(data_store=data_store) - - def _setup_selection_menu_options(self): - self._menu_options['encryption_password'] = \ - Selector( - _('Encryption password'), - lambda x: select_encrypted_password(), - display_func=lambda x: secret(x) if x else '', - default=self._preset.encryption_password, - enabled=True - ) - self._menu_options['encryption_type'] = \ - Selector( - _('Encryption type'), - func=lambda preset: select_encryption_type(preset), - display_func=lambda x: EncryptionType.type_to_text(x) if x else None, - dependencies=['encryption_password'], - default=self._preset.encryption_type, - enabled=True - ) - self._menu_options['partitions'] = \ - Selector( - _('Partitions'), - func=lambda preset: select_partitions_to_encrypt(self._disk_layouts, preset), - display_func=lambda x: f'{sum([len(y) for y in x.values()])} {_("Partitions")}' if x else None, - dependencies=['encryption_password'], - default=self._preset.partitions, - preview_func=self._prev_disk_layouts, - enabled=True - ) - self._menu_options['HSM'] = \ - Selector( - description=_('Use HSM to unlock encrypted drive'), - func=lambda preset: select_hsm(preset), - display_func=lambda x: self._display_hsm(x), - dependencies=['encryption_password'], - default=self._preset.hsm_device, - enabled=True - ) - - def run(self, allow_reset: bool = True) -> Optional[DiskEncryption]: - super().run(allow_reset=allow_reset) - - if self._data_store.get('encryption_password', None): - return DiskEncryption( - encryption_password=self._data_store.get('encryption_password', None), - encryption_type=self._data_store['encryption_type'], - partitions=self._data_store.get('partitions', None), - hsm_device=self._data_store.get('HSM', None) - ) - - return None - - def _display_hsm(self, device: Optional[Fido2Device]) -> Optional[str]: - if device: - return device.manufacturer - - if not Fido2.get_fido2_devices(): - return str(_('No HSM devices available')) - return None - - def _prev_disk_layouts(self) -> Optional[str]: - selector = self._menu_options['partitions'] - if selector.has_selection(): - partitions: Dict[str, Any] = selector.current_selection - - all_partitions = [] - for parts in partitions.values(): - all_partitions += parts - - output = str(_('Partitions to be encrypted')) + '\n' - output += current_partition_layout(all_partitions, with_title=False) - return output.rstrip() - return None - - -def select_encryption_type(preset: EncryptionType) -> Optional[EncryptionType]: - title = str(_('Select disk encryption option')) - options = [ - # _type_to_text(EncryptionType.FullDiskEncryption), - EncryptionType.type_to_text(EncryptionType.Partition) - ] - - preset_value = EncryptionType.type_to_text(preset) - choice = Menu(title, options, preset_values=preset_value).run() - - match choice.type_: - case MenuSelectionType.Reset: return None - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return EncryptionType.text_to_type(choice.value) # type: ignore - - -def select_encrypted_password() -> Optional[str]: - if passwd := get_password(prompt=str(_('Enter disk encryption password (leave blank for no encryption): '))): - return passwd - return None - - -def select_hsm(preset: Optional[Fido2Device] = None) -> Optional[Fido2Device]: - title = _('Select a FIDO2 device to use for HSM') - fido_devices = Fido2.get_fido2_devices() - - if fido_devices: - choice = TableMenu(title, data=fido_devices).run() - match choice.type_: - case MenuSelectionType.Reset: - return None - case MenuSelectionType.Skip: - return preset - case MenuSelectionType.Selection: - return choice.value # type: ignore - - return None - - -def select_partitions_to_encrypt(disk_layouts: Dict[str, Any], preset: Dict[str, Any]) -> Dict[str, Any]: - # If no partitions was marked as encrypted, but a password was supplied and we have some disks to format.. - # Then we need to identify which partitions to encrypt. This will default to / (root). - all_partitions = [] - for blockdevice in disk_layouts.values(): - if partitions := blockdevice.get('partitions'): - partitions = [p for p in partitions if p['mountpoint'] != '/boot'] - all_partitions += partitions - - if all_partitions: - title = str(_('Select which partitions to encrypt')) - partition_table = current_partition_layout(all_partitions, with_title=False).strip() - - choice = TableMenu( - title, - table_data=(all_partitions, partition_table), - multi=True - ).run() - - match choice.type_: - case MenuSelectionType.Reset: - return {} - case MenuSelectionType.Skip: - return preset - case MenuSelectionType.Selection: - selections: List[Any] = choice.value # type: ignore - partitions = {} - - for path, device in disk_layouts.items(): - for part in selections: - if part in device.get('partitions', []): - partitions.setdefault(path, []).append(part) - - return partitions - return {} diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py new file mode 100644 index 00000000..285270fb --- /dev/null +++ b/archinstall/lib/disk/encryption_menu.py @@ -0,0 +1,179 @@ +from pathlib import Path +from typing import Dict, Optional, Any, TYPE_CHECKING, List + +from ..disk import ( + DeviceModification, + PartitionModification, + DiskEncryption, + EncryptionType +) +from ..menu import ( + Selector, + AbstractSubMenu, + MenuSelectionType, + TableMenu +) +from ..user_interaction.utils import get_password +from ..menu import Menu +from ..general import secret +from .fido import Fido2Device, Fido2 +from ..output import FormattedOutput + +if TYPE_CHECKING: + _: Any + + +class DiskEncryptionMenu(AbstractSubMenu): + def __init__( + self, + mods: List[DeviceModification], + data_store: Dict[str, Any], + preset: Optional[DiskEncryption] = None + ): + if preset: + self._preset = preset + else: + self._preset = DiskEncryption() + + self._modifications = mods + super().__init__(data_store=data_store) + + def setup_selection_menu_options(self): + self._menu_options['encryption_password'] = \ + Selector( + _('Encryption password'), + lambda x: select_encrypted_password(), + display_func=lambda x: secret(x) if x else '', + default=self._preset.encryption_password, + enabled=True + ) + self._menu_options['encryption_type'] = \ + Selector( + _('Encryption type'), + func=lambda preset: select_encryption_type(preset), + display_func=lambda x: EncryptionType.type_to_text(x) if x else None, + dependencies=['encryption_password'], + default=self._preset.encryption_type, + enabled=True + ) + self._menu_options['partitions'] = \ + Selector( + _('Partitions'), + func=lambda preset: select_partitions_to_encrypt(self._modifications.device_modifications, preset), + display_func=lambda x: f'{len(x)} {_("Partitions")}' if x else None, + dependencies=['encryption_password'], + default=self._preset.partitions, + preview_func=self._prev_disk_layouts, + enabled=True + ) + self._menu_options['HSM'] = \ + Selector( + description=_('Use HSM to unlock encrypted drive'), + func=lambda preset: select_hsm(preset), + display_func=lambda x: self._display_hsm(x), + dependencies=['encryption_password'], + default=self._preset.hsm_device, + enabled=True + ) + + def run(self, allow_reset: bool = True) -> Optional[DiskEncryption]: + super().run(allow_reset=allow_reset) + + if self._data_store.get('encryption_password', None): + return DiskEncryption( + encryption_password=self._data_store.get('encryption_password', None), + encryption_type=self._data_store['encryption_type'], + partitions=self._data_store.get('partitions', None), + hsm_device=self._data_store.get('HSM', None) + ) + + return None + + def _display_hsm(self, device: Optional[Fido2Device]) -> Optional[str]: + if device: + return device.manufacturer + + if not Fido2.get_fido2_devices(): + return str(_('No HSM devices available')) + return None + + def _prev_disk_layouts(self) -> Optional[str]: + partitions: Optional[List[PartitionModification]] = self._menu_options['partitions'].current_selection + if partitions: + output = str(_('Partitions to be encrypted')) + '\n' + output += FormattedOutput.as_table(partitions) + return output.rstrip() + + return None + + +def select_encryption_type(preset: EncryptionType) -> Optional[EncryptionType]: + title = str(_('Select disk encryption option')) + options = [ + EncryptionType.type_to_text(EncryptionType.Partition) + ] + + preset_value = EncryptionType.type_to_text(preset) + choice = Menu(title, options, preset_values=preset_value).run() + + match choice.type_: + case MenuSelectionType.Reset: return None + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return EncryptionType.text_to_type(choice.value) # type: ignore + + +def select_encrypted_password() -> Optional[str]: + if passwd := get_password(prompt=str(_('Enter disk encryption password (leave blank for no encryption): '))): + return passwd + return None + + +def select_hsm(preset: Optional[Fido2Device] = None) -> Optional[Fido2Device]: + title = _('Select a FIDO2 device to use for HSM') + fido_devices = Fido2.get_fido2_devices() + + if fido_devices: + choice = TableMenu(title, data=fido_devices).run() + match choice.type_: + case MenuSelectionType.Reset: + return None + case MenuSelectionType.Skip: + return preset + case MenuSelectionType.Selection: + return choice.value # type: ignore + + return None + + +def select_partitions_to_encrypt( + modification: List[DeviceModification], + preset: List[PartitionModification] +) -> List[PartitionModification]: + partitions: List[PartitionModification] = [] + + # do not allow encrypting the boot partition + for mod in modification: + partitions += list(filter(lambda x: x.mountpoint != Path('/boot'), mod.partitions)) + + # do not allow encrypting existing partitions that are not marked as wipe + avail_partitions = list(filter(lambda x: not x.exists(), partitions)) + + if avail_partitions: + title = str(_('Select which partitions to encrypt')) + partition_table = FormattedOutput.as_table(avail_partitions) + + choice = TableMenu( + title, + table_data=(avail_partitions, partition_table), + preset=preset, + multi=True + ).run() + + match choice.type_: + case MenuSelectionType.Reset: + return [] + case MenuSelectionType.Skip: + return preset + case MenuSelectionType.Selection: + return choice.multi_value + return [] diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py new file mode 100644 index 00000000..436be4d4 --- /dev/null +++ b/archinstall/lib/disk/fido.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import getpass +import logging +from typing import List + +from .device_model import PartitionModification, Fido2Device +from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes +from ..output import log + + +class Fido2: + _loaded: bool = False + _fido2_devices: List[Fido2Device] = [] + + @classmethod + def get_fido2_devices(cls, reload: bool = False) -> List[Fido2Device]: + """ + Uses systemd-cryptenroll to list the FIDO2 devices + connected that supports FIDO2. + Some devices might show up in udevadm as FIDO2 compliant + when they are in fact not. + + The drawback of systemd-cryptenroll is that it uses human readable format. + That means we get this weird table like structure that is of no use. + + So we'll look for `MANUFACTURER` and `PRODUCT`, we take their index + and we split each line based on those positions. + + Output example: + + PATH MANUFACTURER PRODUCT + /dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID + """ + + # to prevent continous reloading which will slow + # down moving the cursor in the menu + if not cls._loaded or reload: + ret = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') + if not ret: + log('Unable to retrieve fido2 devices', level=logging.ERROR) + return [] + + fido_devices = clear_vt100_escape_codes(ret) + + manufacturer_pos = 0 + product_pos = 0 + devices = [] + + for line in fido_devices.split('\r\n'): + if '/dev' not in line: + manufacturer_pos = line.find('MANUFACTURER') + product_pos = line.find('PRODUCT') + continue + + path = line[:manufacturer_pos].rstrip() + manufacturer = line[manufacturer_pos:product_pos].rstrip() + product = line[product_pos:] + + devices.append( + Fido2Device(path, manufacturer, product) + ) + + cls._loaded = True + cls._fido2_devices = devices + + return cls._fido2_devices + + @classmethod + def fido2_enroll( + cls, + hsm_device: Fido2Device, + part_mod: PartitionModification, + password: str + ): + worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device.path} {part_mod.dev_path}", peek_output=True) + pw_inputted = False + pin_inputted = False + + while worker.is_alive(): + if pw_inputted is False: + if bytes(f"please enter current passphrase for disk {part_mod.dev_path}", 'UTF-8') in worker._trace_log.lower(): + worker.write(bytes(password, 'UTF-8')) + pw_inputted = True + elif pin_inputted is False: + if bytes(f"please enter security token pin", 'UTF-8') in worker._trace_log.lower(): + 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" + ) diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 1083df53..6ea99340 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -1,301 +1,98 @@ from __future__ import annotations -import time -import logging -import json -import pathlib -from typing import Optional, Dict, Any, TYPE_CHECKING -# https://stackoverflow.com/a/39757388/929999 -from ..models.disk_encryption import DiskEncryption -if TYPE_CHECKING: - from .blockdevice import BlockDevice - _: Any +import logging +import signal +import sys +import time +from typing import Any, Optional, TYPE_CHECKING -from .partition import Partition -from .validators import valid_fs_type -from ..exceptions import DiskError, SysCallError -from ..general import SysCommand +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 ..storage import storage - -GPT = 0b00000001 -MBR = 0b00000010 - -# A sane default is 5MiB, that allows for plenty of buffer for GRUB on MBR -# but also 4MiB for memory cards for instance. And another 1MiB to avoid issues. -# (we've been pestered by disk issues since the start, so please let this be here for a few versions) -DEFAULT_PARTITION_START = '5MiB' - -class Filesystem: - # TODO: - # When instance of a HDD is selected, check all usages and gracefully unmount them - # as well as close any crypto handles. - def __init__(self, blockdevice :BlockDevice, mode :int): - self.blockdevice = blockdevice - self.mode = mode - - def __enter__(self, *args :str, **kwargs :str) -> 'Filesystem': - return self - - def __repr__(self) -> str: - return f"Filesystem(blockdevice={self.blockdevice}, mode={self.mode})" +from ..menu import Menu - def __exit__(self, *args :str, **kwargs :str) -> bool: - # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager - if len(args) >= 2 and args[1]: - raise args[1] - - SysCommand('sync') - return True - - def partuuid_to_index(self, uuid :str) -> Optional[int]: - for i in range(storage['DISK_RETRY_ATTEMPTS']): - self.partprobe() - time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i)) - - # We'll use unreliable lbslk to grab children under the /dev/ - output = json.loads(SysCommand(f"lsblk --json {self.blockdevice.device}").decode('UTF-8')) +if TYPE_CHECKING: + _: Any - for device in output['blockdevices']: - for index, partition in enumerate(device.get('children', [])): - # But we'll use blkid to reliably grab the PARTUUID for that child device (partition) - partition_uuid = SysCommand(f"blkid -s PARTUUID -o value /dev/{partition.get('name')}").decode().strip() - if partition_uuid.lower() == uuid.lower(): - return index - raise DiskError(f"Failed to convert PARTUUID {uuid} to a partition index number on blockdevice {self.blockdevice.device}") +class FilesystemHandler: + def __init__( + self, + disk_config: DiskLayoutConfiguration, + enc_conf: Optional[DiskEncryption] = None + ): + self._disk_config = disk_config + self._enc_config = enc_conf - def load_layout(self, layout :Dict[str, Any]) -> None: - from ..luks import luks2 - from .btrfs import BTRFSPartition + 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) + return - # If the layout tells us to wipe the drive, we do so - if layout.get('wipe', False): - if self.mode == GPT: - if not self.parted_mklabel(self.blockdevice.device, "gpt"): - raise KeyError(f"Could not create a GPT label on {self}") - elif self.mode == MBR: - if not self.parted_mklabel(self.blockdevice.device, "msdos"): - raise KeyError(f"Could not create a MS-DOS label on {self}") + device_mods = list(filter(lambda x: len(x.partitions) > 0, self._disk_config.device_modifications)) - self.blockdevice.flush_cache() - time.sleep(3) + if not device_mods: + log('No modifications required', level=logging.DEBUG) + return - prev_partition = None - # We then iterate the partitions in order - for partition in layout.get('partitions', []): - # We don't want to re-add an existing partition (those containing a UUID already) - if partition.get('wipe', False) and not partition.get('PARTUUID', None): - start = partition.get('start') or ( - prev_partition and f'{prev_partition["device_instance"].end_sectors}s' or DEFAULT_PARTITION_START) - partition['device_instance'] = self.add_partition(partition.get('type', 'primary'), - start=start, - end=partition.get('size', '100%'), - partition_format=partition.get('filesystem', {}).get('format', 'btrfs'), - skip_mklabel=layout.get('wipe', False) is not False) + device_paths = ', '.join([str(mod.device.device_info.path) for mod in device_mods]) - elif (partition_uuid := partition.get('PARTUUID')): - # We try to deal with both UUID and PARTUUID of a partition when it's being re-used. - # We should re-name or separate this logi based on partition.get('PARTUUID') and partition.get('UUID') - # but for now, lets just attempt to deal with both. - try: - partition['device_instance'] = self.blockdevice.get_partition(uuid=partition_uuid) - except DiskError: - partition['device_instance'] = self.blockdevice.get_partition(partuuid=partition_uuid) + # Issue a final warning before we continue with something un-revertable. + # We mention the drive one last time, and count from 5 to 0. + print(str(_(' ! Formatting {} in ')).format(device_paths)) - log(_("Re-using partition instance: {}").format(partition['device_instance']), level=logging.DEBUG, fg="gray") - else: - log(f"{self}.load_layout() doesn't know how to work without 'wipe' being set or UUID ({partition.get('PARTUUID')}) was given and found.", fg="yellow", level=logging.WARNING) - continue + if show_countdown: + self._do_countdown() - if partition.get('filesystem', {}).get('format', False): - # needed for backward compatibility with the introduction of the new "format_options" - format_options = partition.get('options',[]) + partition.get('filesystem',{}).get('format_options',[]) - disk_encryption: DiskEncryption = storage['arguments'].get('disk_encryption') + # 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: + partition_table = PartitionTable.MBR - if disk_encryption and partition in disk_encryption.all_partitions: - if not partition['device_instance']: - raise DiskError(f"Internal error caused us to loose the partition. Please report this issue upstream!") + for mod in device_mods: + device_handler.partition(mod, partition_table=partition_table) + device_handler.format(mod, enc_conf=self._enc_config) - if partition.get('mountpoint',None): - loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['mountpoint']).name}loop" - else: - loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['device_instance'].path).name}" + for part_mod in mod.partitions: + if part_mod.fs_type == FilesystemType.Btrfs: + device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config) - partition['device_instance'].encrypt(password=disk_encryption.encryption_password) - # Immediately unlock the encrypted device to format the inner volume - with luks2(partition['device_instance'], loopdev, disk_encryption.encryption_password, auto_unmount=True) as unlocked_device: - if not partition.get('wipe'): - if storage['arguments'] == 'silent': - raise ValueError(f"Missing fs-type to format on newly created encrypted partition {partition['device_instance']}") - else: - if not partition.get('filesystem'): - partition['filesystem'] = {} + def _do_countdown(self) -> bool: + SIG_TRIGGER = False - if not partition['filesystem'].get('format', False): - while True: - partition['filesystem']['format'] = input(f"Enter a valid fs-type for newly encrypted partition {partition['filesystem']['format']}: ").strip() - if not partition['filesystem']['format'] or valid_fs_type(partition['filesystem']['format']) is False: - log(_("You need to enter a valid fs-type in order to continue. See `man parted` for valid fs-type's.")) - continue - break + def kill_handler(sig: int, frame: Any) -> None: + print() + exit(0) - unlocked_device.format(partition['filesystem']['format'], options=format_options) + def sig_handler(sig: int, frame: Any) -> None: + signal.signal(signal.SIGINT, kill_handler) - elif partition.get('wipe', False): - if not partition['device_instance']: - raise DiskError(f"Internal error caused us to loose the partition. Please report this issue upstream!") + original_sigint_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, sig_handler) - partition['device_instance'].format(partition['filesystem']['format'], options=format_options) + for i in range(5, 0, -1): + print(f"{i}", end='') - if partition['filesystem']['format'] == 'btrfs': - # We upgrade the device instance to a BTRFSPartition if we format it as such. - # This is so that we can gain access to more features than otherwise available in Partition() - partition['device_instance'] = BTRFSPartition( - partition['device_instance'].path, - block_device=partition['device_instance'].block_device, - encrypted=False, - filesystem='btrfs', - autodetect_filesystem=False - ) + for x in range(4): + sys.stdout.flush() + time.sleep(0.25) + print(".", end='') - if partition.get('boot', False): - log(f"Marking partition {partition['device_instance']} as bootable.") - self.set(self.partuuid_to_index(partition['device_instance'].part_uuid), 'boot on') + if SIG_TRIGGER: + prompt = _('Do you really want to abort?') + choice = Menu(prompt, Menu.yes_no(), skip=False).run() + if choice.value == Menu.yes(): + exit(0) - prev_partition = partition + if SIG_TRIGGER is False: + sys.stdin.read() - def find_partition(self, mountpoint :str) -> Partition: - for partition in self.blockdevice: - if partition.target_mountpoint == mountpoint or partition.mountpoint == mountpoint: - return partition + SIG_TRIGGER = False + signal.signal(signal.SIGINT, sig_handler) - def partprobe(self) -> bool: - try: - SysCommand(f'partprobe {self.blockdevice.device}') - except SysCallError as error: - log(f"Could not execute partprobe: {error!r}", level=logging.ERROR, fg="red") - raise DiskError(f"Could not run partprobe on {self.blockdevice.device}: {error!r}") + print() + signal.signal(signal.SIGINT, original_sigint_handler) return True - - def raw_parted(self, string: str) -> SysCommand: - try: - cmd_handle = SysCommand(f'/usr/bin/parted -s {string}') - time.sleep(0.5) - return cmd_handle - except SysCallError as error: - log(f"Parted ended with a bad exit code: {error.exit_code} ({error})", level=logging.ERROR, fg="red") - return error - - def parted(self, string: str) -> bool: - """ - Performs a parted execution of the given string - - :param string: A raw string passed to /usr/bin/parted -s - :type string: str - """ - if (parted_handle := self.raw_parted(string)).exit_code == 0: - return self.partprobe() - else: - raise DiskError(f"Parted failed to add a partition: {parted_handle}") - - def use_entire_disk(self, root_filesystem_type :str = 'ext4') -> Partition: - # TODO: Implement this with declarative profiles instead. - raise ValueError("Installation().use_entire_disk() has to be re-worked.") - - def add_partition( - self, - partition_type :str, - start :str, - end :str, - partition_format :Optional[str] = None, - skip_mklabel :bool = False - ) -> Partition: - log(f'Adding partition to {self.blockdevice}, {start}->{end}', level=logging.INFO) - - if len(self.blockdevice.partitions) == 0 and skip_mklabel is False: - # If it's a completely empty drive, and we're about to add partitions to it - # we need to make sure there's a filesystem label. - if self.mode == GPT: - if not self.parted_mklabel(self.blockdevice.device, "gpt"): - raise KeyError(f"Could not create a GPT label on {self}") - elif self.mode == MBR: - if not self.parted_mklabel(self.blockdevice.device, "msdos"): - raise KeyError(f"Could not create a MS-DOS label on {self}") - - self.blockdevice.flush_cache() - - previous_partuuids = [] - for partition in self.blockdevice.partitions.values(): - try: - previous_partuuids.append(partition.part_uuid) - except DiskError: - pass - - # TODO this check should probably run in the setup process rather than during the installation - if self.mode == MBR: - if len(self.blockdevice.partitions) > 3: - DiskError("Too many partitions on disk, MBR disks can only have 3 primary partitions") - - if partition_format: - parted_string = f'{self.blockdevice.device} mkpart {partition_type} {partition_format} {start} {end}' - else: - parted_string = f'{self.blockdevice.device} mkpart {partition_type} {start} {end}' - - log(f"Adding partition using the following parted command: {parted_string}", level=logging.DEBUG) - - if self.parted(parted_string): - for count in range(storage.get('DISK_RETRY_ATTEMPTS', 3)): - self.blockdevice.flush_cache() - - new_partition_uuids = [partition.part_uuid for partition in self.blockdevice.partitions.values()] - new_partuuid_set = (set(previous_partuuids) ^ set(new_partition_uuids)) - - if len(new_partuuid_set) and (new_partuuid := new_partuuid_set.pop()): - try: - return self.blockdevice.get_partition(partuuid=new_partuuid) - except Exception as err: - log(f'Blockdevice: {self.blockdevice}', level=logging.ERROR, fg="red") - log(f'Partitions: {self.blockdevice.partitions}', level=logging.ERROR, fg="red") - log(f'Partition set: {new_partuuid_set}', level=logging.ERROR, fg="red") - log(f'New PARTUUID: {[new_partuuid]}', level=logging.ERROR, fg="red") - log(f'get_partition(): {self.blockdevice.get_partition}', level=logging.ERROR, fg="red") - raise err - else: - log(f"Could not get UUID for partition. Waiting {storage.get('DISK_TIMEOUTS', 1) * count}s before retrying.",level=logging.DEBUG) - self.partprobe() - time.sleep(max(0.1, storage.get('DISK_TIMEOUTS', 1))) - else: - print("Parted did not return True during partition creation") - - total_partitions = set([partition.part_uuid for partition in self.blockdevice.partitions.values()]) - total_partitions.update(previous_partuuids) - - # TODO: This should never be able to happen - log(f"Could not find the new PARTUUID after adding the partition.", level=logging.ERROR, fg="red") - log(f"Previous partitions: {previous_partuuids}", level=logging.ERROR, fg="red") - log(f"New partitions: {total_partitions}", level=logging.ERROR, fg="red") - - raise DiskError(f"Could not add partition using: {parted_string}") - - def set_name(self, partition: int, name: str) -> bool: - return self.parted(f'{self.blockdevice.device} name {partition + 1} "{name}"') == 0 - - def set(self, partition: int, string: str) -> bool: - log(f"Setting {string} on (parted) partition index {partition+1}", level=logging.INFO) - return self.parted(f'{self.blockdevice.device} set {partition + 1} {string}') == 0 - - def parted_mklabel(self, device: str, disk_label: str) -> bool: - log(f"Creating a new partition label on {device}", level=logging.INFO, fg="yellow") - # Try to unmount devices before attempting to run mklabel - try: - SysCommand(f'bash -c "umount {device}?"') - except: - pass - - self.partprobe() - worked = self.raw_parted(f'{device} mklabel {disk_label}').exit_code == 0 - self.partprobe() - - return worked diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py deleted file mode 100644 index 80d0cb53..00000000 --- a/archinstall/lib/disk/helpers.py +++ /dev/null @@ -1,556 +0,0 @@ -from __future__ import annotations -import json -import logging -import os # type: ignore -import pathlib -import re -import time -import glob - -from typing import Union, List, Iterator, Dict, Optional, Any, TYPE_CHECKING -# https://stackoverflow.com/a/39757388/929999 -from .diskinfo import get_lsblk_info -from ..models.subvolume import Subvolume - -from .blockdevice import BlockDevice -from .dmcryptdev import DMCryptDev -from .mapperdev import MapperDev -from ..exceptions import SysCallError, DiskError -from ..general import SysCommand -from ..output import log -from ..storage import storage - -if TYPE_CHECKING: - from .partition import Partition - - -ROOT_DIR_PATTERN = re.compile('^.*?/devices') -GIGA = 2 ** 30 - -def convert_size_to_gb(size :Union[int, float]) -> float: - return round(size / GIGA,1) - -def sort_block_devices_based_on_performance(block_devices :List[BlockDevice]) -> Dict[BlockDevice, int]: - result = {device: 0 for device in block_devices} - - for device, weight in result.items(): - if device.spinning: - weight -= 10 - else: - weight += 5 - - if device.bus_type == 'nvme': - weight += 20 - elif device.bus_type == 'sata': - weight += 10 - - result[device] = weight - - return result - -def filter_disks_below_size_in_gb(devices :List[BlockDevice], gigabytes :int) -> Iterator[BlockDevice]: - for disk in devices: - if disk.size >= gigabytes: - yield disk - -def select_largest_device(devices :List[BlockDevice], gigabytes :int, filter_out :Optional[List[BlockDevice]] = None) -> BlockDevice: - if not filter_out: - filter_out = [] - - copy_devices = [*devices] - for filter_device in filter_out: - if filter_device in copy_devices: - copy_devices.pop(copy_devices.index(filter_device)) - - copy_devices = list(filter_disks_below_size_in_gb(copy_devices, gigabytes)) - - if not len(copy_devices): - return None - - return max(copy_devices, key=(lambda device : device.size)) - -def select_disk_larger_than_or_close_to(devices :List[BlockDevice], gigabytes :int, filter_out :Optional[List[BlockDevice]] = None) -> BlockDevice: - if not filter_out: - filter_out = [] - - copy_devices = [*devices] - for filter_device in filter_out: - if filter_device in copy_devices: - copy_devices.pop(copy_devices.index(filter_device)) - - if not len(copy_devices): - return None - - return min(copy_devices, key=(lambda device : abs(device.size - gigabytes))) - -def convert_to_gigabytes(string :str) -> float: - unit = string.strip()[-1] - size = float(string.strip()[:-1]) - - if unit == 'M': - size = size / 1024 - elif unit == 'T': - size = size * 1024 - - return size - -def device_state(name :str, *args :str, **kwargs :str) -> Optional[bool]: - # Based out of: https://askubuntu.com/questions/528690/how-to-get-list-of-all-non-removable-disk-device-names-ssd-hdd-and-sata-ide-onl/528709#528709 - if os.path.isfile('/sys/block/{}/device/block/{}/removable'.format(name, name)): - with open('/sys/block/{}/device/block/{}/removable'.format(name, name)) as f: - if f.read(1) == '1': - return - - path = ROOT_DIR_PATTERN.sub('', os.readlink('/sys/block/{}'.format(name))) - hotplug_buses = ("usb", "ieee1394", "mmc", "pcmcia", "firewire") - for bus in hotplug_buses: - if os.path.exists('/sys/bus/{}'.format(bus)): - for device_bus in os.listdir('/sys/bus/{}/devices'.format(bus)): - device_link = ROOT_DIR_PATTERN.sub('', os.readlink('/sys/bus/{}/devices/{}'.format(bus, device_bus))) - if re.search(device_link, path): - return - return True - - -def cleanup_bash_escapes(data :str) -> str: - return data.replace(r'\ ', ' ') - -def blkid(cmd :str) -> Dict[str, Any]: - if '-o' in cmd and '-o export' not in cmd: - raise ValueError(f"blkid() requires '-o export' to be used and can therefore not continue reliably.") - elif '-o' not in cmd: - cmd += ' -o export' - - try: - raw_data = SysCommand(cmd).decode() - except SysCallError as error: - log(f"Could not get block device information using blkid() using command {cmd}", level=logging.DEBUG) - raise error - - result = {} - # Process the raw result - devname = None - for line in raw_data.split('\r\n'): - if not len(line): - devname = None - continue - - key, val = line.split('=', 1) - if key.lower() == 'devname': - devname = val - # Lowercase for backwards compatibility with all_disks() previous use cases - result[devname] = { - "path": devname, - "PATH": devname - } - continue - - result[devname][key] = cleanup_bash_escapes(val) - - return result - -def get_loop_info(path :str) -> Dict[str, Any]: - for drive in json.loads(SysCommand(['losetup', '--json']).decode('UTF_8'))['loopdevices']: - if not drive['name'] == path: - continue - - return { - path: { - **drive, - 'type' : 'loop', - 'TYPE' : 'loop', - 'DEVTYPE' : 'loop', - 'PATH' : drive['name'], - 'path' : drive['name'] - } - } - - return {} - -def enrich_blockdevice_information(information :Dict[str, Any]) -> Dict[str, Any]: - result = {} - for device_path, device_information in information.items(): - dev_name = pathlib.Path(device_information['PATH']).name - if not device_information.get('TYPE') or not device_information.get('DEVTYPE'): - with open(f"/sys/class/block/{dev_name}/uevent") as fh: - device_information.update(uevent(fh.read())) - - if (dmcrypt_name := pathlib.Path(f"/sys/class/block/{dev_name}/dm/name")).exists(): - with dmcrypt_name.open('r') as fh: - device_information['DMCRYPT_NAME'] = fh.read().strip() - - result[device_path] = device_information - - return result - -def uevent(data :str) -> Dict[str, Any]: - information = {} - - for line in data.replace('\r\n', '\n').split('\n'): - if len((line := line.strip())): - key, val = line.split('=', 1) - information[key] = val - - return information - -def get_blockdevice_uevent(dev_name :str) -> Dict[str, Any]: - device_information = {} - with open(f"/sys/class/block/{dev_name}/uevent") as fh: - device_information.update(uevent(fh.read())) - - return { - f"/dev/{dev_name}" : { - **device_information, - 'path' : f'/dev/{dev_name}', - 'PATH' : f'/dev/{dev_name}', - 'PTTYPE' : None - } - } - - -def all_disks() -> List[BlockDevice]: - log(f"[Deprecated] archinstall.all_disks() is deprecated. Use archinstall.all_blockdevices() with the appropriate filters instead.", level=logging.WARNING, fg="yellow") - return all_blockdevices(partitions=False, mappers=False) - -def get_blockdevice_info(device_path, exclude_iso_dev :bool = True) -> Dict[str, Any]: - for retry_attempt in range(storage['DISK_RETRY_ATTEMPTS']): - partprobe(device_path) - time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * retry_attempt)) - - try: - if exclude_iso_dev: - # exclude all devices associated with the iso boot locations - iso_devs = ['/run/archiso/airootfs', '/run/archiso/bootmnt'] - - try: - lsblk_info = get_lsblk_info(device_path) - except DiskError: - continue - - if any([dev in lsblk_info.mountpoints for dev in iso_devs]): - continue - - information = blkid(f'blkid -p -o export {device_path}') - return enrich_blockdevice_information(information) - except SysCallError as ex: - if ex.exit_code == 2: - # Assume that it's a loop device, and try to get info on it - try: - resolved_device_name = device_path.readlink().name - except OSError: - resolved_device_name = device_path.name - - try: - information = get_loop_info(device_path) - if not information: - raise SysCallError(f"Could not get loop information for {resolved_device_name}", exit_code=1) - return enrich_blockdevice_information(information) - - except SysCallError: - information = get_blockdevice_uevent(resolved_device_name) - return enrich_blockdevice_information(information) - else: - # We could not reliably get any information, perhaps the disk is clean of information? - if retry_attempt == storage['DISK_RETRY_ATTEMPTS'] - 1: - raise ex - -def all_blockdevices( - mappers: bool = False, - partitions: bool = False, - error: bool = False, - exclude_iso_dev: bool = True -) -> Dict[str, Any]: - """ - Returns BlockDevice() and Partition() objects for all available devices. - """ - from .partition import Partition - - instances = {} - - # Due to lsblk being highly unreliable for this use case, - # we'll iterate the /sys/class definitions and find the information - # from there. - for block_device in glob.glob("/sys/class/block/*"): - try: - device_path = pathlib.Path(f"/dev/{pathlib.Path(block_device).readlink().name}") - except FileNotFoundError: - log(f"Unknown device found by '/sys/class/block/*', ignoring: {device_path}", level=logging.WARNING, fg="yellow") - - if device_path.exists() is False: - log(f"Unknown device found by '/sys/class/block/*', ignoring: {device_path}", level=logging.WARNING, fg="yellow") - continue - - information = get_blockdevice_info(device_path) - if not information: - continue - - for path, path_info in information.items(): - if path_info.get('DMCRYPT_NAME'): - instances[path] = DMCryptDev(dev_path=path) - elif path_info.get('PARTUUID') or path_info.get('PART_ENTRY_NUMBER'): - if partitions: - instances[path] = Partition(path, block_device=BlockDevice(get_parent_of_partition(pathlib.Path(path)))) - elif path_info.get('PTTYPE', False) is not False or path_info.get('TYPE') == 'loop': - instances[path] = BlockDevice(path, path_info) - elif path_info.get('TYPE') in ('squashfs', 'erofs'): - # We can ignore squashfs devices (usually /dev/loop0 on Arch ISO) - continue - else: - log(f"Unknown device found by all_blockdevices(), ignoring: {information}", level=logging.WARNING, fg="yellow") - - if mappers: - for block_device in glob.glob("/dev/mapper/*"): - if (pathobj := pathlib.Path(block_device)).is_symlink(): - instances[f"/dev/mapper/{pathobj.name}"] = MapperDev(mappername=pathobj.name) - - return instances - - -def get_parent_of_partition(path :pathlib.Path) -> pathlib.Path: - partition_name = path.name - pci_device = (pathlib.Path("/sys/class/block") / partition_name).resolve() - return f"/dev/{pci_device.parent.name}" - -def harddrive(size :Optional[float] = None, model :Optional[str] = None, fuzzy :bool = False) -> Optional[BlockDevice]: - collection = all_blockdevices(partitions=False) - for drive in collection: - if size and convert_to_gigabytes(collection[drive]['size']) != size: - continue - if model and (collection[drive]['model'] is None or collection[drive]['model'].lower() != model.lower()): - continue - - return collection[drive] - -def split_bind_name(path :Union[pathlib.Path, str]) -> list: - # log(f"[Deprecated] Partition().subvolumes now contain the split bind name via it's subvolume.name instead.", level=logging.WARNING, fg="yellow") - # we check for the bind notation. if exist we'll only use the "true" device path - if '[' in str(path) : # is a bind path (btrfs subvolume path) - device_path, bind_path = str(path).split('[') - bind_path = bind_path[:-1].strip() # remove the ] - else: - device_path = path - bind_path = None - return device_path,bind_path - -def find_mountpoint(device_path :str) -> Dict[str, Any]: - try: - for filesystem in json.loads(SysCommand(f'/usr/bin/findmnt -R --json {device_path}').decode())['filesystems']: - yield filesystem - except SysCallError: - return {} - -def findmnt(path :pathlib.Path, traverse :bool = False, ignore :List = [], recurse :bool = True) -> Dict[str, Any]: - for traversal in list(map(str, [str(path)] + list(path.parents))): - if traversal in ignore: - continue - - try: - log(f"Getting mount information for device path {traversal}", level=logging.DEBUG) - if (output := SysCommand(f"/usr/bin/findmnt --json {'--submounts' if recurse else ''} {traversal}").decode('UTF-8')): - return json.loads(output) - - except SysCallError as error: - log(f"Could not get mount information on {path} but continuing and ignoring: {error}", level=logging.INFO, fg="gray") - pass - - if not traverse: - break - - raise DiskError(f"Could not get mount information for path {path}") - - -def get_mount_info(path :Union[pathlib.Path, str], traverse :bool = False, return_real_path :bool = False, ignore :List = []) -> Dict[str, Any]: - import traceback - - log(f"Deprecated: archinstall.get_mount_info(). Use archinstall.findmnt() instead, which does not do any automatic parsing. Please change at:\n{''.join(traceback.format_stack())}") - device_path, bind_path = split_bind_name(path) - output = {} - - for traversal in list(map(str, [str(device_path)] + list(pathlib.Path(str(device_path)).parents))): - if traversal in ignore: - continue - - try: - log(f"Getting mount information for device path {traversal}", level=logging.DEBUG) - if (output := SysCommand(f'/usr/bin/findmnt --json {traversal}').decode('UTF-8')): - break - - except SysCallError as error: - print('ERROR:', error) - pass - - if not traverse: - break - - if not output: - raise DiskError(f"Could not get mount information for device path {device_path}") - - output = json.loads(output) - - # for btrfs partitions we redice the filesystem list to the one with the source equals to the parameter - # i.e. the subvolume filesystem we're searching for - if 'filesystems' in output and len(output['filesystems']) > 1 and bind_path is not None: - output['filesystems'] = [entry for entry in output['filesystems'] if entry['source'] == str(path)] - - if 'filesystems' in output: - if len(output['filesystems']) > 1: - raise DiskError(f"Path '{device_path}' contains multiple mountpoints: {output['filesystems']}") - - if return_real_path: - return output['filesystems'][0], traversal - else: - return output['filesystems'][0] - - if return_real_path: - return {}, traversal - else: - return {} - - -def get_all_targets(data :Dict[str, Any], filters :Dict[str, None] = {}) -> Dict[str, None]: - for info in data: - if info.get('target') not in filters: - filters[info.get('target')] = None - - filters.update(get_all_targets(info.get('children', []))) - - return filters - -def get_partitions_in_use(mountpoint :str) -> Dict[str, Any]: - from .partition import Partition - - try: - output = SysCommand(f"/usr/bin/findmnt --json -R {mountpoint}").decode('UTF-8') - except SysCallError: - return {} - - if not output: - return {} - - output = json.loads(output) - - mounts = {} - - block_devices_available = all_blockdevices(mappers=True, partitions=True, error=True) - - block_devices_mountpoints = {} - for blockdev in block_devices_available.values(): - if not type(blockdev) in (Partition, MapperDev): - continue - - if isinstance(blockdev, Partition): - if blockdev.mountpoints: - for blockdev_mountpoint in blockdev.mountpoints: - block_devices_mountpoints[blockdev_mountpoint] = blockdev - else: - if blockdev.mount_information: - for blockdev_mountpoint in blockdev.mount_information: - block_devices_mountpoints[blockdev_mountpoint['target']] = blockdev - - log(f'Filtering available mounts {block_devices_mountpoints} to those under {mountpoint}', level=logging.DEBUG) - - for mountpoint in list(get_all_targets(output['filesystems']).keys()): - # Since all_blockdevices() returns PosixPath objects, we need to convert - # findmnt paths to pathlib.Path() first: - mountpoint = pathlib.Path(mountpoint) - - if mountpoint in block_devices_mountpoints: - if mountpoint not in mounts: - mounts[mountpoint] = block_devices_mountpoints[mountpoint] - # If the already defined mountpoint is a DMCryptDev, and the newly found - # mountpoint is a MapperDev, it has precedence and replaces the old mountpoint definition. - elif type(mounts[mountpoint]) == DMCryptDev and type(block_devices_mountpoints[mountpoint]) == MapperDev: - mounts[mountpoint] = block_devices_mountpoints[mountpoint] - - log(f"Available partitions: {mounts}", level=logging.DEBUG) - - return mounts - - -def get_filesystem_type(path :str) -> Optional[str]: - try: - return SysCommand(f"blkid -o value -s TYPE {path}").decode('UTF-8').strip() - except SysCallError: - return None - - -def disk_layouts() -> Optional[Dict[str, Any]]: - try: - if (handle := SysCommand("lsblk -f -o+TYPE,SIZE -J")).exit_code == 0: - return {str(key): val for key, val in json.loads(handle.decode('UTF-8')).items()} - else: - log(f"Could not return disk layouts: {handle}", level=logging.WARNING, fg="yellow") - return None - except SysCallError as err: - log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") - return None - except json.decoder.JSONDecodeError as err: - log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") - return None - - -def find_partition_by_mountpoint(block_devices :List[BlockDevice], relative_mountpoint :str) -> Partition: - for device in block_devices: - for partition in block_devices[device]['partitions']: - if partition.get('mountpoint', None) == relative_mountpoint: - return partition - -def partprobe(path :str = '') -> bool: - try: - if SysCommand(f'bash -c "partprobe {path}"').exit_code == 0: - return True - except SysCallError: - pass - return False - -def convert_device_to_uuid(path :str) -> str: - device_name, bind_name = split_bind_name(path) - - for i in range(storage['DISK_RETRY_ATTEMPTS']): - partprobe(device_name) - time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i)) # TODO: Remove, we should be relying on blkid instead of lsblk - - # TODO: Convert lsblk to blkid - # (lsblk supports BlockDev and Partition UUID grabbing, blkid requires you to pick PTUUID and PARTUUID) - output = json.loads(SysCommand(f"lsblk --json -o+UUID {device_name}").decode('UTF-8')) - - for device in output['blockdevices']: - if (dev_uuid := device.get('uuid', None)): - return dev_uuid - - raise DiskError(f"Could not retrieve the UUID of {path} within a timely manner.") - - -def has_mountpoint(partition: Union[dict,Partition,MapperDev], target: str, strict: bool = True) -> bool: - """ Determine if a certain partition is mounted (or has a mountpoint) as specific target (path) - Coded for clarity rather than performance - - Input parms: - :parm partition the partition we check - :type Either a Partition object or a dict with the contents of a partition definition in the disk_layouts schema - - :parm target (a string representing a mount path we want to check for. - :type str - - :parm strict if the check will be strict, target is exactly the mountpoint, or no, where the target is a leaf (f.i. to check if it is in /mnt/archinstall/). Not available for root check ('/') for obvious reasons - - """ - # we create the mountpoint list - if isinstance(partition,dict): - subvolumes: List[Subvolume] = partition.get('btrfs',{}).get('subvolumes', []) - mountpoints = [partition.get('mountpoint')] - mountpoints += [volume.mountpoint for volume in subvolumes] - else: - mountpoints = [partition.mountpoint,] + [subvol.target for subvol in partition.subvolumes] - - # we check - if strict or target == '/': - if target in mountpoints: - return True - else: - return False - else: - for mp in mountpoints: - if mp and mp.endswith(target): - return True - return False diff --git a/archinstall/lib/disk/mapperdev.py b/archinstall/lib/disk/mapperdev.py deleted file mode 100644 index bf1b3583..00000000 --- a/archinstall/lib/disk/mapperdev.py +++ /dev/null @@ -1,92 +0,0 @@ -import glob -import pathlib -import logging -import json -from dataclasses import dataclass -from typing import Optional, List, Dict, Any, Iterator, TYPE_CHECKING - -from ..exceptions import SysCallError -from ..general import SysCommand -from ..output import log - -if TYPE_CHECKING: - from .btrfs import BtrfsSubvolumeInfo - -@dataclass -class MapperDev: - mappername :str - - @property - def name(self): - return self.mappername - - @property - def path(self): - return f"/dev/mapper/{self.mappername}" - - @property - def part_uuid(self): - return self.partition.part_uuid - - @property - def partition(self): - from .helpers import uevent, get_parent_of_partition - from .partition import Partition - from .blockdevice import BlockDevice - - for mapper in glob.glob('/dev/mapper/*'): - path_obj = pathlib.Path(mapper) - if path_obj.name == self.mappername and pathlib.Path(mapper).is_symlink(): - dm_device = (pathlib.Path("/dev/mapper/") / path_obj.readlink()).resolve() - - for slave in glob.glob(f"/sys/class/block/{dm_device.name}/slaves/*"): - partition_belonging_to_dmcrypt_device = pathlib.Path(slave).name - - try: - uevent_data = SysCommand(f"blkid -o export /dev/{partition_belonging_to_dmcrypt_device}").decode() - except SysCallError as error: - log(f"Could not get information on device /dev/{partition_belonging_to_dmcrypt_device}: {error}", level=logging.ERROR, fg="red") - - information = uevent(uevent_data) - block_device = BlockDevice(get_parent_of_partition('/dev/' / pathlib.Path(information['DEVNAME']))) - - return Partition(information['DEVNAME'], block_device=block_device) - - raise ValueError(f"Could not convert {self.mappername} to a real dm-crypt device") - - @property - def mountpoint(self) -> Optional[pathlib.Path]: - try: - data = json.loads(SysCommand(f"findmnt --json -R {self.path}").decode()) - for filesystem in data['filesystems']: - return pathlib.Path(filesystem.get('target')) - - except SysCallError as error: - # Not mounted anywhere most likely - log(f"Could not locate mount information for {self.path}: {error}", level=logging.WARNING, fg="yellow") - pass - - return None - - @property - def mountpoints(self) -> List[Dict[str, Any]]: - return [obj['target'] for obj in self.mount_information] - - @property - def mount_information(self) -> List[Dict[str, Any]]: - from .helpers import find_mountpoint - return [{**obj, 'target' : pathlib.Path(obj.get('target', '/dev/null'))} for obj in find_mountpoint(self.path)] - - @property - def filesystem(self) -> Optional[str]: - from .helpers import get_filesystem_type - return get_filesystem_type(self.path) - - @property - def subvolumes(self) -> Iterator['BtrfsSubvolumeInfo']: - from .btrfs import subvolume_info_from_path - - for mountpoint in self.mount_information: - if target := mountpoint.get('target'): - if subvolume := subvolume_info_from_path(pathlib.Path(target)): - yield subvolume diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py deleted file mode 100644 index 87eaa6a7..00000000 --- a/archinstall/lib/disk/partition.py +++ /dev/null @@ -1,661 +0,0 @@ -import glob -import time -import logging -import json -import os -import hashlib -import typing -from dataclasses import dataclass, field -from pathlib import Path -from typing import Optional, Dict, Any, List, Union, Iterator - -from .blockdevice import BlockDevice -from .helpers import get_filesystem_type, convert_size_to_gb, split_bind_name -from ..storage import storage -from ..exceptions import DiskError, SysCallError, UnknownFilesystemFormat -from ..output import log -from ..general import SysCommand -from .btrfs.btrfs_helpers import subvolume_info_from_path -from .btrfs.btrfssubvolumeinfo import BtrfsSubvolumeInfo - -@dataclass -class PartitionInfo: - partition_object: 'Partition' - device_path: str # This would be /dev/sda1 for instance - bootable: bool - size: float - sector_size: int - start: Optional[int] - end: Optional[int] - pttype: Optional[str] - filesystem_type: Optional[str] - partuuid: Optional[str] - uuid: Optional[str] - mountpoints: List[Path] = field(default_factory=list) - - def __post_init__(self): - if not all([self.partuuid, self.uuid]): - for i in range(storage['DISK_RETRY_ATTEMPTS']): - lsblk_info = SysCommand(f"lsblk --json -b -o+LOG-SEC,SIZE,PTTYPE,PARTUUID,UUID,FSTYPE {self.device_path}").decode('UTF-8') - try: - lsblk_info = json.loads(lsblk_info) - except json.decoder.JSONDecodeError: - log(f"Could not decode JSON: {lsblk_info}", fg="red", level=logging.ERROR) - raise DiskError(f'Failed to retrieve information for "{self.device_path}" with lsblk') - - if not (device := lsblk_info.get('blockdevices', [None])[0]): - raise DiskError(f'Failed to retrieve information for "{self.device_path}" with lsblk') - - self.partuuid = device.get('partuuid') - self.uuid = device.get('uuid') - - # Lets build a list of requirements that we would like - # to retry and build (stuff that can take time between partprobes) - requirements = [] - requirements.append(self.partuuid) - - # Unformatted partitions won't have a UUID - if lsblk_info.get('fstype') is not None: - requirements.append(self.uuid) - - if all(requirements): - break - - self.partition_object.partprobe() - time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i)) - - def get_first_mountpoint(self) -> Optional[Path]: - if len(self.mountpoints) > 0: - return self.mountpoints[0] - return None - - -class Partition: - def __init__( - self, - path: str, - block_device: BlockDevice, - part_id :Optional[str] = None, - filesystem :Optional[str] = None, - mountpoint :Optional[str] = None, - encrypted :bool = False, - autodetect_filesystem :bool = True, - ): - if not part_id: - part_id = os.path.basename(path) - - if type(block_device) is str: - raise ValueError(f"Partition()'s 'block_device' parameter has to be a archinstall.BlockDevice() instance!") - - self.block_device = block_device - self._path = path - self._part_id = part_id - self._target_mountpoint = mountpoint - self._encrypted = encrypted - self._wipe = False - self._type = 'primary' - - if mountpoint: - self.mount(mountpoint) - - try: - self._partition_info = self._fetch_information() - - if not autodetect_filesystem and filesystem: - self._partition_info.filesystem_type = filesystem - - if self._partition_info.filesystem_type == 'crypto_LUKS': - self._encrypted = True - except DiskError: - self._partition_info = None - - @typing.no_type_check # I hate doint this but I'm currently unsure where this is used. - def __lt__(self, left_comparitor :BlockDevice) -> bool: - if type(left_comparitor) == Partition: - left_comparitor = left_comparitor.path - else: - left_comparitor = str(left_comparitor) - - # The goal is to check if /dev/nvme0n1p1 comes before /dev/nvme0n1p5 - return self._path < left_comparitor - - def __repr__(self, *args :str, **kwargs :str) -> str: - mount_repr = '' - if self._partition_info: - if mountpoint := self._partition_info.get_first_mountpoint(): - mount_repr = f", mounted={mountpoint}" - elif self._target_mountpoint: - mount_repr = f", rel_mountpoint={self._target_mountpoint}" - - classname = self.__class__.__name__ - - if not self._partition_info: - return f'{classname}(path={self._path})' - elif self._encrypted: - return f'{classname}(path={self._path}, size={self.size}, PARTUUID={self.part_uuid}, parent={self.real_device}, fs={self._partition_info.filesystem_type}{mount_repr})' - else: - return f'{classname}(path={self._path}, size={self.size}, PARTUUID={self.part_uuid}, fs={self._partition_info.filesystem_type}{mount_repr})' - - def as_json(self) -> Dict[str, Any]: - """ - this is used for the table representation of the partition (see FormattedOutput) - """ - partition_info = { - 'type': self._type, - 'PARTUUID': self.part_uuid, - 'wipe': self._wipe, - 'boot': self.boot, - 'ESP': self.boot, - 'mountpoint': self._target_mountpoint, - 'encrypted': self._encrypted, - 'start': self.start, - 'size': self.end, - 'filesystem': self._partition_info.filesystem_type if self._partition_info else 'Unknown' - } - - return partition_info - - def __dump__(self) -> Dict[str, Any]: - # TODO remove this in favour of as_json - return { - 'type': self._type, - 'PARTUUID': self.part_uuid, - 'wipe': self._wipe, - 'boot': self.boot, - 'ESP': self.boot, - 'mountpoint': self._target_mountpoint, - 'encrypted': self._encrypted, - 'start': self.start, - 'size': self.end, - 'filesystem': { - 'format': self._partition_info.filesystem_type if self._partition_info else 'None' - } - } - - def _call_lsblk(self) -> Dict[str, Any]: - for retry_attempt in range(storage['DISK_RETRY_ATTEMPTS']): - self.partprobe() - time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * retry_attempt)) # TODO: Remove, we should be relying on blkid instead of lsblk - # This sleep might be overkill, but lsblk is known to - # work against a chaotic cache that can change during call - # causing no information to be returned (blkid is better) - # time.sleep(1) - - # TODO: Maybe incorporate a re-try system here based on time.sleep(max(0.1, storage.get('DISK_TIMEOUTS', 1))) - - try: - output = SysCommand(f"lsblk --json -b -o+LOG-SEC,SIZE,PTTYPE,PARTUUID,UUID,FSTYPE {self.device_path}").decode('UTF-8') - except SysCallError as error: - # Get the output minus the message/info from lsblk if it returns a non-zero exit code. - output = error.worker.decode('UTF-8') - if '{' in output: - output = output[output.find('{'):] - - if output: - try: - lsblk_info = json.loads(output) - return lsblk_info - except json.decoder.JSONDecodeError: - log(f"Could not decode JSON: {output}", fg="red", level=logging.ERROR) - - raise DiskError(f'Failed to get partition information "{self.device_path}" with lsblk') - - def _call_sfdisk(self) -> Dict[str, Any]: - output = SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8') - - if output: - sfdisk_info = json.loads(output) - partitions = sfdisk_info.get('partitiontable', {}).get('partitions', []) - node = list(filter(lambda x: x['node'] == self._path, partitions)) - - if len(node) > 0: - return node[0] - - return {} - - raise DiskError(f'Failed to read disk "{self.block_device.path}" with sfdisk') - - def _fetch_information(self) -> PartitionInfo: - lsblk_info = self._call_lsblk() - sfdisk_info = self._call_sfdisk() - - if not (device := lsblk_info.get('blockdevices', [])): - raise DiskError(f'Failed to retrieve information for "{self.device_path}" with lsblk') - - # Grab the first (and only) block device in the list as we're targeting a specific partition - device = device[0] - - mountpoints = [Path(mountpoint) for mountpoint in device['mountpoints'] if mountpoint] - bootable = sfdisk_info.get('bootable', False) or sfdisk_info.get('type', '') == 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B' - - return PartitionInfo( - partition_object=self, - device_path=self._path, - pttype=device['pttype'], - partuuid=device['partuuid'], - uuid=device['uuid'], - sector_size=device['log-sec'], - size=convert_size_to_gb(device['size']), - start=sfdisk_info.get('start', None), - end=sfdisk_info.get('size', None), - bootable=bootable, - filesystem_type=device['fstype'], - mountpoints=mountpoints - ) - - @property - def target_mountpoint(self) -> Optional[str]: - return self._target_mountpoint - - @property - def path(self) -> str: - return self._path - - @property - def filesystem(self) -> str: - if self._partition_info: - return self._partition_info.filesystem_type - - @property - def mountpoint(self) -> Optional[Path]: - if len(self.mountpoints) > 0: - return self.mountpoints[0] - return None - - @property - def mountpoints(self) -> List[Path]: - if self._partition_info: - return self._partition_info.mountpoints - - @property - def sector_size(self) -> int: - if self._partition_info: - return self._partition_info.sector_size - - @property - def start(self) -> Optional[int]: - if self._partition_info: - return self._partition_info.start - - @property - def end(self) -> Optional[int]: - if self._partition_info: - return self._partition_info.end - - @property - def end_sectors(self) -> Optional[int]: - if self._partition_info: - start = self._partition_info.start - end = self._partition_info.end - if start and end: - return start + end - - @property - def size(self) -> Optional[float]: - if self._partition_info: - return self._partition_info.size - - @property - def boot(self) -> bool: - if self._partition_info: - return self._partition_info.bootable - - @property - def partition_type(self) -> Optional[str]: - if self._partition_info: - return self._partition_info.pttype - - @property - def part_uuid(self) -> str: - if self._partition_info: - return self._partition_info.partuuid - - @property - def uuid(self) -> Optional[str]: - """ - Returns the UUID as returned by lsblk for the **partition**. - This is more reliable than relying on /dev/disk/by-uuid as - it doesn't seam to be able to detect md raid partitions. - For bind mounts all the subvolumes share the same uuid - """ - for i in range(storage['DISK_RETRY_ATTEMPTS']): - if not self.partprobe(): - raise DiskError(f"Could not perform partprobe on {self.device_path}") - - time.sleep(storage.get('DISK_TIMEOUTS', 1) * i) - - partuuid = self._safe_uuid - if partuuid: - return partuuid - - raise DiskError(f"Could not get PARTUUID for {self.path} using 'blkid -s PARTUUID -o value {self.path}'") - - @property - def _safe_uuid(self) -> Optional[str]: - """ - A near copy of self.uuid but without any delays. - This function should only be used where uuid is not crucial. - For instance when you want to get a __repr__ of the class. - """ - if not self.partprobe(): - if self.block_device.partition_type == 'iso9660': - return None - - log(f"Could not reliably refresh PARTUUID of partition {self.device_path} due to partprobe error.", level=logging.DEBUG) - - try: - return SysCommand(f'blkid -s UUID -o value {self.device_path}').decode('UTF-8').strip() - except SysCallError as error: - if self.block_device.partition_type == 'iso9660': - # Parent device is a Optical Disk (.iso dd'ed onto a device for instance) - return None - - log(f"Could not get PARTUUID of partition using 'blkid -s UUID -o value {self.device_path}': {error}") - - @property - def _safe_part_uuid(self) -> Optional[str]: - """ - A near copy of self.uuid but without any delays. - This function should only be used where uuid is not crucial. - For instance when you want to get a __repr__ of the class. - """ - if not self.partprobe(): - if self.block_device.partition_type == 'iso9660': - return None - - log(f"Could not reliably refresh PARTUUID of partition {self.device_path} due to partprobe error.", level=logging.DEBUG) - - try: - return SysCommand(f'blkid -s PARTUUID -o value {self.device_path}').decode('UTF-8').strip() - except SysCallError as error: - if self.block_device.partition_type == 'iso9660': - # Parent device is a Optical Disk (.iso dd'ed onto a device for instance) - return None - - log(f"Could not get PARTUUID of partition using 'blkid -s PARTUUID -o value {self.device_path}': {error}") - - if self._partition_info: - return self._partition_info.uuid - - @property - def encrypted(self) -> Union[bool, None]: - return self._encrypted - - @property - def parent(self) -> str: - return self.real_device - - @property - def real_device(self) -> str: - output = SysCommand('lsblk -J').decode('UTF-8') - - if output: - for blockdevice in json.loads(output)['blockdevices']: - if parent := self.find_parent_of(blockdevice, os.path.basename(self.device_path)): - return f"/dev/{parent}" - return self._path - - raise DiskError('Unable to get disk information for command "lsblk -J"') - - @property - def device_path(self) -> str: - """ for bind mounts returns the physical path of the partition - """ - device_path, bind_name = split_bind_name(self._path) - return device_path - - @property - def bind_name(self) -> str: - """ for bind mounts returns the bind name (subvolume path). - Returns none if this property does not exist - """ - device_path, bind_name = split_bind_name(self._path) - return bind_name - - @property - def subvolumes(self) -> Iterator[BtrfsSubvolumeInfo]: - from .helpers import findmnt - - def iterate_children_recursively(information): - for child in information.get('children', []): - if target := child.get('target'): - if child.get('fstype') == 'btrfs': - if subvolume := subvolume_info_from_path(Path(target)): - yield subvolume - - if child.get('children'): - for subchild in iterate_children_recursively(child): - yield subchild - - if self._partition_info.filesystem_type == 'btrfs': - for mountpoint in self._partition_info.mountpoints: - if result := findmnt(mountpoint): - for filesystem in result.get('filesystems', []): - if subvolume := subvolume_info_from_path(mountpoint): - yield subvolume - - for child in iterate_children_recursively(filesystem): - yield child - - def partprobe(self) -> bool: - try: - if self.block_device: - return 0 == SysCommand(f'partprobe {self.block_device.device}').exit_code - except SysCallError as error: - log(f"Unreliable results might be given for {self._path} due to partprobe error: {error}", level=logging.DEBUG) - - return False - - def detect_inner_filesystem(self, password :str) -> Optional[str]: - log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=logging.INFO) - from ..luks import luks2 - - try: - with luks2(self, storage.get('ENC_IDENTIFIER', 'ai') + 'loop', password, auto_unmount=True) as unlocked_device: - return unlocked_device.filesystem - except SysCallError: - pass - return None - - def has_content(self) -> bool: - fs_type = self._partition_info.filesystem_type - if not fs_type or "swap" in fs_type: - return False - - temporary_mountpoint = '/tmp/' + hashlib.md5(bytes(f"{time.time()}", 'UTF-8') + os.urandom(12)).hexdigest() - temporary_path = Path(temporary_mountpoint) - - temporary_path.mkdir(parents=True, exist_ok=True) - if (handle := SysCommand(f'/usr/bin/mount {self._path} {temporary_mountpoint}')).exit_code != 0: - raise DiskError(f'Could not mount and check for content on {self._path} because: {handle}') - - files = len(glob.glob(f"{temporary_mountpoint}/*")) - iterations = 0 - while SysCommand(f"/usr/bin/umount -R {temporary_mountpoint}").exit_code != 0 and (iterations := iterations + 1) < 10: - time.sleep(1) - - temporary_path.rmdir() - - return True if files > 0 else False - - def encrypt(self, password: Optional[str] = None) -> str: - """ - A wrapper function for luks2() instances and the .encrypt() method of that instance. - """ - from ..luks import luks2 - - handle = luks2(self, None, None) - return handle.encrypt(self, password=password) - - def format(self, filesystem :Optional[str] = None, path :Optional[str] = None, log_formatting :bool = True, options :List[str] = [], retry :bool = True) -> bool: - """ - Format can be given an overriding path, for instance /dev/null to test - the formatting functionality and in essence the support for the given filesystem. - """ - if filesystem is None: - filesystem = self._partition_info.filesystem_type - - if path is None: - path = self._path - - # This converts from fat32 -> vfat to unify filesystem names - filesystem = get_mount_fs_type(filesystem) - - # To avoid "unable to open /dev/x: No such file or directory" - start_wait = time.time() - while Path(path).exists() is False and time.time() - start_wait < 10: - time.sleep(0.025) - - if log_formatting: - log(f'Formatting {path} -> {filesystem}', level=logging.INFO) - - try: - if filesystem == 'btrfs': - options = ['-f'] + options - - mkfs = SysCommand(f"/usr/bin/mkfs.btrfs {' '.join(options)} {path}").decode('UTF-8') - if mkfs and 'UUID:' not in mkfs: - raise DiskError(f'Could not format {path} with {filesystem} because: {mkfs}') - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'vfat': - options = ['-F32'] + options - log(f"/usr/bin/mkfs.vfat {' '.join(options)} {path}") - if (handle := SysCommand(f"/usr/bin/mkfs.vfat {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'ext4': - options = ['-F'] + options - - if (handle := SysCommand(f"/usr/bin/mkfs.ext4 {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'ext2': - options = ['-F'] + options - - if (handle := SysCommand(f"/usr/bin/mkfs.ext2 {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = 'ext2' - elif filesystem == 'xfs': - options = ['-f'] + options - - if (handle := SysCommand(f"/usr/bin/mkfs.xfs {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'f2fs': - options = ['-f'] + options - - if (handle := SysCommand(f"/usr/bin/mkfs.f2fs {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'ntfs3': - options = ['-f'] + options - - if (handle := SysCommand(f"/usr/bin/mkfs.ntfs -Q {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'crypto_LUKS': - # from ..luks import luks2 - # encrypted_partition = luks2(self, None, None) - # encrypted_partition.format(path) - self._partition_info.filesystem_type = filesystem - - else: - raise UnknownFilesystemFormat(f"Fileformat '{filesystem}' is not yet implemented.") - except SysCallError as error: - log(f"Formatting ran in to an error: {error}", level=logging.WARNING, fg="orange") - if retry is True: - log(f"Retrying in {storage.get('DISK_TIMEOUTS', 1)} seconds.", level=logging.WARNING, fg="orange") - time.sleep(storage.get('DISK_TIMEOUTS', 1)) - - return self.format(filesystem, path, log_formatting, options, retry=False) - - if get_filesystem_type(path) == 'crypto_LUKS' or get_filesystem_type(self.real_device) == 'crypto_LUKS': - self._encrypted = True - else: - self._encrypted = False - - return True - - def find_parent_of(self, data :Dict[str, Any], name :str, parent :Optional[str] = None) -> Optional[str]: - if data['name'] == name: - return parent - elif 'children' in data: - for child in data['children']: - if parent := self.find_parent_of(child, name, parent=data['name']): - return parent - - return None - - def mount(self, target :str, fs :Optional[str] = None, options :str = '') -> bool: - if not self._partition_info.get_first_mountpoint(): - log(f'Mounting {self} to {target}', level=logging.INFO) - - if not fs: - fs = self._partition_info.filesystem_type - - fs_type = get_mount_fs_type(fs) - - Path(target).mkdir(parents=True, exist_ok=True) - - if self.bind_name: - device_path = self.device_path - # TODO options should be better be a list than a string - if options: - options = f"{options},subvol={self.bind_name}" - else: - options = f"subvol={self.bind_name}" - else: - device_path = self._path - try: - if options: - mnt_handle = SysCommand(f"/usr/bin/mount -t {fs_type} -o {options} {device_path} {target}") - else: - mnt_handle = SysCommand(f"/usr/bin/mount -t {fs_type} {device_path} {target}") - - # TODO: Should be redundant to check for exit_code - if mnt_handle.exit_code != 0: - raise DiskError(f"Could not mount {self._path} to {target} using options {options}") - except SysCallError as err: - raise err - - # Update the partition info since the mount info has changed after this call. - self._partition_info = self._fetch_information() - return True - - return False - - def unmount(self) -> bool: - SysCommand(f"/usr/bin/umount {self._path}") - - # Update the partition info since the mount info has changed after this call. - self._partition_info = self._fetch_information() - return True - - def filesystem_supported(self) -> bool: - """ - The support for a filesystem (this partition) is tested by calling - partition.format() with a path set to '/dev/null' which returns two exceptions: - 1. SysCallError saying that /dev/null is not formattable - but the filesystem is supported - 2. UnknownFilesystemFormat that indicates that we don't support the given filesystem type - """ - try: - self.format(self._partition_info.filesystem_type, '/dev/null', log_formatting=False) - except (SysCallError, DiskError): - pass # We supported it, but /dev/null is not formattable as expected so the mkfs call exited with an error code - except UnknownFilesystemFormat as err: - raise err - return True - - -def get_mount_fs_type(fs :str) -> str: - if fs == 'ntfs': - return 'ntfs3' # Needed to use the Paragon R/W NTFS driver - elif fs == 'fat32': - return 'vfat' # This is the actual type used for fat32 mounting - return fs diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py new file mode 100644 index 00000000..686e8c29 --- /dev/null +++ b/archinstall/lib/disk/partitioning_menu.py @@ -0,0 +1,335 @@ +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 .subvolume_menu import SubvolumeMenu + +if TYPE_CHECKING: + _: Any + + +class PartitioningList(ListManager): + """ + subclass of ListManager for the managing of user accounts + """ + def __init__(self, prompt: str, device: BDevice, device_partitions: List[PartitionModification]): + self._device = device + self._actions = { + 'create_new_partition': str(_('Create a new partition')), + 'suggest_partition_layout': str(_('Suggest partition layout')), + 'remove_added_partitions': str(_('Remove all newly added partitions')), + 'assign_mountpoint': str(_('Assign mountpoint')), + 'mark_formatting': str(_('Mark/Unmark to be formatted (wipes data)')), + 'mark_bootable': str(_('Mark/Unmark as bootable')), + 'set_filesystem': str(_('Change filesystem')), + 'btrfs_mark_compressed': str(_('Mark/Unmark as compressed')), # btrfs only + 'btrfs_set_subvolumes': str(_('Set subvolumes')), # btrfs only + 'delete_partition': str(_('Delete partition')) + } + + display_actions = list(self._actions.values()) + super().__init__(prompt, device_partitions, display_actions[:2], display_actions[3:]) + + def reformat(self, data: List[PartitionModification]) -> Dict[str, Optional[PartitionModification]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[PartitionModification]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, user in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = user + + return display_data + + def selected_action_display(self, partition: PartitionModification) -> str: + return str(_('Partition')) + + def filter_options(self, selection: PartitionModification, options: List[str]) -> List[str]: + not_filter = [] + + # only display formatting if the partition exists already + if not selection.exists(): + not_filter += [self._actions['mark_formatting']] + else: + # only allow these options if the existing partition + # was marked as formatting, otherwise we run into issues where + # 1. select a new fs -> potentially mark as wipe now + # 2. Switch back to old filesystem -> should unmark wipe now, but + # how do we know it was the original one? + not_filter += [ + self._actions['set_filesystem'], + self._actions['assign_mountpoint'], + self._actions['mark_bootable'], + self._actions['btrfs_mark_compressed'], + self._actions['btrfs_set_subvolumes'] + ] + + # non btrfs partitions shouldn't get btrfs options + if selection.fs_type != FilesystemType.Btrfs: + not_filter += [self._actions['btrfs_mark_compressed'], self._actions['btrfs_set_subvolumes']] + else: + not_filter += [self._actions['assign_mountpoint']] + + return [o for o in options if o not in not_filter] + + def handle_action( + self, + action: str, + entry: Optional[PartitionModification], + data: List[PartitionModification] + ) -> List[PartitionModification]: + action_key = [k for k, v in self._actions.items() if v == action][0] + + match action_key: + case 'create_new_partition': + new_partition = self._create_new_partition() + data += [new_partition] + case 'suggest_partition_layout': + new_partitions = self._suggest_partition_layout(data) + if len(new_partitions) > 0: + data = new_partitions + case 'remove_added_partitions': + choice = self._reset_confirmation() + if choice.value == Menu.yes(): + data = [part for part in data if part.is_exists_or_modify()] + case 'assign_mountpoint' if entry: + entry.mountpoint = self._prompt_mountpoint() + if entry.mountpoint == Path('/boot'): + entry.set_flag(PartitionFlag.Boot) + case 'mark_formatting' if entry: + self._prompt_formatting(entry) + case 'mark_bootable' if entry: + entry.invert_flag(PartitionFlag.Boot) + case 'set_filesystem' if entry: + fs_type = self._prompt_partition_fs_type() + if fs_type: + entry.fs_type = fs_type + # btrfs subvolumes will define mountpoints + if fs_type == FilesystemType.Btrfs: + entry.mountpoint = None + case 'btrfs_mark_compressed' if entry: + self._set_compressed(entry) + case 'btrfs_set_subvolumes' if entry: + self._set_btrfs_subvolumes(entry) + case 'delete_partition' if entry: + data = self._delete_partition(entry, data) + + return data + + def _delete_partition( + self, + entry: PartitionModification, + data: List[PartitionModification] + ) -> List[PartitionModification]: + if entry.is_exists_or_modify(): + entry.status = ModificationStatus.Delete + return data + else: + return [d for d in data if d != entry] + + def _set_compressed(self, partition: PartitionModification): + compression = 'compress=zstd' + + if compression in partition.mount_options: + partition.mount_options = [o for o in partition.mount_options if o != compression] + else: + partition.mount_options.append(compression) + + def _set_btrfs_subvolumes(self, partition: PartitionModification): + partition.btrfs_subvols = SubvolumeMenu( + _("Manage btrfs subvolumes for current partition"), + partition.btrfs_subvols + ).run() + + def _prompt_formatting(self, partition: PartitionModification): + # an existing partition can toggle between Exist or Modify + if partition.is_modify(): + partition.status = ModificationStatus.Exist + return + elif partition.exists(): + partition.status = ModificationStatus.Modify + + # If we mark a partition for formatting, but the format is CRYPTO LUKS, there's no point in formatting it really + # without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set, + # it's safe to change the filesystem for this partition. + if partition.fs_type == FilesystemType.Crypto_luks: + prompt = str(_('This partition is currently encrypted, to format it a filesystem has to be specified')) + fs_type = self._prompt_partition_fs_type(prompt) + partition.fs_type = fs_type + + if fs_type == FilesystemType.Btrfs: + partition.mountpoint = None + + def _prompt_mountpoint(self) -> Path: + header = str(_('Partition mount-points are relative to inside the installation, the boot would be /boot as an example.')) + '\n' + header += str(_('If mountpoint /boot is set, then the partition will also be marked as bootable.')) + '\n' + prompt = str(_('Mountpoint: ')) + + print(header) + + while True: + value = TextInput(prompt).run().strip() + + if value: + mountpoint = Path(value) + break + + return mountpoint + + def _prompt_partition_fs_type(self, prompt: str = '') -> FilesystemType: + options = {fs.value: fs for fs in FilesystemType if fs != FilesystemType.Crypto_luks} + + prompt = prompt + '\n' + str(_('Enter a desired filesystem type for the partition')) + choice = Menu(prompt, options, sort=False, skip=False).run() + return options[choice.single_value] + + def _validate_sector(self, start_sector: str, end_sector: Optional[str] = None) -> bool: + if not start_sector.isdigit(): + return False + + if end_sector: + if end_sector.endswith('%'): + if not end_sector[:-1].isdigit(): + return False + elif not end_sector.isdigit(): + return False + elif int(start_sector) > int(end_sector): + return False + + return True + + def _prompt_sectors(self) -> Tuple[Size, Size]: + device_info = self._device.device_info + + text = str(_('Current free sectors on device {}:')).format(device_info.path) + '\n\n' + free_space_table = FormattedOutput.as_table(device_info.free_space_regions) + prompt = text + free_space_table + '\n' + + total_sectors = device_info.total_size.format_size(Unit.sectors, device_info.sector_size) + prompt += str(_('Total sectors: {}')).format(total_sectors) + '\n' + print(prompt) + + largest_free_area = max(device_info.free_space_regions, key=lambda r: r.get_length()) + + # prompt until a valid start sector was entered + while True: + start_prompt = str(_('Enter the start sector (default: {}): ')).format(largest_free_area.start) + start_sector = TextInput(start_prompt).run().strip() + + if not start_sector or self._validate_sector(start_sector): + break + + log(f'Invalid start sector entered: {start_sector}', fg='red', level=logging.INFO) + + if not start_sector: + start_sector = str(largest_free_area.start) + end_sector = str(largest_free_area.end) + else: + end_sector = '100%' + + # prompt until valid end sector was entered + while True: + end_prompt = str(_('Enter the end sector of the partition (percentage or block number, default: {}): ')).format(end_sector) + end_value = TextInput(end_prompt).run().strip() + + 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) + + # override the default value with the user value + if end_value: + end_sector = end_value + + start_size = Size(int(start_sector), Unit.sectors, device_info.sector_size) + + if end_sector.endswith('%'): + end_size = Size(int(end_sector[:-1]), Unit.Percent, device_info.sector_size, device_info.total_size) + else: + end_size = Size(int(end_sector), Unit.sectors, device_info.sector_size) + + return start_size, end_size + + def _create_new_partition(self) -> PartitionModification: + fs_type = self._prompt_partition_fs_type() + + start_size, end_size = self._prompt_sectors() + length = end_size - start_size + + # new line for the next prompt + print() + + mountpoint = None + if fs_type != FilesystemType.Btrfs: + mountpoint = self._prompt_mountpoint() + + partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, + start=start_size, + length=length, + fs_type=fs_type, + mountpoint=mountpoint + ) + + if partition.mountpoint == Path('/boot'): + partition.set_flag(PartitionFlag.Boot) + + return partition + + def _reset_confirmation(self) -> MenuSelection: + prompt = str(_('This will remove all newly added partitions, continue?')) + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run() + return choice + + def _suggest_partition_layout(self, data: List[PartitionModification]) -> List[PartitionModification]: + # if modifications have been done already, inform the user + # that this operation will erase those modifications + if any([not entry.exists() for entry in data]): + choice = self._reset_confirmation() + if choice.value == Menu.no(): + return [] + + from ..user_interaction.disk_conf import suggest_single_disk_layout + + device_modification = suggest_single_disk_layout(self._device) + return device_modification.partitions + + +def manual_partitioning( + device: BDevice, + prompt: str = '', + preset: List[PartitionModification] = [] +) -> List[PartitionModification]: + if not prompt: + prompt = str(_('Partition management: {}')).format(device.device_info.path) + '\n' + prompt += str(_('Total length: {}')).format(device.device_info.total_size.format_size(Unit.MiB)) + + manual_preset = [] + + if not preset: + # we'll display the existing partitions of the device + for partition in device.partition_infos: + manual_preset.append( + PartitionModification.from_existing_partition(partition) + ) + else: + manual_preset = preset + + menu_list = PartitioningList(prompt, device, manual_preset) + partitions = menu_list.run() + + if menu_list.is_last_choice_cancel(): + return preset + + return partitions diff --git a/archinstall/lib/disk/subvolume_menu.py b/archinstall/lib/disk/subvolume_menu.py new file mode 100644 index 00000000..32a0e616 --- /dev/null +++ b/archinstall/lib/disk/subvolume_menu.py @@ -0,0 +1,101 @@ +from pathlib import Path +from typing import Dict, List, Optional, Any, TYPE_CHECKING + +from .device_model import SubvolumeModification +from ..menu import Menu, TextInput, MenuSelectionType, ListManager +from ..output import FormattedOutput + +if TYPE_CHECKING: + _: Any + + +class SubvolumeMenu(ListManager): + def __init__(self, prompt: str, btrfs_subvols: List[SubvolumeModification]): + self._actions = [ + str(_('Add subvolume')), + str(_('Edit subvolume')), + str(_('Delete subvolume')) + ] + super().__init__(prompt, btrfs_subvols, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[SubvolumeModification]) -> Dict[str, Optional[SubvolumeModification]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[SubvolumeModification]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, subvol in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = subvol + + return display_data + + def selected_action_display(self, subvolume: SubvolumeModification) -> str: + return str(subvolume.name) + + def _prompt_options(self, editing: Optional[SubvolumeModification] = None) -> List[str]: + preset_options = [] + if editing: + preset_options = editing.mount_options + + choice = Menu( + str(_("Select the desired subvolume options ")), + ['nodatacow', 'compress'], + skip=True, + preset_values=preset_options, + multi=True + ).run() + + if choice.type_ == MenuSelectionType.Selection: + return choice.value # type: ignore + + return [] + + def _add_subvolume(self, editing: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]: + name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run() + + if not name: + return None + + mountpoint = TextInput(f'{_("Subvolume mountpoint")}: ', str(editing.mountpoint) if editing else '').run() + + if not mountpoint: + return None + + options = self._prompt_options(editing) + + subvolume = SubvolumeModification(Path(name), Path(mountpoint)) + subvolume.compress = 'compress' in options + subvolume.nodatacow = 'nodatacow' in options + + return subvolume + + def handle_action( + self, + action: str, + entry: Optional[SubvolumeModification], + data: List[SubvolumeModification] + ) -> List[SubvolumeModification]: + if action == self._actions[0]: # add + new_subvolume = self._add_subvolume() + + if new_subvolume is not None: + # in case a user with the same username as an existing user + # was created we'll replace the existing one + data = [d for d in data if d.name != new_subvolume.name] + data += [new_subvolume] + elif entry is not None: + if action == self._actions[1]: # edit subvolume + new_subvolume = self._add_subvolume(entry) + + if new_subvolume is not None: + # we'll remove the original subvolume and add the modified version + data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name] + data += [new_subvolume] + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] + + return data diff --git a/archinstall/lib/disk/user_guides.py b/archinstall/lib/disk/user_guides.py deleted file mode 100644 index 5809c073..00000000 --- a/archinstall/lib/disk/user_guides.py +++ /dev/null @@ -1,240 +0,0 @@ -from __future__ import annotations -import logging -from typing import Optional, Dict, Any, List, TYPE_CHECKING - -# https://stackoverflow.com/a/39757388/929999 -from ..models.subvolume import Subvolume - -if TYPE_CHECKING: - from .blockdevice import BlockDevice - _: Any - -from .helpers import sort_block_devices_based_on_performance, select_largest_device, select_disk_larger_than_or_close_to -from ..hardware import has_uefi -from ..output import log -from ..menu import Menu - - -def suggest_single_disk_layout(block_device :BlockDevice, - default_filesystem :Optional[str] = None, - advanced_options :bool = False) -> Dict[str, Any]: - - if not default_filesystem: - from ..user_interaction import ask_for_main_filesystem_format - default_filesystem = ask_for_main_filesystem_format(advanced_options) - - MIN_SIZE_TO_ALLOW_HOME_PART = 40 # GiB - using_subvolumes = False - using_home_partition = False - compression = False - - if default_filesystem == '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() - - layout = { - block_device.path : { - "wipe" : True, - "partitions" : [] - } - } - - # 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. - layout[block_device.path]['partitions'].append({ - # Boot - "type" : "primary", - "start" : "3MiB", - "size" : "203MiB", - "boot" : True, - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/boot", - "filesystem" : { - "format" : "fat32" - } - }) - - # 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. - if has_uefi(): - layout[block_device.path]['partitions'][-1]['start'] = '1MiB' - layout[block_device.path]['partitions'][-1]['size'] = '512MiB' - - layout[block_device.path]['partitions'].append({ - # Root - "type" : "primary", - "start" : "206MiB", - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/" if not using_subvolumes else None, - "filesystem" : { - "format" : default_filesystem, - "mount_options" : ["compress=zstd"] if compression else [] - } - }) - - if has_uefi(): - layout[block_device.path]['partitions'][-1]['start'] = '513MiB' - - if not using_subvolumes and block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART: - 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() - - # Set a size for / (/root) - if using_subvolumes or block_device.size < MIN_SIZE_TO_ALLOW_HOME_PART or not using_home_partition: - # We'll use subvolumes - # Or the disk size is too small to allow for a separate /home - # Or the user doesn't want to create a separate partition for /home - layout[block_device.path]['partitions'][-1]['size'] = '100%' - else: - layout[block_device.path]['partitions'][-1]['size'] = f"{min(block_device.size, 20)}GiB" - - if default_filesystem == 'btrfs' and using_subvolumes: - # if input('Do you want to use a recommended structure? (Y/n): ').strip().lower() in ('', 'y', 'yes'): - # 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 - layout[block_device.path]['partitions'][1]['btrfs'] = { - 'subvolumes': [ - Subvolume('@', '/'), - Subvolume('@home', '/home'), - Subvolume('@log', '/var/log'), - Subvolume('@pkg', '/var/cache/pacman/pkg'), - Subvolume('@.snapshots', '/.snapshots') - ] - } - 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 - layout[block_device.path]['partitions'].append({ - # Home - "type" : "primary", - "start" : f"{min(block_device.size, 20)}GiB", - "size" : "100%", - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/home", - "filesystem" : { - "format" : default_filesystem, - "mount_options" : ["compress=zstd"] if compression else [] - } - }) - - return layout - - -def suggest_multi_disk_layout(block_devices :List[BlockDevice], default_filesystem :Optional[str] = None, advanced_options :bool = False): - - if not default_filesystem: - from ..user_interaction import ask_for_main_filesystem_format - default_filesystem = ask_for_main_filesystem_format(advanced_options) - - # 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_SIZE_TO_ALLOW_HOME_PART = 40 # GiB - ARCH_LINUX_INSTALLED_SIZE = 20 # GiB, rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? - - block_devices = sort_block_devices_based_on_performance(block_devices).keys() - - home_device = select_largest_device(block_devices, gigabytes=MIN_SIZE_TO_ALLOW_HOME_PART) - root_device = select_disk_larger_than_or_close_to(block_devices, gigabytes=ARCH_LINUX_INSTALLED_SIZE, filter_out=[home_device]) - - 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: {}GB\n').format(MIN_SIZE_TO_ALLOW_HOME_PART) - text += _('Minimum capacity for Arch Linux partition: {}GB').format(ARCH_LINUX_INSTALLED_SIZE) - Menu(str(text), [str(_('Continue'))], skip=False).run() - return None - - compression = False - - if default_filesystem == 'btrfs': - # prompt = 'Would you like to use BTRFS subvolumes with a default structure?' - # choice = Menu(prompt, ['yes', 'no'], skip=False, default_option='yes').run() - # using_subvolumes = choice == '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() - - log(f"Suggesting multi-disk-layout using {len(block_devices)} disks, where {root_device} will be /root and {home_device} will be /home", level=logging.DEBUG) - - layout = { - root_device.path : { - "wipe" : True, - "partitions" : [] - }, - home_device.path : { - "wipe" : True, - "partitions" : [] - }, - } - - # TODO: Same deal as with the single disk layout, we should - # probably check if the drive will be encrypted. - layout[root_device.path]['partitions'].append({ - # Boot - "type" : "primary", - "start" : "3MiB", - "size" : "203MiB", - "boot" : True, - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/boot", - "filesystem" : { - "format" : "fat32" - } - }) - - if has_uefi(): - layout[root_device.path]['partitions'][-1]['start'] = '1MiB' - layout[root_device.path]['partitions'][-1]['size'] = '512MiB' - - layout[root_device.path]['partitions'].append({ - # Root - "type" : "primary", - "start" : "206MiB", - "size" : "100%", - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/", - "filesystem" : { - "format" : default_filesystem, - "mount_options" : ["compress=zstd"] if compression else [] - } - }) - if has_uefi(): - layout[root_device.path]['partitions'][-1]['start'] = '513MiB' - - layout[home_device.path]['partitions'].append({ - # Home - "type" : "primary", - "start" : "1MiB", - "size" : "100%", - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/home", - "filesystem" : { - "format" : default_filesystem, - "mount_options" : ["compress=zstd"] if compression else [] - } - }) - - return layout diff --git a/archinstall/lib/disk/validators.py b/archinstall/lib/disk/validators.py deleted file mode 100644 index 076a8ba2..00000000 --- a/archinstall/lib/disk/validators.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import List - -def valid_parted_position(pos :str) -> bool: - if not len(pos): - return False - - if pos.isdigit(): - return True - - pos_lower = pos.lower() - - if (pos_lower.endswith('b') or pos_lower.endswith('s')) and pos[:-1].isdigit(): - return True - - if any(pos_lower.endswith(size) and pos[:-len(size)].replace(".", "", 1).isdigit() - for size in ['%', 'kb', 'mb', 'gb', 'tb', 'kib', 'mib', 'gib', 'tib']): - return True - - return False - - -def fs_types() -> List[str]: - # https://www.gnu.org/software/parted/manual/html_node/mkpart.html - # Above link doesn't agree with `man parted` /mkpart documentation: - """ - fs-type can - be one of "btrfs", "ext2", - "ext3", "ext4", "fat16", - "fat32", "hfs", "hfs+", - "linux-swap", "ntfs", "reis‐ - erfs", "udf", or "xfs". - """ - return [ - "btrfs", - "ext2", - "ext3", "ext4", # `man parted` allows these - "fat16", "fat32", - "hfs", "hfs+", # "hfsx", not included in `man parted` - "linux-swap", - "ntfs", - "reiserfs", - "udf", # "ufs", not included in `man parted` - "xfs", # `man parted` allows this - ] - - -def valid_fs_type(fstype :str) -> bool: - return fstype.lower() in fs_types() diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 79ab024b..57f13288 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -1,4 +1,5 @@ from __future__ import annotations + import hashlib import json import logging @@ -17,7 +18,7 @@ import urllib.error import pathlib from datetime import datetime, date from typing import Callable, Optional, Dict, Any, List, Union, Iterator, TYPE_CHECKING -# https://stackoverflow.com/a/39757388/929999 + if TYPE_CHECKING: from .installer import Installer @@ -140,7 +141,7 @@ class JsonEncoder: return obj.isoformat() elif isinstance(obj, (list, set, tuple)): return [json.loads(json.dumps(item, cls=JSON)) for item in obj] - elif isinstance(obj, (pathlib.Path)): + elif isinstance(obj, pathlib.Path): return str(obj) else: return obj @@ -184,22 +185,21 @@ class UNSAFE_JSON(json.JSONEncoder, json.JSONDecoder): def encode(self, obj :Any) -> Any: return super(UNSAFE_JSON, self).encode(self._encode(obj)) + class SysCommandWorker: - def __init__(self, + def __init__( + self, cmd :Union[str, List[str]], callbacks :Optional[Dict[str, Any]] = None, peek_output :Optional[bool] = False, - peak_output :Optional[bool] = False, environment_vars :Optional[Dict[str, Any]] = None, logfile :Optional[None] = None, working_directory :Optional[str] = './', - remove_vt100_escape_codes_from_lines :bool = True): - - if peak_output: - log("SysCommandWorker()'s peak_output is deprecated, use peek_output instead.", level=logging.WARNING, fg='red') - + remove_vt100_escape_codes_from_lines :bool = True + ): if not callbacks: callbacks = {} + if not environment_vars: environment_vars = {} @@ -216,8 +216,6 @@ class SysCommandWorker: self.cmd = cmd self.callbacks = callbacks self.peek_output = peek_output - if not self.peek_output and peak_output: - self.peek_output = peak_output # define the standard locale for command outputs. For now the C ascii one. Can be overridden self.environment_vars = {**storage.get('CMD_LOCALE',{}),**environment_vars} self.logfile = logfile @@ -396,7 +394,7 @@ class SysCommandWorker: os.chmod(str(history_logfile), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) except PermissionError: pass - # If history_logfile does not exist, ignore the error + # If history_logfile does not exist, ignore the error except FileNotFoundError: pass except Exception as e: @@ -431,14 +429,10 @@ class SysCommand: callbacks :Optional[Dict[str, Callable[[Any], Any]]] = None, start_callback :Optional[Callable[[Any], Any]] = None, peek_output :Optional[bool] = False, - peak_output :Optional[bool] = False, environment_vars :Optional[Dict[str, Any]] = None, working_directory :Optional[str] = './', remove_vt100_escape_codes_from_lines :bool = True): - if peak_output: - log("SysCommandWorker()'s peak_output is deprecated, use peek_output instead.", level=logging.WARNING, fg='red') - _callbacks = {} if callbacks: for hook, func in callbacks.items(): @@ -449,8 +443,6 @@ class SysCommand: self.cmd = cmd self._callbacks = _callbacks self.peek_output = peek_output - if not self.peek_output and peak_output: - self.peek_output = peak_output self.environment_vars = environment_vars self.working_directory = working_directory self.remove_vt100_escape_codes_from_lines = remove_vt100_escape_codes_from_lines @@ -575,9 +567,8 @@ def run_custom_user_commands(commands :List[str], installation :Installer) -> No with open(f"{installation.target}/var/tmp/user-command.{index}.sh", "w") as temp_script: temp_script.write(command) - execution_output = SysCommand(f"arch-chroot {installation.target} bash /var/tmp/user-command.{index}.sh") + SysCommand(f"arch-chroot {installation.target} bash /var/tmp/user-command.{index}.sh") - log(execution_output) os.unlink(f"{installation.target}/var/tmp/user-command.{index}.sh") def json_stream_to_structure(configuration_identifier : str, stream :str, target :dict) -> bool : diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py new file mode 100644 index 00000000..bc9164ee --- /dev/null +++ b/archinstall/lib/global_menu.py @@ -0,0 +1,364 @@ +from __future__ import annotations + +from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING + +from . import disk +from .general import SysCommand, secret +from .menu import Selector, AbstractMenu +from .models import NetworkConfiguration +from .models.bootloader import Bootloader +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 + +if TYPE_CHECKING: + _: Any + + +class GlobalMenu(AbstractMenu): + def __init__(self, data_store: Dict[str, Any]): + super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3) + + def setup_selection_menu_options(self): + # archinstall.Language will not use preset values + self._menu_options['archinstall-language'] = \ + Selector( + _('Archinstall language'), + lambda x: self._select_archinstall_language(x), + display_func=lambda x: x.display_name, + default=self.translation_handler.get_language_by_abbr('en')) + self._menu_options['keyboard-layout'] = \ + Selector( + _('Keyboard layout'), + lambda preset: select_language(preset), + default='us') + self._menu_options['mirror-region'] = \ + Selector( + _('Mirror region'), + lambda preset: select_mirror_regions(preset), + display_func=lambda x: list(x.keys()) if x else '[]', + default={}) + 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'), + lambda preset: self._select_disk_config(preset), + preview_func=self._prev_disk_layouts, + display_func=lambda x: self._display_disk_layout(x), + ) + self._menu_options['disk_encryption'] = \ + Selector( + _('Disk encryption'), + lambda preset: self._disk_encryption(preset), + preview_func=self._prev_disk_encryption, + display_func=lambda x: self._display_disk_encryption(x), + dependencies=['disk_config']) + self._menu_options['swap'] = \ + Selector( + _('Swap'), + lambda preset: ask_for_swap(preset), + default=True) + self._menu_options['bootloader'] = \ + Selector( + _('Bootloader'), + lambda preset: ask_for_bootloader(preset), + display_func=lambda x: x.value, + default=Bootloader.get_default()) + self._menu_options['hostname'] = \ + Selector( + _('Hostname'), + lambda preset: ask_hostname(preset), + default='archlinux') + # root password won't have preset value + self._menu_options['!root-password'] = \ + Selector( + _('Root password'), + lambda preset:self._set_root_password(), + display_func=lambda x: secret(x) if x else 'None') + 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, + 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', + 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', + default=None + ) + self._menu_options['parallel downloads'] = \ + Selector( + _('Parallel Downloads'), + add_number_of_parrallel_downloads, + display_func=lambda x: x if x else '0', + default=0 + ) + self._menu_options['kernels'] = \ + Selector( + _('Kernels'), + lambda preset: select_kernel(preset), + display_func=lambda x: ', '.join(x) if x else None, + default=['linux']) + self._menu_options['packages'] = \ + Selector( + _('Additional packages'), + # lambda x: ask_additional_packages_to_install(storage['arguments'].get('packages', None)), + ask_additional_packages_to_install, + default=[]) + self._menu_options['additional-repositories'] = \ + Selector( + _('Optional repositories'), + select_additional_repositories, + display_func=lambda x: ', '.join(x) if x else None, + default=[]) + self._menu_options['nic'] = \ + Selector( + _('Network configuration'), + ask_to_configure_network, + display_func=lambda x: self._display_network_conf(x), + preview_func=self._prev_network_config, + default={}) + self._menu_options['timezone'] = \ + Selector( + _('Timezone'), + lambda preset: ask_for_a_timezone(preset), + default='UTC') + self._menu_options['ntp'] = \ + Selector( + _('Automatic time sync (NTP)'), + lambda preset: self._select_ntp(preset), + default=True) + self._menu_options['__separator__'] = \ + Selector('') + self._menu_options['save_config'] = \ + Selector( + _('Save configuration'), + lambda preset: save_config(self._data_store), + no_store=True) + self._menu_options['install'] = \ + Selector( + self._install_text(), + exec_func=lambda n,v: True if len(self._missing_configs()) == 0 else False, + preview_func=self._prev_install_missing_config, + no_store=True) + + self._menu_options['abort'] = Selector(_('Abort'), exec_func=lambda n,v:exit(1)) + + def _update_install_text(self, name: str, value: str): + text = self._install_text() + self._menu_options['install'].update_description(text) + + def post_callback(self, name: str, value: str): + self._update_install_text(name, value) + + def _install_text(self): + missing = len(self._missing_configs()) + if missing > 0: + return _('Install ({} config(s) missing)').format(missing) + return _('Install') + + def _display_network_conf(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str: + if not cur_value: + return _('Not configured, unavailable unless setup manually') + else: + if isinstance(cur_value, list): + return str(_('Configured {} interfaces')).format(len(cur_value)) + else: + return str(cur_value) + + def _disk_encryption(self, preset: Optional[disk.DiskEncryption]) -> Optional[disk.DiskEncryption]: + mods: Optional[List[disk.DeviceModification]] = self._menu_options['disk_config'].current_selection + + if not mods: + # this should not happen as the encryption menu has the disk_config as dependency + raise ValueError('No disk layout specified') + + data_store: Dict[str, Any] = {} + disk_encryption = disk.DiskEncryptionMenu(mods, data_store, preset=preset).run() + return disk_encryption + + def _prev_network_config(self) -> Optional[str]: + selector = self._menu_options['nic'] + if selector.has_selection(): + ifaces = selector.current_selection + if isinstance(ifaces, list): + return FormattedOutput.as_table(ifaces) + return None + + def _prev_disk_layouts(self) -> Optional[str]: + selector = self._menu_options['disk_config'] + disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = selector.current_selection + + if disk_layout_conf: + device_mods: List[disk.DeviceModification] = \ + list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications)) + + if device_mods: + output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg()) + output_btrfs = '' + + for mod in device_mods: + # create partition table + partition_table = FormattedOutput.as_table(mod.partitions) + + output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n' + output_partition += partition_table + '\n' + + # create btrfs table + btrfs_partitions = list( + filter(lambda p: len(p.btrfs_subvols) > 0, mod.partitions) + ) + for partition in btrfs_partitions: + output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n' + + output = output_partition + output_btrfs + return output.rstrip() + + return None + + def _display_disk_layout(self, current_value: Optional[disk.DiskLayoutConfiguration] = None) -> str: + if current_value: + return current_value.config_type.display_msg() + return '' + + def _prev_disk_encryption(self) -> Optional[str]: + encryption: Optional[disk.DiskEncryption] = self._menu_options['disk_encryption'].current_selection + if encryption: + enc_type = disk.EncryptionType.type_to_text(encryption.encryption_type) + output = str(_('Encryption type')) + f': {enc_type}\n' + output += str(_('Password')) + f': {secret(encryption.encryption_password)}\n' + + if encryption.partitions: + output += 'Partitions: {} selected'.format(len(encryption.partitions)) + '\n' + + if encryption.hsm_device: + output += f'HSM: {encryption.hsm_device.manufacturer}' + + return output + + return None + + def _display_disk_encryption(self, current_value: Optional[disk.DiskEncryption]) -> str: + if current_value: + return disk.EncryptionType.type_to_text(current_value.encryption_type) + return '' + + def _prev_install_missing_config(self) -> Optional[str]: + if missing := self._missing_configs(): + text = str(_('Missing configurations:\n')) + for m in missing: + text += f'- {m}\n' + return text[:-1] # remove last new line + return None + + def _prev_users(self) -> Optional[str]: + selector = self._menu_options['!users'] + users: Optional[List[User]] = selector.current_selection + + if users: + return FormattedOutput.as_table(users) + return None + + def _prev_profile(self) -> Optional[str]: + selector = self._menu_options['profile_config'] + profile_config: Optional[ProfileConfiguration] = selector.current_selection + + if profile_config and profile_config.profile: + output = str(_('Profiles')) + ': ' + if profile_names := profile_config.profile.current_selection_names(): + output += ', '.join(profile_names) + '\n' + else: + output += profile_config.profile.name + '\n' + + if profile_config.gfx_driver: + output += str(_('Graphics driver')) + ': ' + profile_config.gfx_driver + '\n' + + if profile_config.greeter: + output += str(_('Greeter')) + ': ' + profile_config.greeter.value + '\n' + + return output + + return None + + def _set_root_password(self) -> Optional[str]: + prompt = str(_('Enter root password (leave blank to disable root): ')) + password = get_password(prompt=prompt) + return password + + def _select_ntp(self, preset :bool = True) -> bool: + ntp = ask_ntp(preset) + + value = str(ntp).lower() + SysCommand(f'timedatectl set-ntp {value}') + + return ntp + + def _select_disk_config( + self, + preset: Optional[disk.DiskLayoutConfiguration] = None + ) -> Optional[disk.DiskLayoutConfiguration]: + disk_config = select_disk_config( + preset, + storage['arguments'].get('advanced', False) + ) + + if disk_config != preset: + self._menu_options['disk_encryption'].set_current_selection(None) + + return disk_config + + def _select_profile(self, current_profile: Optional[ProfileConfiguration]): + from .profile.profile_menu import ProfileMenu + store: Dict[str, Any] = {} + profile_config = ProfileMenu(store, preset=current_profile).run() + return profile_config + + def _select_audio(self, current: Union[str, None]) -> Optional[str]: + profile_config: Optional[ProfileConfiguration] = self._menu_options['profile_config'].current_selection + if profile_config and profile_config.profile: + is_desktop = profile_config.profile.is_desktop_profile() if profile_config else False + selection = ask_for_audio_selection(is_desktop, current) + return selection + return None + + def _create_user_account(self, defined_users: List[User]) -> List[User]: + users = ask_for_additional_users(defined_users=defined_users) + return users diff --git a/archinstall/lib/hsm/__init__.py b/archinstall/lib/hsm/__init__.py deleted file mode 100644 index a3f64019..00000000 --- a/archinstall/lib/hsm/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .fido import Fido2 diff --git a/archinstall/lib/hsm/fido.py b/archinstall/lib/hsm/fido.py deleted file mode 100644 index 1c226322..00000000 --- a/archinstall/lib/hsm/fido.py +++ /dev/null @@ -1,109 +0,0 @@ -from __future__ import annotations - -import getpass -import logging - -from dataclasses import dataclass -from pathlib import Path -from typing import List, Dict - -from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes -from ..disk.partition import Partition -from ..general import log - - -@dataclass -class Fido2Device: - path: Path - manufacturer: str - product: str - - def json(self) -> Dict[str, str]: - return { - 'path': str(self.path), - 'manufacturer': self.manufacturer, - 'product': self.product - } - - @classmethod - def parse_arg(cls, arg: Dict[str, str]) -> 'Fido2Device': - return Fido2Device( - Path(arg['path']), - arg['manufacturer'], - arg['product'] - ) - - -class Fido2: - _loaded: bool = False - _fido2_devices: List[Fido2Device] = [] - - @classmethod - def get_fido2_devices(cls, reload: bool = False) -> List[Fido2Device]: - """ - Uses systemd-cryptenroll to list the FIDO2 devices - connected that supports FIDO2. - Some devices might show up in udevadm as FIDO2 compliant - when they are in fact not. - - The drawback of systemd-cryptenroll is that it uses human readable format. - That means we get this weird table like structure that is of no use. - - So we'll look for `MANUFACTURER` and `PRODUCT`, we take their index - and we split each line based on those positions. - - Output example: - - PATH MANUFACTURER PRODUCT - /dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID - """ - - # to prevent continous reloading which will slow - # down moving the cursor in the menu - if not cls._loaded or reload: - ret = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') - if not ret: - log('Unable to retrieve fido2 devices', level=logging.ERROR) - return [] - - fido_devices = clear_vt100_escape_codes(ret) - - manufacturer_pos = 0 - product_pos = 0 - devices = [] - - for line in fido_devices.split('\r\n'): - if '/dev' not in line: - manufacturer_pos = line.find('MANUFACTURER') - product_pos = line.find('PRODUCT') - continue - - path = line[:manufacturer_pos].rstrip() - manufacturer = line[manufacturer_pos:product_pos].rstrip() - product = line[product_pos:] - - devices.append( - Fido2Device(path, manufacturer, product) - ) - - cls._loaded = True - cls._fido2_devices = devices - - return cls._fido2_devices - - @classmethod - def fido2_enroll(cls, hsm_device: Fido2Device, partition :Partition, password :str): - worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device.path} {partition.real_device}", peek_output=True) - pw_inputted = False - pin_inputted = False - - while worker.is_alive(): - if pw_inputted is False and bytes(f"please enter current passphrase for disk {partition.real_device}", 'UTF-8') in worker._trace_log.lower(): - worker.write(bytes(password, 'UTF-8')) - pw_inputted = True - - elif pin_inputted is False and bytes(f"please enter security token pin", 'UTF-8') in worker._trace_log.lower(): - 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") diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index b4d253b3..ddbcc2f2 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1,30 +1,29 @@ -import time +import glob import logging import os import re -import shutil import shlex -import pathlib +import shutil import subprocess -import glob -from types import ModuleType -from typing import Union, Dict, Any, List, Optional, Iterator, Mapping, TYPE_CHECKING -from .disk import get_partitions_in_use, Partition -from .general import SysCommand, generate_password +import time +from pathlib import Path +from typing import Any, Iterator, List, Mapping, Optional, TYPE_CHECKING, Union, Dict + +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 .disk.helpers import findmnt +from .luks import Luks2 from .mirrors import use_mirrors -from .models.disk_encryption import DiskEncryption +from .models.bootloader import Bootloader +from .models.network_configuration import NetworkConfiguration +from .models.users import User +from .output import log +from .pacman import run_pacman from .plugins import plugins +from .services import service_state from .storage import storage -from .output import log -from .profiles import Profile -from .disk.partition import get_mount_fs_type -from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError -from .models.users import User -from .models.subvolume import Subvolume -from .hsm import Fido2 if TYPE_CHECKING: _: Any @@ -36,9 +35,6 @@ __packages__ = ["base", "base-devel", "linux-firmware", "linux", "linux-lts", "l # Additional packages that are installed if the user is running the Live ISO with accessibility tools enabled __accessibility_packages__ = ["brltty", "espeakup", "alsa-utils"] -from .pacman import run_pacman -from .models.network_configuration import NetworkConfiguration - class InstallationFile: def __init__(self, installation :'Installer', filename :str, owner :str, mode :str = "w"): @@ -92,26 +88,35 @@ class Installer: :param hostname: The given /etc/hostname for the machine. :type hostname: str, optional - """ - - def __init__(self, target :str, *, base_packages :Optional[List[str]] = None, kernels :Optional[List[str]] = None): - if base_packages is None: + def __init__( + self, + target: Path, + disk_config: disk.DiskLayoutConfiguration, + disk_encryption: Optional[disk.DiskEncryption] = None, + base_packages: List[str] = [], + kernels: Optional[List[str]] = None + ): + if not base_packages: base_packages = __packages__[:3] + if kernels is None: self.kernels = ['linux'] else: self.kernels = kernels + + self._disk_config = disk_config + self._disk_encryption = disk_encryption + + if self._disk_encryption is None: + self._disk_encryption = disk.DiskEncryption(disk.EncryptionType.NoEncryption) + self.target = target self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S') self.milliseconds = int(str(time.time()).split('.')[1]) + self.helper_flags = {'base': False, 'bootloader': False} + self.base_packages = base_packages - self.helper_flags = { - 'base': False, - 'bootloader': False - } - - self.base_packages = base_packages.split(' ') if type(base_packages) is str else base_packages for kernel in self.kernels: self.base_packages.append(kernel) @@ -136,19 +141,10 @@ class Installer: self._zram_enabled = False - self._disk_encryption: DiskEncryption = storage['arguments'].get('disk_encryption') - - 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 __enter__(self, *args :str, **kwargs :str) -> 'Installer': + def __enter__(self, *args: str, **kwargs: str) -> 'Installer': return self - def __exit__(self, *args :str, **kwargs :str) -> None: + def __exit__(self, *args :str, **kwargs :str) -> bool: # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager if len(args) >= 2 and args[1]: @@ -165,7 +161,6 @@ class Installer: if not (missing_steps := self.post_install_check()): self.log('Installation completed without any errors. You may now reboot.', fg='green', level=logging.INFO) 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) @@ -178,146 +173,168 @@ class Installer: self.sync_log_to_install_medium() return False - @property - def partitions(self) -> List[Partition]: - return get_partitions_in_use(self.target).values() + 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 sync_log_to_install_medium(self) -> bool: - # Copy over the install log (if there is one) to the install medium if - # at least the base has been strapped in, otherwise we won't have a filesystem/structure to copy to. - if self.helper_flags.get('base-strapped', False) is True: - if filename := storage.get('LOG_FILE', None): - absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename) + def _verify_service_stop(self): + """ + Certain services might be running that affects the system during installation. + Currently, only one such service is "reflector.service" which updates /etc/pacman.d/mirrorlist + We need to wait for it before we continue since we opted in to use a custom mirror/region. + """ + log('Waiting for automatic mirror selection (reflector) to complete...', level=logging.INFO) + while service_state('reflector') not in ('dead', 'failed', 'exited'): + time.sleep(1) - if not os.path.isdir(f"{self.target}/{os.path.dirname(absolute_logfile)}"): - os.makedirs(f"{self.target}/{os.path.dirname(absolute_logfile)}") + log('Waiting pacman-init.service to complete.', level=logging.INFO) + while service_state('pacman-init') not in ('dead', 'failed', 'exited'): + time.sleep(1) - shutil.copy2(absolute_logfile, f"{self.target}/{absolute_logfile}") + 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'): + time.sleep(1) - return True + def _verify_boot_part(self): + """ + Check that mounted /boot device has at minimum size for installation + The reason this check is here is to catch pre-mounted device configuration and potentially + configured one that has not gone through any previous checks (e.g. --silence mode) - def _create_keyfile(self,luks_handle , partition :dict, password :str): - """ roiutine to create keyfiles, so it can be moved elsewhere + NOTE: this function should be run AFTER running the mount_ordered_layout function """ - if self._disk_encryption and self._disk_encryption.generate_encryption_file(partition): - if not (cryptkey_dir := pathlib.Path(f"{self.target}/etc/cryptsetup-keys.d")).exists(): - cryptkey_dir.mkdir(parents=True) - # Once we store the key as ../xyzloop.key systemd-cryptsetup can automatically load this key - # if we name the device to "xyzloop". - if partition.get('mountpoint',None): - encryption_key_path = f"/etc/cryptsetup-keys.d/{pathlib.Path(partition['mountpoint']).name}loop.key" - else: - encryption_key_path = f"/etc/cryptsetup-keys.d/{pathlib.Path(partition['device_instance'].path).name}.key" - with open(f"{self.target}{encryption_key_path}", "w") as keyfile: - keyfile.write(generate_password(length=512)) + boot_mount = self.target / 'boot' + lsblk_info = disk.get_lsblk_by_mountpoint(boot_mount) + + if len(lsblk_info) > 0: + if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB): + raise DiskError( + f'The boot partition mounted at {boot_mount} is not large enough to install a boot loader. ' + f'Please resize it to at least 200MiB and re-run the installation.' + ) - os.chmod(f"{self.target}{encryption_key_path}", 0o400) + def sanity_check(self): + self._verify_boot_part() + self._verify_service_stop() - luks_handle.add_key(pathlib.Path(f"{self.target}{encryption_key_path}"), password=password) - luks_handle.crypttab(self, encryption_key_path, options=["luks", "key-slot=1"]) + def mount_ordered_layout(self): + log('Mounting partitions in order', level=logging.INFO) - def _has_root(self, partition :dict) -> bool: - """ - Determine if an encrypted partition contains root in it - """ - if partition.get("mountpoint") is None: - if (sub_list := partition.get("btrfs",{}).get('subvolumes',{})): - for mountpoint in [sub_list[subvolume].get("mountpoint") if isinstance(subvolume, dict) else subvolume.mountpoint for subvolume in sub_list]: - if mountpoint == '/': - return True - return False + for mod in self._disk_config.device_modifications: + # partitions have to mounted in the right order on btrfs the mountpoint will + # be empty as the actual subvolumes are getting mounted instead so we'll use + # '/' just for sorting + sorted_part_mods = sorted(mod.partitions, key=lambda x: x.mountpoint if x.mountpoint else Path('/')) + + if self._disk_encryption.encryption_type is not disk.EncryptionType.NoEncryption: + enc_partitions = list(filter(lambda x: x in self._disk_encryption.partitions, sorted_part_mods)) else: - return False - elif partition.get("mountpoint") == '/': - return True - else: - return False + enc_partitions = [] - def mount_ordered_layout(self, layouts: Dict[str, Any]) -> None: - from .luks import luks2 - from .disk.btrfs import setup_subvolumes, mount_subvolume - - # set the partitions as a list not part of a tree (which we don't need anymore (i think) - list_part = [] - list_luks_handles = [] - for blockdevice in layouts: - list_part.extend(layouts[blockdevice]['partitions']) - - # TODO: Implement a proper mount-queue system that does not depend on return values. - mount_queue = {} - - # we manage the encrypted partititons - if self._disk_encryption: - for partition in self._disk_encryption.all_partitions: - # open the luks device and all associate stuff - loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['device_instance'].path).name}" - - # note that we DON'T auto_unmount (i.e. close the encrypted device so it can be used - with (luks_handle := luks2(partition['device_instance'], loopdev, self._disk_encryption.encryption_password, auto_unmount=False)) as unlocked_device: - if self._disk_encryption.generate_encryption_file(partition) and not self._has_root(partition): - list_luks_handles.append([luks_handle, partition, self._disk_encryption.encryption_password]) - # this way all the requesrs will be to the dm_crypt device and not to the physical partition - partition['device_instance'] = unlocked_device - - if self._has_root(partition) and self._disk_encryption.generate_encryption_file(partition) is False: - if self._disk_encryption.hsm_device: - Fido2.fido2_enroll(self._disk_encryption.hsm_device, partition['device_instance'], self._disk_encryption.encryption_password) - - btrfs_subvolumes = [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', [])] - - for partition in btrfs_subvolumes: - device_instance = partition['device_instance'] - mount_options = partition.get('filesystem', {}).get('mount_options', []) - self.mount(device_instance, "/", options=','.join(mount_options)) - setup_subvolumes(installation=self, partition_dict=partition) - device_instance.unmount() - - # We then handle any special cases, such as btrfs - for partition in btrfs_subvolumes: - subvolumes: List[Subvolume] = partition['btrfs']['subvolumes'] - for subvolume in sorted(subvolumes, key=lambda item: item.mountpoint): - # We cache the mount call for later - mount_queue[subvolume.mountpoint] = lambda sub_vol=subvolume, device=partition['device_instance']: mount_subvolume( - installation=self, - device=device, - subvolume=sub_vol - ) + # attempt to decrypt all luks partitions + luks_handlers = self._prepare_luks_partitions(enc_partitions) - # We mount ordinary partitions, and we sort them by the mountpoint - for partition in sorted([entry for entry in list_part if entry.get('mountpoint', False)], key=lambda part: part['mountpoint']): - mountpoint = partition['mountpoint'] - log(f"Mounting {mountpoint} to {self.target}{mountpoint} using {partition['device_instance']}", level=logging.INFO) + for part_mod in sorted_part_mods: + if part_mod not in luks_handlers: # partition is not encrypted + self._mount_partition(part_mod) + else: # mount encrypted partition + self._mount_luks_partiton(part_mod, luks_handlers[part_mod]) - if partition.get('filesystem',{}).get('mount_options',[]): - mount_options = ','.join(partition['filesystem']['mount_options']) - mount_queue[mountpoint] = lambda instance=partition['device_instance'], target=f"{self.target}{mountpoint}", options=mount_options: instance.mount(target, options=options) - else: - mount_queue[mountpoint] = lambda instance=partition['device_instance'], target=f"{self.target}{mountpoint}": instance.mount(target) + def _prepare_luks_partitions(self, partitions: List[disk.PartitionModification]) -> Dict[disk.PartitionModification, Luks2]: + luks_handlers = {} - log(f"Using mount order: {list(sorted(mount_queue.items(), key=lambda item: item[0]))}", level=logging.DEBUG, fg="white") + for part_mod in partitions: + luks_handler = disk.device_handler.unlock_luks2_dev( + part_mod.dev_path, + part_mod.mapper_name, + self._disk_encryption.encryption_password + ) + luks_handlers[part_mod] = luks_handler + + return luks_handlers + + def _mount_partition(self, part_mod: disk.PartitionModification): + # it would be none if it's btrfs as the subvolumes will have the mountpoints defined + if part_mod.mountpoint is not None: + target = self.target / part_mod.relative_mountpoint + disk.device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options) + + if part_mod.fs_type == disk.FilesystemType.Btrfs: + self._mount_btrfs_subvol(part_mod.dev_path, part_mod.btrfs_subvols) + + def _mount_luks_partiton(self, part_mod: disk.PartitionModification, luks_handler: Luks2): + # it would be none if it's btrfs as the subvolumes will have the mountpoints defined + if part_mod.mountpoint is not None: + target = self.target / part_mod.relative_mountpoint + disk.device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options) + + if part_mod.fs_type == disk.FilesystemType.Btrfs: + self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols) + + def _mount_btrfs_subvol(self, dev_path: Path, subvolumes: List[disk.SubvolumeModification]): + for subvol in subvolumes: + mountpoint = self.target / subvol.relative_mountpoint + mount_options = subvol.mount_options + [f'subvol={subvol.name}'] + disk.device_handler.mount(dev_path, mountpoint, options=mount_options) + + def generate_key_files(self): + for part_mod in self._disk_encryption.partitions: + gen_enc_file = self._disk_encryption.should_generate_encryption_file(part_mod) + + luks_handler = Luks2( + part_mod.dev_path, + mapper_name=part_mod.mapper_name, + password=self._disk_encryption.encryption_password + ) - # We mount everything by sorting on the mountpoint itself. - for mountpoint, frozen_func in sorted(mount_queue.items(), key=lambda item: item[0]): - frozen_func() + if gen_enc_file and not part_mod.is_root(): + log(f'Creating key-file: {part_mod.dev_path}', level=logging.INFO) + luks_handler.create_keyfile(self.target) + if part_mod.is_root() and not gen_enc_file: + if self._disk_encryption.hsm_device: + disk.Fido2.fido2_enroll( + self._disk_encryption.hsm_device, + part_mod, + self._disk_encryption.encryption_password + ) + + def activate_ntp(self): + """ + If NTP is activated, confirm activiation in the ISO and at least one time-sync finishes + """ + SysCommand('timedatectl set-ntp true') + + logged = False + while service_state('dbus-org.freedesktop.timesync1.service') not in ['running']: + if not logged: + log(f"Waiting for dbus-org.freedesktop.timesync1.service to enter running state", level=logging.INFO) + logged = True time.sleep(1) - try: - findmnt(pathlib.Path(f"{self.target}{mountpoint}"), traverse=False) - except DiskError: - raise DiskError(f"Target {self.target}{mountpoint} never got mounted properly (unable to get mount information using findmnt).") + logged = False + while 'Server: n/a' in SysCommand('timedatectl timesync-status --no-pager --property=Server --value'): + if not logged: + log(f"Waiting for timedatectl timesync-status to report a timesync against a server", level=logging.INFO) + logged = True + time.sleep(1) - # once everything is mounted, we generate the key files in the correct place - for handle in list_luks_handles: - ppath = handle[1]['device_instance'].path - log(f"creating key-file for {ppath}",level=logging.INFO) - self._create_keyfile(handle[0],handle[1],handle[2]) + def sync_log_to_install_medium(self) -> bool: + # Copy over the install log (if there is one) to the install medium if + # at least the base has been strapped in, otherwise we won't have a filesystem/structure to copy to. + if self.helper_flags.get('base-strapped', False) is True: + if filename := storage.get('LOG_FILE', None): + absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename) + + if not os.path.isdir(f"{self.target}/{os.path.dirname(absolute_logfile)}"): + os.makedirs(f"{self.target}/{os.path.dirname(absolute_logfile)}") - def mount(self, partition :Partition, mountpoint :str, create_mountpoint :bool = True, options='') -> None: - if create_mountpoint and not os.path.isdir(f'{self.target}{mountpoint}'): - os.makedirs(f'{self.target}{mountpoint}') + shutil.copy2(absolute_logfile, f"{self.target}/{absolute_logfile}") - partition.mount(f'{self.target}{mountpoint}', options=options) + return True def add_swapfile(self, size='4G', enable_resume=True, file='/swapfile'): if file[:1] != '/': @@ -394,7 +411,7 @@ class Installer: else: pacman_conf.write(line) - def pacstrap(self, *packages :str, **kwargs :str) -> bool: + def pacstrap(self, *packages: Union[str, List[str]], **kwargs :str) -> bool: if type(packages[0]) in (list, tuple): packages = packages[0] @@ -437,7 +454,7 @@ class Installer: return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist') - def genfstab(self, flags :str = '-pU') -> bool: + def genfstab(self, flags :str = '-pU'): self.log(f"Updating {self.target}/etc/fstab", level=logging.INFO) try: @@ -460,7 +477,37 @@ class Installer: for entry in self.FSTAB_ENTRIES: fstab_fh.write(f'{entry}\n') - return True + for mod in self._disk_config.device_modifications: + for part_mod in mod.partitions: + if part_mod.fs_type != disk.FilesystemType.Btrfs: + continue + + fstab_file = Path(f'{self.target}/etc/fstab') + + with fstab_file.open('r') as fp: + fstab = fp.readlines() + + # Replace the {installation}/etc/fstab with entries + # using the compress=zstd where the mountpoint has compression set. + for index, line in enumerate(fstab): + # So first we grab the mount options by using subvol=.*? as a locator. + # And we also grab the mountpoint for the entry, for instance /var/log + subvoldef = re.findall(',.*?subvol=.*?[\t ]', line) + mountpoint = re.findall('[\t ]/.*?[\t ]', line) + + if not subvoldef or not mountpoint: + continue + + for sub_vol in part_mod.btrfs_subvols: + # We then locate the correct subvolume and check if it's compressed, + # and skip entries where compression is already defined + # We then sneak in the compress=zstd option if it doesn't already exist: + if sub_vol.compress and str(sub_vol.mountpoint) == Path(mountpoint[0].strip()) and ',compress=zstd,' not in line: + fstab[index] = line.replace(subvoldef[0], f',compress=zstd{subvoldef[0]}') + break + + with fstab_file.open('w') as fp: + fp.writelines(fstab) def set_hostname(self, hostname: str, *args :str, **kwargs :str) -> None: with open(f'{self.target}/etc/hostname', 'w') as fh: @@ -509,8 +556,8 @@ class Installer: if result := plugin.on_timezone(zone): zone = result - if (pathlib.Path("/usr") / "share" / "zoneinfo" / zone).exists(): - (pathlib.Path(self.target) / "etc" / "localtime").unlink(missing_ok=True) + if (Path("/usr") / "share" / "zoneinfo" / zone).exists(): + (Path(self.target) / "etc" / "localtime").unlink(missing_ok=True) SysCommand(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{zone} /etc/localtime') return True @@ -523,10 +570,6 @@ class Installer: return False - def activate_ntp(self) -> None: - log(f"activate_ntp() is deprecated, use activate_time_syncronization()", fg="yellow", level=logging.INFO) - self.activate_time_syncronization() - 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) self.enable_service('systemd-timesyncd') @@ -540,7 +583,10 @@ class Installer: # fstrim is owned by util-linux, a dependency of both base and systemd. self.enable_service("fstrim.timer") - def enable_service(self, *services :str) -> None: + def enable_service(self, *services: Union[str, List[str]]) -> None: + if type(services[0]) in (list, tuple): + services = services[0] + for service in services: self.log(f'Enabling service {service}', level=logging.INFO) try: @@ -552,10 +598,10 @@ class Installer: if hasattr(plugin, 'on_service'): plugin.on_service(service) - def run_command(self, cmd :str, *args :str, **kwargs :str) -> None: + def run_command(self, cmd :str, *args :str, **kwargs :str) -> SysCommand: return SysCommand(f'/usr/bin/arch-chroot {self.target} {cmd}') - def arch_chroot(self, cmd :str, run_as :Optional[str] = None): + def arch_chroot(self, cmd :str, run_as :Optional[str] = None) -> SysCommand: if run_as: cmd = f"su - {run_as} -c {shlex.quote(cmd)}" @@ -645,21 +691,6 @@ class Installer: return True - def detect_encryption(self, partition :Partition) -> bool: - from .disk.mapperdev import MapperDev - from .disk.dmcryptdev import DMCryptDev - from .disk.helpers import get_filesystem_type - - if type(partition) is MapperDev: - # Returns MapperDev.partition - return partition.partition - elif type(partition) is DMCryptDev: - return partition.MapperDev.partition - elif get_filesystem_type(partition.path) == 'crypto_LUKS': - return partition - - return False - def mkinitcpio(self, *flags :str) -> bool: for plugin in plugins.values(): if hasattr(plugin, 'on_mkinitcpio'): @@ -668,7 +699,7 @@ class Installer: return True # mkinitcpio will error out if there's no vconsole. - if (vconsole := pathlib.Path(f"{self.target}/etc/vconsole.conf")).exists() is False: + 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") @@ -677,7 +708,7 @@ class Installer: mkinit.write(f"BINARIES=({' '.join(self.BINARIES)})\n") mkinit.write(f"FILES=({' '.join(self.FILES)})\n") - if self._disk_encryption and not self._disk_encryption.hsm_device: + if not self._disk_encryption.hsm_device: # For now, if we don't use HSM we revert to the old # way of setting up encryption hooks for mkinitcpio. # This is purely for stability reasons, we're going away from this. @@ -694,46 +725,36 @@ class Installer: return False def minimal_installation( - self, testing: bool = False, multilib: bool = False, - hostname: str = 'archinstall', locales: List[str] = ['en_US.UTF-8 UTF-8']) -> bool: - # Add necessary packages if encrypting the drive - # (encrypted partitions default to btrfs for now, so we need btrfs-progs) - # TODO: Perhaps this should be living in the function which dictates - # the partitioning. Leaving here for now. - - for partition in self.partitions: - if partition.filesystem == 'btrfs': - # if partition.encrypted: - if 'btrfs-progs' not in self.base_packages: - self.base_packages.append('btrfs-progs') - if partition.filesystem == 'xfs': - if 'xfs' not in self.base_packages: - self.base_packages.append('xfsprogs') - if partition.filesystem == 'f2fs': - if 'f2fs' not in self.base_packages: - self.base_packages.append('f2fs-tools') - - # Configure mkinitcpio to handle some specific use cases. - if partition.filesystem == 'btrfs': - if 'btrfs' not in self.MODULES: - self.MODULES.append('btrfs') - if '/usr/bin/btrfs' not in self.BINARIES: - self.BINARIES.append('/usr/bin/btrfs') - # There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed. - if partition.filesystem == 'ntfs3' and partition.mountpoint == self.target: - if 'fsck' in self.HOOKS: - self.HOOKS.remove('fsck') - - if self.detect_encryption(partition): - if self._disk_encryption and self._disk_encryption.hsm_device: - # Required bby mkinitcpio to add support for fido2-device options - self.pacstrap('libfido2') - - if 'sd-encrypt' not in self.HOOKS: - self.HOOKS.insert(self.HOOKS.index('filesystems'), 'sd-encrypt') - else: - if 'encrypt' not in self.HOOKS: - self.HOOKS.insert(self.HOOKS.index('filesystems'), 'encrypt') + self, + testing: bool = False, + multilib: bool = False, + hostname: str = 'archinstall', + locales: List[str] = ['en_US.UTF-8 UTF-8'] + ): + for mod in self._disk_config.device_modifications: + for part in mod.partitions: + if (pkg := part.fs_type.installation_pkg) is not None: + self.base_packages.append(pkg) + if (module := part.fs_type.installation_module) is not None: + self.MODULES.append(module) + if (binary := part.fs_type.installation_binary) is not None: + self.BINARIES.append(binary) + + # There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed. + if part.fs_type.fs_type_mount == 'ntfs3' and part.mountpoint == self.target: + if 'fsck' in self.HOOKS: + self.HOOKS.remove('fsck') + + if part in self._disk_encryption.partitions: + if self._disk_encryption.hsm_device: + # Required bby mkinitcpio to add support for fido2-device options + self.pacstrap('libfido2') + + if 'sd-encrypt' not in self.HOOKS: + self.HOOKS.insert(self.HOOKS.index('filesystems'), 'sd-encrypt') + else: + if 'encrypt' not in self.HOOKS: + self.HOOKS.insert(self.HOOKS.index('filesystems'), 'encrypt') if not has_uefi(): self.base_packages.append('grub') @@ -742,11 +763,11 @@ class Installer: vendor = cpu_vendor() if vendor == "AuthenticAMD": self.base_packages.append("amd-ucode") - if (ucode := pathlib.Path(f"{self.target}/boot/amd-ucode.img")).exists(): + if (ucode := Path(f"{self.target}/boot/amd-ucode.img")).exists(): ucode.unlink() elif vendor == "GenuineIntel": self.base_packages.append("intel-ucode") - if (ucode := pathlib.Path(f"{self.target}/boot/intel-ucode.img")).exists(): + 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) @@ -802,9 +823,7 @@ class Installer: if hasattr(plugin, 'on_install'): plugin.on_install(self) - return True - - def setup_swap(self, kind :str = 'zram') -> bool: + def setup_swap(self, kind :str = 'zram'): if kind == 'zram': self.log(f"Setting up swap on zram") self.pacstrap('zram-generator') @@ -818,16 +837,27 @@ class Installer: self.enable_service('systemd-zram-setup@zram0.service') self._zram_enabled = True - - return True else: raise ValueError(f"Archinstall currently only supports setting up swap on zram") - def add_systemd_bootloader(self, boot_partition :Partition, root_partition :Partition) -> bool: + def _get_boot_partition(self) -> Optional[disk.PartitionModification]: + for layout in self._disk_config.device_modifications: + if boot := layout.get_boot_partition(): + return boot + return None + + def _get_root_partition(self) -> Optional[disk.PartitionModification]: + for mod in self._disk_config.device_modifications: + if root := mod.get_root_partition(self._disk_config.relative_mountpoint): + return root + return None + + def _add_systemd_bootloader(self, root_partition: disk.PartitionModification): self.pacstrap('efibootmgr') if not has_uefi(): raise HardwareIncompatibilityError + # TODO: Ideally we would want to check if another config # points towards the same disk and/or partition. # And in which case we should do some clean up. @@ -882,74 +912,73 @@ class Installer: 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) + self.log( + f"Unknown CPU vendor '{vendor}' detected. Archinstall won't add any ucode to systemd-boot config.", + level=logging.DEBUG) 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. - root_fs_type = get_mount_fs_type(root_partition.filesystem) - if root_fs_type is not None: - options_entry = f'rw rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}\n' - else: - options_entry = f'rw {" ".join(self.KERNEL_PARAMS)}\n' + options_entry = f'rw rootfstype={root_partition.fs_type.fs_type_mount} {" ".join(self.KERNEL_PARAMS)}\n' - for subvolume in root_partition.subvolumes: - if subvolume.root is True and subvolume.name != '': - options_entry = f"rootflags=subvol={subvolume.name} " + options_entry + for sub_vol in root_partition.btrfs_subvols: + if sub_vol.is_root(): + options_entry = f"rootflags=subvol={sub_vol.name} " + options_entry # Zswap should be disabled when using zram. - # # https://github.com/archlinux/archinstall/issues/881 if self._zram_enabled: options_entry = "zswap.enabled=0 " + options_entry - if real_device := self.detect_encryption(root_partition): + 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 PART-UUID on {real_device}: '{real_device.uuid}/{real_device.part_uuid}'.", level=logging.DEBUG) + log('Root partition is an encrypted device, identifying by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) kernel_options = f"options" if self._disk_encryption and self._disk_encryption.hsm_device: # Note: lsblk UUID must be used, not PARTUUID for sd-encrypt to work - kernel_options += f" rd.luks.name={real_device.uuid}=luksdev" + kernel_options += f' rd.luks.name={root_partition.uuid}=luksdev' # Note: tpm2-device and fido2-device don't play along very well: # https://github.com/archlinux/archinstall/pull/1196#issuecomment-1129715645 - kernel_options += f" rd.luks.options=fido2-device=auto,password-echo=no" + kernel_options += f' rd.luks.options=fido2-device=auto,password-echo=no' else: - kernel_options += f" cryptdevice=PARTUUID={real_device.part_uuid}:luksdev" + kernel_options += f' cryptdevice=PARTUUID={root_partition.partuuid}:luksdev' entry.write(f'{kernel_options} root=/dev/mapper/luksdev {options_entry}') else: - log(f"Identifying root partition by PARTUUID on {root_partition}, looking for '{root_partition.part_uuid}'.", level=logging.DEBUG) - entry.write(f'options root=PARTUUID={root_partition.part_uuid} {options_entry}') + log(f'Identifying root partition by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + entry.write(f'options root=PARTUUID={root_partition.partuuid} {options_entry}') - self.helper_flags['bootloader'] = "systemd" - - return True + self.helper_flags['bootloader'] = 'systemd' - def add_grub_bootloader(self, boot_partition :Partition, root_partition :Partition) -> bool: + def _add_grub_bootloader( + self, + boot_partition: disk.PartitionModification, + root_partition: disk.PartitionModification + ): self.pacstrap('grub') # no need? - root_fs_type = get_mount_fs_type(root_partition.filesystem) + _file = "/etc/default/grub" - if real_device := self.detect_encryption(root_partition): - root_uuid = SysCommand(f"blkid -s UUID -o value {real_device.path}").decode().rstrip() - _file = "/etc/default/grub" - add_to_CMDLINE_LINUX = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"cryptdevice=UUID={root_uuid}:cryptlvm rootfstype={root_fs_type}\"/'" - enable_CRYPTODISK = "sed -i 's/#GRUB_ENABLE_CRYPTODISK=y/GRUB_ENABLE_CRYPTODISK=y/'" + if root_partition.fs_type.is_crypto(): + log(f"Using UUID {root_partition.uuid} as encrypted root identifier", level=logging.DEBUG) - log(f"Using UUID {root_uuid} of {real_device} as encrypted root identifier.", level=logging.INFO) - SysCommand(f"/usr/bin/arch-chroot {self.target} {add_to_CMDLINE_LINUX} {_file}") - SysCommand(f"/usr/bin/arch-chroot {self.target} {enable_CRYPTODISK} {_file}") + 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/'" + + SysCommand(f"/usr/bin/arch-chroot {self.target} {enable_cryptdisk} {_file}") else: - _file = "/etc/default/grub" - add_to_CMDLINE_LINUX = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"rootfstype={root_fs_type}\"/'" - SysCommand(f"/usr/bin/arch-chroot {self.target} {add_to_CMDLINE_LINUX} {_file}") + cmd_line_linux = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"rootfstype={root_partition.fs_type.value}\"/'" + + SysCommand(f"/usr/bin/arch-chroot {self.target} {cmd_line_linux} {_file}") + + log(f"GRUB boot partition: {boot_partition.dev_path}", level=logging.INFO) - log(f"GRUB uses {boot_partition.path} as the boot partition.", level=logging.INFO) if has_uefi(): self.pacstrap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? + try: SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable', peek_output=True) except SysCallError: @@ -961,7 +990,7 @@ class Installer: try: SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=i386-pc --recheck {boot_partition.parent}', peek_output=True) except SysCallError as error: - raise DiskError(f"Could not install GRUB to {boot_partition.path}: {error}") + raise DiskError(f"Failed to install GRUB boot on {boot_partition.dev_path}: {error}") try: SysCommand(f'/usr/bin/arch-chroot {self.target} grub-mkconfig -o /boot/grub/grub.cfg') @@ -970,22 +999,22 @@ class Installer: self.helper_flags['bootloader'] = "grub" - return True - - def add_efistub_bootloader(self, boot_partition :Partition, root_partition :Partition) -> bool: + def _add_efistub_bootloader( + self, + boot_partition: disk.PartitionModification, + root_partition: disk.PartitionModification + ): self.pacstrap('efibootmgr') if not has_uefi(): raise HardwareIncompatibilityError + # TODO: Ideally we would want to check if another config # points towards the same disk and/or partition. # And in which case we should do some clean up. - root_fs_type = get_mount_fs_type(root_partition.filesystem) - for kernel in self.kernels: # Setup the firmware entry - label = f'Arch Linux ({kernel})' loader = f"/vmlinuz-{kernel}" @@ -1004,22 +1033,22 @@ class Installer: # blkid doesn't trigger on loopback devices really well, # so we'll use the old manual method until we get that sorted out. - if real_device := self.detect_encryption(root_partition): + + 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 PART-UUID on {real_device}: '{real_device.part_uuid}'.", level=logging.DEBUG) - kernel_parameters.append(f'cryptdevice=PARTUUID={real_device.part_uuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}') + log(f'Identifying root partition by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.fs_type.value} {" ".join(self.KERNEL_PARAMS)}') else: - log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.part_uuid}'.", level=logging.DEBUG) - kernel_parameters.append(f'root=PARTUUID={root_partition.part_uuid} rw rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}') + log(f'Root partition is an encrypted device identifying by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.fs_type.value} {" ".join(self.KERNEL_PARAMS)}') - SysCommand(f'efibootmgr --disk {boot_partition.path[:-1]} --part {boot_partition.path[-1]} --create --label "{label}" --loader {loader} --unicode \'{" ".join(kernel_parameters)}\' --verbose') + device = disk.device_handler.get_device_by_partition_path(boot_partition.dev_path) + SysCommand(f'efibootmgr --disk {device.path} --part {device.path} --create --label "{label}" --loader {loader} --unicode \'{" ".join(kernel_parameters)}\' --verbose') self.helper_flags['bootloader'] = "efistub" - return True - - def add_bootloader(self, bootloader :str = 'systemd-bootctl') -> bool: + def add_bootloader(self, bootloader: Bootloader) -> bool: """ Adds a bootloader to the installation instance. Archinstall supports one of three types: @@ -1039,52 +1068,33 @@ class Installer: return True if type(self.target) == str: - self.target = pathlib.Path(self.target) - - boot_partition = None - root_partition = None - for partition in self.partitions: - if self.target / 'boot' in partition.mountpoints: - boot_partition = partition - elif self.target in partition.mountpoints: - root_partition = partition - - if boot_partition is None or root_partition is None: - raise ValueError(f"Could not detect root ({root_partition}) or boot ({boot_partition}) in {self.target} based on: {self.partitions}") - - self.log(f'Adding bootloader {bootloader} to {boot_partition if boot_partition else root_partition}', level=logging.INFO) - - if bootloader == 'systemd-bootctl': - self.add_systemd_bootloader(boot_partition, root_partition) - elif bootloader == "grub-install": - self.add_grub_bootloader(boot_partition, root_partition) - elif bootloader == 'efistub': - self.add_efistub_bootloader(boot_partition, root_partition) - else: - raise RequirementError(f"Unknown (or not yet implemented) bootloader requested: {bootloader}") + self.target = Path(self.target) - return True + boot_partition = self._get_boot_partition() + root_partition = self._get_root_partition() - def add_additional_packages(self, *packages :str) -> bool: - return self.pacstrap(*packages) + if boot_partition is None: + raise ValueError(f'Could not detect boot at mountpoint {self.target}') - def install_profile(self, profile :str) -> ModuleType: - """ - Installs a archinstall profile script (.py file). - This profile can be either local, remote or part of the library. + if root_partition is None: + raise ValueError(f'Could not detect root at mountpoint {self.target}') - :param profile: Can be a local path or a remote path (URL) - :return: Returns the imported script as a module, this way - you can access any remaining functions exposed by the profile. - :rtype: module - """ - storage['installation_session'] = self + self.log(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}', level=logging.INFO) + + match bootloader: + case Bootloader.Systemd: + self._add_systemd_bootloader(root_partition) + case Bootloader.Grub: + self._add_grub_bootloader(boot_partition, root_partition) + case Bootloader.Efistub: + self._add_efistub_bootloader(boot_partition, root_partition) - if type(profile) == str: - profile = Profile(self, profile) + def add_additional_packages(self, *packages: Union[str, List[str]]) -> bool: + return self.pacstrap(*packages) - self.log(f'Installing archinstall profile {profile}', level=logging.INFO) - return profile.install() + def _enable_users(self, service: str, users: List[User]): + for user in users: + self.arch_chroot(f'systemctl enable --user {service}', run_as=user.username) def enable_sudo(self, entity: str, group :bool = False): self.log(f'Enabling sudo permissions for {entity}.', level=logging.INFO) @@ -1092,7 +1102,7 @@ class Installer: sudoers_dir = f"{self.target}/etc/sudoers.d" # Creates directory if not exists - if not (sudoers_path := pathlib.Path(sudoers_dir)).exists(): + if not (sudoers_path := Path(sudoers_dir)).exists(): sudoers_path.mkdir(parents=True) # Guarantees sudoer confs directory recommended perms os.chmod(sudoers_dir, 0o440) @@ -1114,7 +1124,7 @@ class Installer: sudoers.write(f'{"%" if group else ""}{entity} ALL=(ALL) ALL\n') # Guarantees sudoer conf file recommended perms - os.chmod(pathlib.Path(rule_file_name), 0o440) + os.chmod(Path(rule_file_name), 0o440) def create_users(self, users: Union[User, List[User]]): if not isinstance(users, list): diff --git a/archinstall/lib/locale_helpers.py b/archinstall/lib/locale_helpers.py index 5580fa91..d1fb4562 100644 --- a/archinstall/lib/locale_helpers.py +++ b/archinstall/lib/locale_helpers.py @@ -1,11 +1,12 @@ import logging -from typing import Iterator, List, Callable +from typing import Iterator, List, Callable, Optional from .exceptions import ServiceException 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() @@ -45,20 +46,25 @@ def get_locale_mode_text(mode): 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 :str = None, - charset :str = 'C', - numbers :str = 'C', - time :str = 'C', - collate :str = 'C', - messages :str = 'C'): + +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) diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index ad6bf093..fc531a06 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -1,92 +1,78 @@ from __future__ import annotations -import json + import logging -import os -import pathlib import shlex import time -from typing import Optional, List,TYPE_CHECKING -# https://stackoverflow.com/a/39757388/929999 -if TYPE_CHECKING: - from .installer import Installer +from dataclasses import dataclass +from pathlib import Path +from typing import Optional, List -from .disk import Partition, convert_device_to_uuid -from .general import SysCommand, SysCommandWorker +from . import disk +from .general import SysCommand, generate_password, SysCommandWorker from .output import log from .exceptions import SysCallError, DiskError from .storage import storage -from .disk.helpers import get_filesystem_type -from .disk.mapperdev import MapperDev -from .disk.btrfs import BTRFSPartition - - -class luks2: - def __init__(self, - partition: Partition, - mountpoint: Optional[str], - password: Optional[str], - key_file :Optional[str] = None, - auto_unmount :bool = False, - *args :str, - **kwargs :str): - - self.password = password - self.partition = partition - self.mountpoint = mountpoint - self.args = args - self.kwargs = kwargs - self.key_file = key_file - self.auto_unmount = auto_unmount - self.filesystem = 'crypto_LUKS' - self.mapdev = None - - def __enter__(self) -> Partition: - if not self.key_file: - self.key_file = f"/tmp/{os.path.basename(self.partition.path)}.disk_pw" # TODO: Make disk-pw-file randomly unique? - - if type(self.password) != bytes: - self.password = bytes(self.password, 'UTF-8') - - with open(self.key_file, 'wb') as fh: - fh.write(self.password) - - return self.unlock(self.partition, self.mountpoint, self.key_file) - - def __exit__(self, *args :str, **kwargs :str) -> bool: - # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager + + +@dataclass +class Luks2: + luks_dev_path: Path + mapper_name: Optional[str] = None + password: Optional[str] = None + key_file: Optional[Path] = None + auto_unmount: bool = False + + # will be set internally after unlocking the device + _mapper_dev: Optional[Path] = None + + @property + def mapper_dev(self) -> Optional[Path]: + if self.mapper_name: + return Path(f'/dev/mapper/{self.mapper_name}') + return None + + def __post_init__(self): + if self.luks_dev_path is None: + raise ValueError('Partition must have a path set') + + def __enter__(self): + self.unlock(self.key_file) + + def __exit__(self, *args: str, **kwargs: str): if self.auto_unmount: - self.close() + self.lock() + + def _default_key_file(self) -> Path: + return Path(f'/tmp/{self.luks_dev_path.name}.disk_pw') - if len(args) >= 2 and args[1]: - raise args[1] + def _password_bytes(self) -> bytes: + if not self.password: + raise ValueError('Password for luks2 device was not specified') - return True + if isinstance(self.password, bytes): + return self.password + else: + return bytes(self.password, 'UTF-8') - def encrypt(self, partition :Partition, - password :Optional[str] = None, - key_size :int = 512, - hash_type :str = 'sha512', - iter_time :int = 10000, - key_file :Optional[str] = None) -> str: + def encrypt( + self, + key_size: int = 512, + hash_type: str = 'sha512', + iter_time: int = 10000, + key_file: Optional[Path] = None + ) -> Path: + log(f'Luks2 encrypting: {self.luks_dev_path}', level=logging.INFO) - log(f'Encrypting {partition} (This might take a while)', level=logging.INFO) + byte_password = self._password_bytes() if not key_file: if self.key_file: key_file = self.key_file else: - key_file = f"/tmp/{os.path.basename(self.partition.path)}.disk_pw" # TODO: Make disk-pw-file randomly unique? - - if not password: - password = self.password - - if type(password) != bytes: - password = bytes(password, 'UTF-8') + key_file = self._default_key_file() - with open(key_file, 'wb') as fh: - fh.write(password) - - partition.partprobe() + with open(key_file, 'wb') as fh: + fh.write(byte_password) cryptsetup_args = shlex.join([ '/usr/bin/cryptsetup', @@ -97,120 +83,163 @@ class luks2: '--hash', hash_type, '--key-size', str(key_size), '--iter-time', str(iter_time), - '--key-file', os.path.abspath(key_file), + '--key-file', str(key_file), '--use-urandom', - 'luksFormat', partition.path, + 'luksFormat', str(self.luks_dev_path), ]) try: # Retry formatting the volume because archinstall can some times be too quick # which generates a "Device /dev/sdX does not exist or access denied." between # setting up partitions and us trying to encrypt it. + cmd_handle = None for i in range(storage['DISK_RETRY_ATTEMPTS']): if (cmd_handle := SysCommand(cryptsetup_args)).exit_code != 0: time.sleep(storage['DISK_TIMEOUTS']) else: break - if cmd_handle.exit_code != 0: - raise DiskError(f'Could not encrypt volume "{partition.path}": {b"".join(cmd_handle)}') + if cmd_handle is not None and cmd_handle.exit_code != 0: + output = str(b''.join(cmd_handle)) + raise DiskError(f'Could not encrypt volume "{self.luks_dev_path}": {output}') except SysCallError as err: if err.exit_code == 1: - log(f'{partition} is being used, trying to unmount and crypt-close the device and running one more attempt at encrypting the device.', level=logging.DEBUG) - # Partition was in use, unmount it and try again - partition.unmount() - - # Get crypt-information about the device by doing a reverse lookup starting with the partition path - # For instance: /dev/sda - SysCommand(f'bash -c "partprobe"') - devinfo = json.loads(b''.join(SysCommand(f"lsblk --fs -J {partition.path}")).decode('UTF-8'))['blockdevices'][0] - - # For each child (sub-partition/sub-device) - if len(children := devinfo.get('children', [])): - for child in children: - # Unmount the child location - if child_mountpoint := child.get('mountpoint', None): - log(f'Unmounting {child_mountpoint}', level=logging.DEBUG) - SysCommand(f"umount -R {child_mountpoint}") - - # And close it if possible. - log(f"Closing crypt device {child['name']}", level=logging.DEBUG) - SysCommand(f"cryptsetup close {child['name']}") + log(f'luks2 partition currently in use: {self.luks_dev_path}') + log('Attempting to unmount, crypt-close and trying encryption again') + self.lock() # Then try again to set up the crypt-device - cmd_handle = SysCommand(cryptsetup_args) + SysCommand(cryptsetup_args) else: raise err return key_file - def unlock(self, partition :Partition, mountpoint :str, key_file :str) -> Partition: + def _get_luks_uuid(self) -> str: + command = f'/usr/bin/cryptsetup luksUUID {self.luks_dev_path}' + + try: + result = SysCommand(command) + if result.exit_code != 0: + raise DiskError(f'Unable to get UUID for Luks device: {result.decode()}') + + return result.decode() # type: ignore + except SysCallError as err: + log(f'Unable to get UUID for Luks device: {self.luks_dev_path}', level=logging.INFO) + raise err + + def is_unlocked(self) -> bool: + return self.mapper_name is not None and Path(f'/dev/mapper/{self.mapper_name}').exists() + + def unlock(self, key_file: Optional[Path] = None): """ - Mounts a luks2 compatible partition to a certain mountpoint. - Keyfile must be specified as there's no way to interact with the pw-prompt atm. + Unlocks the luks device, an optional key file location for unlocking can be specified, + otherwise a default location for the key file will be used. - :param mountpoint: The name without absolute path, for instance "luksdev" will point to /dev/mapper/luksdev - :type mountpoint: str + :param key_file: An alternative key file + :type key_file: Path """ + log(f'Unlocking luks2 device: {self.luks_dev_path}', level=logging.DEBUG) + + if not self.mapper_name: + raise ValueError('mapper name missing') + + byte_password = self._password_bytes() + + if not key_file: + if self.key_file: + key_file = self.key_file + else: + key_file = self._default_key_file() - if '/' in mountpoint: - os.path.basename(mountpoint) # TODO: Raise exception instead? + with open(key_file, 'wb') as fh: + fh.write(byte_password) wait_timer = time.time() - while pathlib.Path(partition.path).exists() is False and time.time() - wait_timer < 10: + while Path(self.luks_dev_path).exists() is False and time.time() - wait_timer < 10: time.sleep(0.025) - SysCommand(f'/usr/bin/cryptsetup open {partition.path} {mountpoint} --key-file {os.path.abspath(key_file)} --type luks2') - if os.path.islink(f'/dev/mapper/{mountpoint}'): - self.mapdev = f'/dev/mapper/{mountpoint}' - - if (filesystem_type := get_filesystem_type(pathlib.Path(self.mapdev))) == 'btrfs': - return BTRFSPartition( - self.mapdev, - block_device=MapperDev(mountpoint).partition.block_device, - encrypted=True, - filesystem=filesystem_type, - autodetect_filesystem=False - ) - - return Partition( - self.mapdev, - block_device=MapperDev(mountpoint).partition.block_device, - encrypted=True, - filesystem=get_filesystem_type(self.mapdev), - autodetect_filesystem=False - ) - - def close(self, mountpoint :Optional[str] = None) -> bool: - if not mountpoint: - mountpoint = self.mapdev - - SysCommand(f'/usr/bin/cryptsetup close {self.mapdev}') - return os.path.islink(self.mapdev) is False - - def format(self, path :str) -> None: - if (handle := SysCommand(f"/usr/bin/cryptsetup -q -v luksErase {path}")).exit_code != 0: - raise DiskError(f'Could not format {path} with {self.filesystem} because: {b"".join(handle)}') - - def add_key(self, path :pathlib.Path, password :str) -> bool: - if not path.exists(): - raise OSError(2, f"Could not import {path} as a disk encryption key, file is missing.", str(path)) - - log(f'Adding additional key-file {path} for {self.partition}', level=logging.INFO) - worker = SysCommandWorker(f"/usr/bin/cryptsetup -q -v luksAddKey {self.partition.path} {path}", - environment_vars={'LC_ALL':'C'}) + SysCommand(f'/usr/bin/cryptsetup open {self.luks_dev_path} {self.mapper_name} --key-file {key_file} --type luks2') + + if not self.mapper_dev or not self.mapper_dev.is_symlink(): + raise DiskError(f'Failed to open luks2 device: {self.luks_dev_path}') + + def lock(self): + disk.device_handler.umount(self.luks_dev_path) + + # Get crypt-information about the device by doing a reverse lookup starting with the partition path + # For instance: /dev/sda + disk.device_handler.partprobe(self.luks_dev_path) + lsblk_info = disk.get_lsblk_info(self.luks_dev_path) + + # For each child (sub-partition/sub-device) + for child in lsblk_info.children: + # Unmount the child location + for mountpoint in child.mountpoints: + log(f'Unmounting {mountpoint}', level=logging.DEBUG) + disk.device_handler.umount(mountpoint, recursive=True) + + # And close it if possible. + log(f"Closing crypt device {child.name}", level=logging.DEBUG) + SysCommand(f"cryptsetup close {child.name}") + + self._mapper_dev = None + + def create_keyfile(self, target_path: Path, override: bool = False): + """ + Routine to create keyfiles, so it can be moved elsewhere + """ + if self.mapper_name is None: + raise ValueError('Mapper name must be provided') + + # Once we store the key as ../xyzloop.key systemd-cryptsetup can + # automatically load this key if we name the device to "xyzloop" + key_file_path = target_path / 'etc/cryptsetup-keys.d/' / self.mapper_name + key_file = key_file_path / '.key' + crypttab_path = target_path / 'etc/crypttab' + + if key_file.exists(): + if not override: + log(f'Key file {key_file} already exists, keeping existing') + return + else: + log(f'Key file {key_file} already exists, overriding') + + key_file_path.mkdir(parents=True, exist_ok=True) + + with open(key_file, "w") as keyfile: + keyfile.write(generate_password(length=512)) + + key_file_path.chmod(0o400) + + self._add_key(key_file) + 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) + + command = f'/usr/bin/cryptsetup -q -v luksAddKey {self.luks_dev_path} {key_file}' + worker = SysCommandWorker(command, environment_vars={'LC_ALL': 'C'}) pw_injected = False + while worker.is_alive(): if b'Enter any existing passphrase' in worker and pw_injected is False: - worker.write(bytes(password, 'UTF-8')) + worker.write(self._password_bytes()) pw_injected = True if worker.exit_code != 0: - raise DiskError(f'Could not add encryption key {path} to {self.partition} because: {worker}') - - return True - - def crypttab(self, installation :Installer, key_path :str, options :List[str] = ["luks", "key-slot=1"]) -> None: - log(f'Adding a crypttab entry for key {key_path} in {installation}', level=logging.INFO) - with open(f"{installation.target}/etc/crypttab", "a") as crypttab: - crypttab.write(f"{self.mountpoint} UUID={convert_device_to_uuid(self.partition.path)} {key_path} {','.join(options)}\n") + raise DiskError(f'Could not add encryption key {key_file} to {self.luks_dev_path}: {worker.decode()}') + + def _crypttab( + self, + crypttab_path: Path, + key_file: Path, + options: List[str] + ) -> None: + log(f'Adding crypttab entry for key {key_file}', level=logging.INFO) + + with open(crypttab_path, 'a') as crypttab: + opt = ','.join(options) + uuid = self._get_luks_uuid() + row = f"{self.mapper_name} UUID={uuid} {key_file} {opt}\n" + crypttab.write(row) diff --git a/archinstall/lib/menu/__init__.py b/archinstall/lib/menu/__init__.py index 9b0adb8b..9c86faf5 100644 --- a/archinstall/lib/menu/__init__.py +++ b/archinstall/lib/menu/__init__.py @@ -1,2 +1,9 @@ -from .menu import Menu as Menu -from .global_menu import GlobalMenu as GlobalMenu \ No newline at end of file +from .abstract_menu import Selector, AbstractMenu, AbstractSubMenu +from .list_manager import ListManager +from .menu import ( + MenuSelectionType, + MenuSelection, + Menu, +) +from .table_selection_menu import TableMenu +from .text_input import TextInput diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index d659d709..53816655 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -7,7 +7,6 @@ from .menu import Menu, MenuSelectionType from ..locale_helpers import set_keyboard_language from ..output import log from ..translationhandler import TranslationHandler, Language -from ..user_interaction.general_conf import select_archinstall_language if TYPE_CHECKING: _: Any @@ -16,17 +15,17 @@ if TYPE_CHECKING: class Selector: def __init__( self, - description :str, - func :Optional[Callable] = None, - display_func :Optional[Callable] = None, - default :Any = None, - enabled :bool = False, - dependencies :List = [], - dependencies_not :List = [], - exec_func :Optional[Callable] = None, - preview_func :Optional[Callable] = None, - mandatory :bool = False, - no_store :bool = False + description: str, + func: Optional[Callable[[str], Any]] = None, + display_func: Optional[Callable] = None, + default: Optional[Any] = None, + enabled: bool = False, + dependencies: List = [], + dependencies_not: List = [], + exec_func: Optional[Callable] = None, + preview_func: Optional[Callable] = None, + mandatory: bool = False, + no_store: bool = False ): """ Create a new menu selection entry @@ -82,6 +81,11 @@ class Selector: self._preview_func = preview_func self.mandatory = mandatory self._no_store = no_store + self._default = default + + @property + def default(self) -> Any: + return self._default @property def description(self) -> str: @@ -96,7 +100,7 @@ class Selector: return self._dependencies_not @property - def current_selection(self): + def current_selection(self) -> Optional[Any]: return self._current_selection @property @@ -106,14 +110,14 @@ class Selector: def do_store(self) -> bool: return self._no_store is False - def set_enabled(self, status :bool = True): + def set_enabled(self, status: bool = True): self.enabled = status - def update_description(self, description :str): + def update_description(self, description: str): self._description = description def menu_text(self, padding: int = 0) -> str: - if self._description == '': # special menu option for __separator__ + if self._description == '': # special menu option for __separator__ return '' current = '' @@ -134,7 +138,7 @@ class Selector: return f'{description} {current}' - def set_current_selection(self, current :Optional[str]): + def set_current_selection(self, current: Optional[Any]): self._current_selection = current def has_selection(self) -> bool: @@ -158,14 +162,17 @@ class Selector: def is_mandatory(self) -> bool: return self.mandatory - def set_mandatory(self, status :bool = True): - self.mandatory = status - if status and not self.is_enabled(): - self.set_enabled(True) + def set_mandatory(self, value: bool): + self.mandatory = value class AbstractMenu: - def __init__(self, data_store: Optional[Dict[str, Any]] = None, auto_cursor=False, preview_size :float = 0.2): + def __init__( + self, + data_store: Dict[str, Any] = {}, + auto_cursor: bool = False, + preview_size: float = 0.2 + ): """ Create a new selection menu. @@ -179,25 +186,28 @@ class AbstractMenu: ;type preview_size: float (range 0..1) """ - self._enabled_order :List[str] = [] + self._enabled_order: List[str] = [] self._translation_handler = TranslationHandler() self.is_context_mgr = False - self._data_store = data_store if data_store is not None else {} + self._data_store = data_store self.auto_cursor = auto_cursor self._menu_options: Dict[str, Selector] = {} - self._setup_selection_menu_options() self.preview_size = preview_size self._last_choice = None + self.setup_selection_menu_options() + self._sync_all() + self._populate_default_values() + @property def last_choice(self): return self._last_choice - def __enter__(self, *args :Any, **kwargs :Any) -> AbstractMenu: + def __enter__(self, *args: Any, **kwargs: Any) -> AbstractMenu: self.is_context_mgr = True return self - def __exit__(self, *args :Any, **kwargs :Any) -> None: + def __exit__(self, *args: Any, **kwargs: Any) -> None: # 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]: @@ -216,7 +226,50 @@ class AbstractMenu: def translation_handler(self) -> TranslationHandler: return self._translation_handler - def _setup_selection_menu_options(self): + def _populate_default_values(self): + for config_key, selector in self._menu_options.items(): + if selector.default is not None and config_key not in self._data_store: + self._data_store[config_key] = selector.default + + def _sync_all(self): + for key in self._menu_options.keys(): + self._sync(key) + + def _sync(self, selector_name: str): + value = self._data_store.get(selector_name, None) + selector = self._menu_options.get(selector_name, None) + + if value is not None: + self._menu_options[selector_name].set_current_selection(value) + 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() """ @@ -226,7 +279,7 @@ class AbstractMenu: """ will be called before each action in the menu """ return - def post_callback(self, selection_name: Optional[str] = None, value: Any = None): + def post_callback(self, selection_name: str, value: Any): """ will be called after each action in the menu """ return True @@ -234,31 +287,16 @@ class AbstractMenu: """ will be called at the end of the processing of the menu """ return - def synch(self, selector_name :str, omit_if_set :bool = False,omit_if_disabled :bool = False): - """ loads menu options with data_store value """ - arg = self._data_store.get(selector_name, None) - # don't display the menu option if it was defined already - if arg is not None and omit_if_set: - return - - if not self.option(selector_name).is_enabled() and omit_if_disabled: - return - - if arg is not None: - self._menu_options[selector_name].set_current_selection(arg) - def _update_enabled_order(self, selector_name: str): self._enabled_order.append(selector_name) - def enable(self, selector_name :str, omit_if_set :bool = False , mandatory :bool = False): + def enable(self, selector_name: str, mandatory: bool = False): """ activates menu options """ if self._menu_options.get(selector_name, None): self._menu_options[selector_name].set_enabled(True) self._update_enabled_order(selector_name) - - if mandatory: - self._menu_options[selector_name].set_mandatory(True) - self.synch(selector_name,omit_if_set) + self._menu_options[selector_name].set_mandatory(mandatory) + self._sync(selector_name) else: raise ValueError(f'No selector found: {selector_name}') @@ -274,7 +312,11 @@ class AbstractMenu: def _find_selection(self, selection_name: str) -> Tuple[str, Selector]: enabled_menus = self._menus_to_enable() padding = self._get_menu_text_padding(list(enabled_menus.values())) - option = [(k, v) for k, v in self._menu_options.items() if v.menu_text(padding).strip() == selection_name.strip()] + + option = [] + for k, v in self._menu_options.items(): + if v.menu_text(padding).strip() == selection_name.strip(): + option.append((k, v)) if len(option) != 1: raise ValueError(f'Selection not found: {selection_name}') @@ -283,12 +325,7 @@ class AbstractMenu: return config_name, selector def run(self, allow_reset: bool = False): - """ Calls the Menu framework""" - # we synch all the options just in case - for item in self.list_options(): - self.synch(item) - - self.post_callback() # as all the values can vary i have to exec this callback + self._sync_all() cursor_pos = None while True: @@ -341,13 +378,13 @@ class AbstractMenu: break # we get the last action key - actions = {str(v.description):k for k,v in self._menu_options.items()} + actions = {str(v.description): k for k, v in self._menu_options.items()} self._last_choice = actions[selection.value.strip()] # type: ignore if not self.is_context_mgr: self.__exit__() - def _process_selection(self, selection_name :str) -> bool: + def _process_selection(self, selection_name: str) -> bool: """ determines and executes the selection y Can / Should be extended to handle specific selection issues Returns true if the menu shall continue, False if it has ended @@ -356,7 +393,7 @@ class AbstractMenu: config_name, selector = self._find_selection(selection_name) return self.exec_option(config_name, selector) - def exec_option(self, config_name :str, p_selector :Optional[Selector] = None) -> bool: + def exec_option(self, config_name: str, p_selector: Optional[Selector] = None) -> bool: """ processes the execution of a given menu entry - pre process callback - selection function @@ -372,17 +409,21 @@ class AbstractMenu: self.pre_callback(config_name) result = None + if selector.func is not None: presel_val = self.option(config_name).get_selection() result = selector.func(presel_val) self._menu_options[config_name].set_current_selection(result) if selector.do_store(): self._data_store[config_name] = result - exec_ret_val = selector.exec_func(config_name,result) if selector.exec_func is not None else False - self.post_callback(config_name,result) - if exec_ret_val and self._check_mandatory_status(): + exec_ret_val = selector.exec_func(config_name, result) if selector.exec_func else False + + self.post_callback(config_name, result) + + if exec_ret_val: return False + return True def _set_kb_language(self): @@ -392,7 +433,7 @@ class AbstractMenu: 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: + def _verify_selection_enabled(self, selection_name: str) -> bool: """ general """ if selection := self._menu_options.get(selection_name, None): if not selection.enabled: @@ -429,16 +470,10 @@ class AbstractMenu: return ordered_menus - def option(self,name :str) -> Selector: + def option(self, name: str) -> Selector: # TODO check inexistent name return self._menu_options[name] - def list_options(self) -> Iterator: - """ Iterator to retrieve the enabled menu option names - """ - for item in self._menu_options: - yield item - def list_enabled_options(self) -> Iterator: """ Iterator to retrieve the enabled menu options at a given time. The results are dynamic (if between calls to the iterator some elements -still not retrieved- are (de)activated @@ -447,44 +482,21 @@ class AbstractMenu: if item in self._menus_to_enable(): yield item - def set_option(self, name :str, selector :Selector): - self._menu_options[name] = selector - self.synch(name) - - def _check_mandatory_status(self) -> bool: - for field in self._menu_options: - option = self._menu_options[field] - if option.is_mandatory() and not option.has_selection(): - return False - return True - - def set_mandatory(self, field :str, status :bool): - self.option(field).set_mandatory(status) - - def mandatory_overview(self) -> Tuple[int, int]: - mandatory_fields = 0 - mandatory_waiting = 0 - for field, option in self._menu_options.items(): - if option.is_mandatory(): - mandatory_fields += 1 - if not option.has_selection(): - mandatory_waiting += 1 - return mandatory_fields, mandatory_waiting - def _select_archinstall_language(self, preset_value: Language) -> Language: + from ..user_interaction.general_conf import select_archinstall_language language = select_archinstall_language(self.translation_handler.translated_languages, preset_value) self._translation_handler.activate(language) return language class AbstractSubMenu(AbstractMenu): - def __init__(self, data_store: Optional[Dict[str, Any]] = None): + def __init__(self, data_store: Dict[str, Any] = {}): super().__init__(data_store=data_store) self._menu_options['__separator__'] = Selector('') self._menu_options['back'] = \ Selector( - _('Back'), + Menu.back(), no_store=True, enabled=True, exec_func=lambda n, v: True, diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py deleted file mode 100644 index 7c5b153e..00000000 --- a/archinstall/lib/menu/global_menu.py +++ /dev/null @@ -1,429 +0,0 @@ -from __future__ import annotations - -from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING - -import archinstall -from ..disk.encryption import DiskEncryptionMenu -from ..general import SysCommand, secret -from ..hardware import has_uefi -from ..menu import Menu -from ..menu.abstract_menu import Selector, AbstractMenu -from ..models import NetworkConfiguration -from ..models.disk_encryption import DiskEncryption, EncryptionType -from ..models.users import User -from ..output import FormattedOutput -from ..profiles import is_desktop_profile, Profile -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, save_config -from ..user_interaction import select_additional_repositories -from ..user_interaction import select_disk_layout -from ..user_interaction import select_harddrives -from ..user_interaction import select_kernel -from ..user_interaction import select_language -from ..user_interaction import select_locale_enc -from ..user_interaction import select_locale_lang -from ..user_interaction import select_mirror_regions -from ..user_interaction import select_profile -from ..user_interaction.partitioning_conf import current_partition_layout - -if TYPE_CHECKING: - _: Any - - -class GlobalMenu(AbstractMenu): - def __init__(self,data_store): - self._disk_check = True - super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3) - - def _setup_selection_menu_options(self): - # archinstall.Language will not use preset values - self._menu_options['archinstall-language'] = \ - Selector( - _('Archinstall language'), - lambda x: self._select_archinstall_language(x), - display_func=lambda x: x.display_name, - default=self.translation_handler.get_language_by_abbr('en')) - self._menu_options['keyboard-layout'] = \ - Selector( - _('Keyboard layout'), - lambda preset: select_language(preset), - default='us') - self._menu_options['mirror-region'] = \ - Selector( - _('Mirror region'), - lambda preset: select_mirror_regions(preset), - display_func=lambda x: list(x.keys()) if x else '[]', - default={}) - 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['harddrives'] = \ - Selector( - _('Drive(s)'), - lambda preset: self._select_harddrives(preset), - display_func=lambda x: f'{len(x)} ' + str(_('Drive(s)')) if x is not None and len(x) > 0 else '', - preview_func=self._prev_harddrives, - ) - self._menu_options['disk_layouts'] = \ - Selector( - _('Disk layout'), - lambda preset: select_disk_layout( - preset, - storage['arguments'].get('harddrives', []), - storage['arguments'].get('advanced', False) - ), - preview_func=self._prev_disk_layouts, - display_func=lambda x: self._display_disk_layout(x), - dependencies=['harddrives']) - self._menu_options['disk_encryption'] = \ - Selector( - _('Disk encryption'), - lambda preset: self._disk_encryption(preset), - preview_func=self._prev_disk_encryption, - display_func=lambda x: self._display_disk_encryption(x), - dependencies=['disk_layouts']) - self._menu_options['swap'] = \ - Selector( - _('Swap'), - lambda preset: ask_for_swap(preset), - default=True) - self._menu_options['bootloader'] = \ - Selector( - _('Bootloader'), - lambda preset: ask_for_bootloader(storage['arguments'].get('advanced', False),preset), - default="systemd-bootctl" if has_uefi() else "grub-install") - self._menu_options['hostname'] = \ - Selector( - _('Hostname'), - ask_hostname, - default='archlinux') - # root password won't have preset value - self._menu_options['!root-password'] = \ - Selector( - _('Root password'), - lambda preset:self._set_root_password(), - display_func=lambda x: secret(x) if x else 'None') - 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, - preview_func=self._prev_users) - self._menu_options['profile'] = \ - Selector( - _('Profile'), - lambda preset: self._select_profile(preset), - display_func=lambda x: x if x else 'None' - ) - self._menu_options['audio'] = \ - Selector( - _('Audio'), - lambda preset: ask_for_audio_selection(is_desktop_profile(storage['arguments'].get('profile', None)),preset), - display_func=lambda x: x if x else 'None', - default=None - ) - - self._menu_options['parallel downloads'] = \ - Selector( - _('Parallel Downloads'), - add_number_of_parrallel_downloads, - display_func=lambda x: x if x else '0', - default=0 - ) - - self._menu_options['kernels'] = \ - Selector( - _('Kernels'), - lambda preset: select_kernel(preset), - default=['linux']) - self._menu_options['packages'] = \ - Selector( - _('Additional packages'), - # lambda x: ask_additional_packages_to_install(storage['arguments'].get('packages', None)), - ask_additional_packages_to_install, - default=[]) - self._menu_options['additional-repositories'] = \ - Selector( - _('Optional repositories'), - select_additional_repositories, - default=[]) - self._menu_options['nic'] = \ - Selector( - _('Network configuration'), - ask_to_configure_network, - display_func=lambda x: self._display_network_conf(x), - preview_func=self._prev_network_config, - default={}) - self._menu_options['timezone'] = \ - Selector( - _('Timezone'), - lambda preset: ask_for_a_timezone(preset), - default='UTC') - self._menu_options['ntp'] = \ - Selector( - _('Automatic time sync (NTP)'), - lambda preset: self._select_ntp(preset), - default=True) - self._menu_options['__separator__'] = \ - Selector('') - self._menu_options['save_config'] = \ - Selector( - _('Save configuration'), - lambda preset: save_config(self._data_store), - no_store=True) - self._menu_options['install'] = \ - Selector( - self._install_text(), - exec_func=lambda n,v: True if len(self._missing_configs()) == 0 else False, - preview_func=self._prev_install_missing_config, - no_store=True) - - self._menu_options['abort'] = Selector(_('Abort'), exec_func=lambda n,v:exit(1)) - - def _update_install_text(self, name :Optional[str] = None, result :Any = None): - text = self._install_text() - self._menu_options['install'].update_description(text) - - def post_callback(self,name :Optional[str] = None ,result :Any = None): - self._update_install_text(name, result) - - def _install_text(self): - missing = len(self._missing_configs()) - if missing > 0: - return _('Install ({} config(s) missing)').format(missing) - return _('Install') - - def _display_network_conf(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str: - if not cur_value: - return _('Not configured, unavailable unless setup manually') - else: - if isinstance(cur_value, list): - return str(_('Configured {} interfaces')).format(len(cur_value)) - else: - return str(cur_value) - - def _disk_encryption(self, preset: Optional[DiskEncryption]) -> Optional[DiskEncryption]: - data_store: Dict[str, Any] = {} - - selector = self._menu_options['disk_layouts'] - - if selector.has_selection(): - layouts: Dict[str, Dict[str, Any]] = selector.current_selection - else: - # this should not happen as the encryption menu has the disk layout as dependency - raise ValueError('No disk layout specified') - - disk_encryption = DiskEncryptionMenu(data_store, preset, layouts).run() - return disk_encryption - - def _prev_network_config(self) -> Optional[str]: - selector = self._menu_options['nic'] - if selector.has_selection(): - ifaces = selector.current_selection - if isinstance(ifaces, list): - return FormattedOutput.as_table(ifaces) - return None - - def _prev_harddrives(self) -> Optional[str]: - selector = self._menu_options['harddrives'] - if selector.has_selection(): - drives = selector.current_selection - return FormattedOutput.as_table(drives) - return None - - def _prev_disk_layouts(self) -> Optional[str]: - selector = self._menu_options['disk_layouts'] - if selector.has_selection(): - layouts: Dict[str, Dict[str, Any]] = selector.current_selection - - output = '' - for device, layout in layouts.items(): - output += f'{_("Device")}: {device}\n\n' - output += current_partition_layout(layout['partitions'], with_title=False) - output += '\n\n' - - return output.rstrip() - - return None - - def _display_disk_layout(self, current_value: Optional[Dict[str, Any]]) -> str: - if current_value: - total_partitions = [entry['partitions'] for entry in current_value.values()] - total_nr = sum([len(p) for p in total_partitions]) - return f'{total_nr} {_("Partitions")}' - return '' - - def _prev_disk_encryption(self) -> Optional[str]: - selector = self._menu_options['disk_encryption'] - if selector.has_selection(): - encryption: DiskEncryption = selector.current_selection - - enc_type = EncryptionType.type_to_text(encryption.encryption_type) - output = str(_('Encryption type')) + f': {enc_type}\n' - output += str(_('Password')) + f': {secret(encryption.encryption_password)}\n' - - if encryption.all_partitions: - output += 'Partitions: {} selected'.format(len(encryption.all_partitions)) + '\n' - - if encryption.hsm_device: - output += f'HSM: {encryption.hsm_device.manufacturer}' - - return output - - return None - - def _display_disk_encryption(self, current_value: Optional[DiskEncryption]) -> str: - if current_value: - return EncryptionType.type_to_text(current_value.encryption_type) - return '' - - def _prev_install_missing_config(self) -> Optional[str]: - if missing := self._missing_configs(): - text = str(_('Missing configurations:\n')) - for m in missing: - text += f'- {m}\n' - return text[:-1] # remove last new line - return None - - def _prev_users(self) -> Optional[str]: - selector = self._menu_options['!users'] - if selector.has_selection(): - users: List[User] = selector.current_selection - return FormattedOutput.as_table(users) - return None - - def _missing_configs(self) -> List[str]: - def check(s): - return self._menu_options.get(s).has_selection() - - def has_superuser() -> bool: - users = self._menu_options['!users'].current_selection - return any([u.sudo for u in users]) - - missing = [] - if not check('bootloader'): - missing += ['Bootloader'] - if not check('hostname'): - missing += ['Hostname'] - if not check('!root-password') and not has_superuser(): - missing += [str(_('Either root-password or at least 1 user with sudo privileges must be specified'))] - if self._disk_check: - if not check('harddrives'): - missing += [str(_('Drive(s)'))] - if check('harddrives'): - if not self._menu_options['harddrives'].is_empty() and not check('disk_layouts'): - missing += [str(_('Disk layout'))] - - return missing - - def _set_root_password(self) -> Optional[str]: - prompt = str(_('Enter root password (leave blank to disable root): ')) - password = get_password(prompt=prompt) - return password - - # def _select_encrypted_password(self) -> Optional[str]: - # if passwd := get_password(prompt=str(_('Enter disk encryption password (leave blank for no encryption): '))): - # return passwd - # return None - - def _select_ntp(self, preset :bool = True) -> bool: - ntp = ask_ntp(preset) - - value = str(ntp).lower() - SysCommand(f'timedatectl set-ntp {value}') - - return ntp - - def _select_harddrives(self, old_harddrives: List[str] = []) -> List: - harddrives = select_harddrives(old_harddrives) - - if harddrives is not None: - if len(harddrives) == 0: - prompt = _( - "You decided to skip harddrive selection\nand will use whatever drive-setup is mounted at {} (experimental)\n" - "WARNING: Archinstall won't check the suitability of this setup\n" - "Do you wish to continue?" - ).format(storage['MOUNT_POINT']) - - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), skip=False).run() - - if choice.value == Menu.no(): - self._disk_check = True - return self._select_harddrives(old_harddrives) - else: - self._disk_check = False - - # in case the harddrives got changed we have to reset the disk layout as well - if old_harddrives != harddrives: - self._menu_options['disk_layouts'].set_current_selection(None) - storage['arguments']['disk_layouts'] = {} - - return harddrives - - def _select_profile(self, preset) -> Optional[Profile]: - ret: Optional[Profile] = None - profile = select_profile(preset) - - if profile is None: - if any([ - archinstall.storage.get('profile_minimal', False), - archinstall.storage.get('_selected_servers', None), - archinstall.storage.get('_desktop_profile', None), - archinstall.arguments.get('desktop-environment', None), - archinstall.arguments.get('gfx_driver_packages', None) - ]): - return preset - else: # ctrl+c was actioned and all profile settings have been reset - return None - - servers = archinstall.storage.get('_selected_servers', []) - desktop = archinstall.storage.get('_desktop_profile', None) - desktop_env = archinstall.arguments.get('desktop-environment', None) - gfx_driver = archinstall.arguments.get('gfx_driver_packages', None) - - # Check the potentially selected profiles preparations to get early checks if some additional questions are needed. - if profile and profile.has_prep_function(): - namespace = f'{profile.namespace}.py' - with profile.load_instructions(namespace=namespace) as imported: - if imported._prep_function(servers=servers, desktop=desktop, desktop_env=desktop_env, gfx_driver=gfx_driver): - ret = profile - - match ret.name: - case 'minimal': - reset = ['_selected_servers', '_desktop_profile', 'desktop-environment', 'gfx_driver_packages'] - case 'server': - reset = ['_desktop_profile', 'desktop-environment'] - case 'desktop': - reset = ['_selected_servers'] - case 'xorg': - reset = ['_selected_servers', '_desktop_profile', 'desktop-environment'] - - for r in reset: - archinstall.storage[r] = None - else: - return self._select_profile(preset) - elif profile: - ret = profile - - return ret - - def _create_user_account(self, defined_users: List[User]) -> List[User]: - users = ask_for_additional_users(defined_users=defined_users) - return users diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index 1e09d987..be31fdf0 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -34,7 +34,7 @@ class ListManager: self._data = copy.deepcopy(entries) explainer = str(_('\n Choose an object from the list, and select one of the available actions for it to execute')) - self._prompt = prompt + explainer if prompt else explainer + self._prompt = prompt if prompt else explainer self._separator = '' self._confirm_action = str(_('Confirm and exit')) @@ -44,13 +44,18 @@ class ListManager: self._base_actions = base_actions self._sub_menu_actions = sub_menu_actions - self._last_choice = None + self._last_choice: Optional[str] = None @property - def last_choice(self): + def last_choice(self) -> Optional[str]: return self._last_choice - def run(self): + def is_last_choice_cancel(self) -> bool: + if self._last_choice is not None: + return self._last_choice == self._cancel_action + return False + + def run(self) -> List[Any]: while True: # this will return a dictionary with the key as the menu entry to be displayed # and the value is the original value from the self._data container @@ -76,10 +81,11 @@ class ListManager: elif choice.value in self._terminate_actions: break else: # an entry of the existing selection was choosen - selected_entry = data_formatted[choice.value] + selected_entry = data_formatted[choice.value] # type: ignore self._run_actions_on_entry(selected_entry) - self._last_choice = choice + self._last_choice = choice.value # type: ignore + if choice.value == self._cancel_action: return self._original_data # return the original list else: @@ -122,21 +128,29 @@ class ListManager: self._data = self.handle_action(choice.value, entry, self._data) def selected_action_display(self, selection: Any) -> str: - # this will return the value to be displayed in the - # "Select an action for '{}'" string + """ + this will return the value to be displayed in the + "Select an action for '{}'" string + """ raise NotImplementedError('Please implement me in the child class') - def reformat(self, data: List[Any]) -> Dict[str, Any]: - # this should return a dictionary of display string to actual data entry - # mapping; if the value for a given display string is None it will be used - # in the header value (useful when displaying tables) + def reformat(self, data: List[Any]) -> Dict[str, Optional[Any]]: + """ + this should return a dictionary of display string to actual data entry + mapping; if the value for a given display string is None it will be used + in the header value (useful when displaying tables) + """ raise NotImplementedError('Please implement me in the child class') def handle_action(self, action: Any, entry: Optional[Any], data: List[Any]) -> List[Any]: - # this function is called when a base action or - # a specific action for an entry is triggered + """ + this function is called when a base action or + a specific action for an entry is triggered + """ raise NotImplementedError('Please implement me in the child class') - def filter_options(self, selection :Any, options :List[str]) -> List[str]: - # filter which actions to show for an specific selection + def filter_options(self, selection: Any, options: List[str]) -> List[str]: + """ + filter which actions to show for an specific selection + """ return options diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 09685c55..44ac33a6 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -3,7 +3,7 @@ from enum import Enum, auto from os import system from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional, Callable -from .simple_menu import TerminalMenu +from simple_term_menu import TerminalMenu from ..exceptions import RequirementError from ..output import log @@ -27,42 +27,56 @@ class MenuSelection: type_: MenuSelectionType value: Optional[Union[str, List[str]]] = None + @property + def single_value(self) -> Any: + return self.value + + @property + def multi_value(self) -> List[Any]: + return self.value + class Menu(TerminalMenu): @classmethod - def yes(cls): + def back(cls) -> str: + return str(_('← Back')) + + @classmethod + def yes(cls) -> str: return str(_('yes')) @classmethod - def no(cls): + def no(cls) -> str: return str(_('no')) @classmethod - def yes_no(cls): + def yes_no(cls) -> List[str]: return [cls.yes(), cls.no()] def __init__( self, - title :str, - p_options :Union[List[str], Dict[str, Any]], - skip :bool = True, - multi :bool = False, - default_option : Optional[str] = None, - sort :bool = True, - preset_values :Union[str, List[str]] = None, - cursor_index : Optional[int] = None, + title: str, + p_options: Union[List[str], Dict[str, Any]], + skip: bool = True, + multi: bool = False, + default_option: Optional[str] = None, + sort: bool = True, + preset_values: Optional[Union[str, List[str]]] = None, + cursor_index: Optional[int] = None, preview_command: Optional[Callable] = None, preview_size: float = 0.0, preview_title: str = 'Info', - header :Union[List[str],str] = None, - allow_reset :bool = False, - allow_reset_warning_msg :str = '', + header: Union[List[str],str] = None, + allow_reset: bool = False, + allow_reset_warning_msg: Optional[str] = None, clear_screen: bool = True, show_search_hint: bool = True, cycle_cursor: bool = True, clear_menu_on_exit: bool = True, - skip_empty_entries: bool = False + skip_empty_entries: bool = False, + display_back_option: bool = False, + extra_bottom_space: bool = False ): """ Creates a new menu @@ -72,7 +86,7 @@ class Menu(TerminalMenu): :param p_options: Options to be displayed in the menu to chose from; if dict is specified then the keys of such will be used as options - :type options: list, dict + :type p_options: list, dict :param skip: Indicate if the selection is not mandatory and can be skipped :type skip: bool @@ -101,16 +115,17 @@ class Menu(TerminalMenu): :param preview_title: Title of the preview window :type preview_title: str - param header: one or more header lines for the menu - type param: string or list + :param header: one or more header lines for the menu + :type header: string or list - param raise_error_on_interrupt: This will explicitly handle a ctrl+c instead and return that specific state - type param: bool + :param allow_reset: This will explicitly handle a ctrl+c instead and return that specific state + :type allow_reset: bool - param raise_error_warning_msg: If raise_error_on_interrupt is True and this is non-empty, there will be a warning with a user confirmation displayed - type param: str + param allow_reset_warning_msg: If raise_error_on_interrupt is True the warnign is set, a user confirmation is displayed + type allow_reset_warning_msg: str - :param kwargs : any SimpleTerminal parameter + :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 @@ -152,7 +167,6 @@ class Menu(TerminalMenu): self._multi = multi self._raise_error_on_interrupt = allow_reset self._raise_error_warning_msg = allow_reset_warning_msg - self._preview_command = preview_command action_info = '' if skip: @@ -182,6 +196,14 @@ class Menu(TerminalMenu): default = f'{default_option} {self._default_str}' self._menu_options = [default] + [o for o in self._menu_options if default_option != o] + if display_back_option and not multi and skip: + skip_empty_entries = True + self._menu_options += ['', self.back()] + + if extra_bottom_space: + skip_empty_entries = True + self._menu_options += [''] + self._preselection(preset_values,cursor_index) cursor = "> " @@ -194,13 +216,10 @@ class Menu(TerminalMenu): menu_cursor=cursor, menu_cursor_style=main_menu_cursor_style, menu_highlight_style=main_menu_style, - # cycle_cursor=True, - # clear_screen=True, multi_select=multi, - # show_search_hint=True, preselected_entries=self.preset_values, cursor_index=self.cursor_index, - preview_command=lambda x: self._preview_wrapper(preview_command, x), + preview_command=lambda x: self._show_preview(preview_command, x), preview_size=preview_size, preview_title=preview_title, raise_error_on_interrupt=self._raise_error_on_interrupt, @@ -212,6 +231,17 @@ class Menu(TerminalMenu): skip_empty_entries=skip_empty_entries ) + def _show_preview(self, preview_command: Optional[Callable], selection: str) -> Optional[str]: + if selection == self.back(): + return None + + if preview_command: + if self._default_option is not None and f'{self._default_option} {self._default_str}' == selection: + selection = self._default_option + return preview_command(selection) + + return None + def _show(self) -> MenuSelection: try: idx = self.show() @@ -225,39 +255,37 @@ class Menu(TerminalMenu): return elem if idx is not None: - if isinstance(idx, (list, tuple)): + if isinstance(idx, (list, tuple)): # on multi selection results = [] for i in idx: option = check_default(self._menu_options[i]) results.append(option) return MenuSelection(type_=MenuSelectionType.Selection, value=results) - else: + else: # on single selection result = check_default(self._menu_options[idx]) return MenuSelection(type_=MenuSelectionType.Selection, value=result) else: return MenuSelection(type_=MenuSelectionType.Skip) - def _preview_wrapper(self, preview_command: Optional[Callable], current_selection: str) -> Optional[str]: - if preview_command: - if self._default_option is not None and f'{self._default_option} {self._default_str}' == current_selection: - current_selection = self._default_option - return preview_command(current_selection) - return None - def run(self) -> MenuSelection: - ret = self._show() + selection = self._show() - if ret.type_ == MenuSelectionType.Reset: - if self._raise_error_on_interrupt and len(self._raise_error_warning_msg) > 0: + if selection.type_ == MenuSelectionType.Reset: + if self._raise_error_on_interrupt and self._raise_error_warning_msg is not None: response = Menu(self._raise_error_warning_msg, Menu.yes_no(), skip=False).run() if response.value == Menu.no(): return self.run() - elif ret.type_ is MenuSelectionType.Skip: + elif selection.type_ is MenuSelectionType.Skip: if not self._skip: system('clear') return self.run() - return ret + if selection.type_ == MenuSelectionType.Selection: + if selection.value == self.back(): + selection.type_ = MenuSelectionType.Skip + selection.value = None + + return selection def set_cursor_pos(self,pos :int): if pos and 0 < pos < len(self._menu_entries): diff --git a/archinstall/lib/menu/simple_menu.py b/archinstall/lib/menu/simple_menu.py deleted file mode 100644 index 1980e2ce..00000000 --- a/archinstall/lib/menu/simple_menu.py +++ /dev/null @@ -1,2002 +0,0 @@ -""" -This file is copied over from the simple-term-menu project -(https://github.com/IngoMeyer441/simple-term-menu) -In order to comply with installation methods of Arch Linux. -We here by copy the MIT license attached to the project at the time of copy: - -Copyright 2021 Forschungszentrum Jülich GmbH - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" -import argparse -import copy -import ctypes -import io -import locale -import os -import platform -import re -import shlex -import signal -import string -import subprocess -import sys -from locale import getlocale -from types import FrameType -from typing import ( - Any, - Callable, - Dict, - Iterable, - Iterator, - List, - Match, - Optional, - Pattern, - Sequence, - Set, - TextIO, - Tuple, - Union, - cast, -) - -try: - import termios -except ImportError as e: - raise NotImplementedError('"{}" is currently not supported.'.format(platform.system())) from e - -__author__ = "Ingo Meyer" -__email__ = "i.meyer@fz-juelich.de" -__copyright__ = "Copyright © 2021 Forschungszentrum Jülich GmbH. All rights reserved." -__license__ = "MIT" -__version_info__ = (1, 5, 0) -__version__ = ".".join(map(str, __version_info__)) - - -DEFAULT_ACCEPT_KEYS = ("enter",) -DEFAULT_CLEAR_MENU_ON_EXIT = True -DEFAULT_CLEAR_SCREEN = False -DEFAULT_CYCLE_CURSOR = True -DEFAULT_EXIT_ON_SHORTCUT = True -DEFAULT_MENU_CURSOR = "> " -DEFAULT_MENU_CURSOR_STYLE = ("fg_red", "bold") -DEFAULT_MENU_HIGHLIGHT_STYLE = ("standout",) -DEFAULT_MULTI_SELECT = False -DEFAULT_MULTI_SELECT_CURSOR = "[*] " -DEFAULT_MULTI_SELECT_CURSOR_BRACKETS_STYLE = ("fg_gray",) -DEFAULT_MULTI_SELECT_CURSOR_STYLE = ("fg_yellow", "bold") -DEFAULT_MULTI_SELECT_KEYS = (" ", "tab") -DEFAULT_MULTI_SELECT_SELECT_ON_ACCEPT = True -DEFAULT_PREVIEW_BORDER = True -DEFAULT_PREVIEW_SIZE = 0.25 -DEFAULT_PREVIEW_TITLE = "preview" -DEFAULT_QUIT_KEYS = ("escape", "q") -DEFAULT_SEARCH_CASE_SENSITIVE = False -DEFAULT_SEARCH_HIGHLIGHT_STYLE = ("fg_black", "bg_yellow", "bold") -DEFAULT_SEARCH_KEY = "/" -DEFAULT_SHORTCUT_BRACKETS_HIGHLIGHT_STYLE = ("fg_gray",) -DEFAULT_SHORTCUT_KEY_HIGHLIGHT_STYLE = ("fg_blue",) -DEFAULT_SHOW_MULTI_SELECT_HINT = False -DEFAULT_SHOW_SEARCH_HINT = False -DEFAULT_SHOW_SHORTCUT_HINTS = False -DEFAULT_SHOW_SHORTCUT_HINTS_IN_STATUS_BAR = True -DEFAULT_STATUS_BAR_BELOW_PREVIEW = False -DEFAULT_STATUS_BAR_STYLE = ("fg_yellow", "bg_black") -MIN_VISIBLE_MENU_ENTRIES_COUNT = 3 - - -class InvalidParameterCombinationError(Exception): - pass - - -class InvalidStyleError(Exception): - pass - - -class NoMenuEntriesError(Exception): - pass - - -class PreviewCommandFailedError(Exception): - pass - - -class UnknownMenuEntryError(Exception): - pass - - -def get_locale() -> str: - user_locale = locale.getlocale()[1] - if user_locale is None: - return "ascii" - else: - return user_locale.lower() - - -def wcswidth(text: str) -> int: - if not hasattr(wcswidth, "libc"): - if platform.system() == "Darwin": - wcswidth.libc = ctypes.cdll.LoadLibrary("libSystem.dylib") # type: ignore - else: - wcswidth.libc = ctypes.cdll.LoadLibrary("libc.so.6") # type: ignore - user_locale = get_locale() - # First replace any null characters with the unicode replacement character (U+FFFD) since they cannot be passed - # in a `c_wchar_p` - encoded_text = text.replace("\0", "\uFFFD").encode(encoding=user_locale, errors="replace") - return wcswidth.libc.wcswidth( # type: ignore - ctypes.c_wchar_p(encoded_text.decode(encoding=user_locale)), len(encoded_text) - ) - - -def static_variables(**variables: Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]: - def decorator(f: Callable[..., Any]) -> Callable[..., Any]: - for key, value in variables.items(): - setattr(f, key, value) - return f - - return decorator - - -class BoxDrawingCharacters: - if getlocale()[1] == "UTF-8": - # Unicode box characters - horizontal = "─" - vertical = "│" - upper_left = "┌" - upper_right = "┐" - lower_left = "└" - lower_right = "┘" - else: - # ASCII box characters - horizontal = "-" - vertical = "|" - upper_left = "+" - upper_right = "+" - lower_left = "+" - lower_right = "+" - - -class TerminalMenu: - class Search: - def __init__( - self, - menu_entries: Iterable[str], - search_text: Optional[str] = None, - case_senitive: bool = False, - show_search_hint: bool = False, - ): - self._menu_entries = menu_entries - self._case_sensitive = case_senitive - self._show_search_hint = show_search_hint - self._matches = [] # type: List[Tuple[int, Match[str]]] - self._search_regex = None # type: Optional[Pattern[str]] - self._change_callback = None # type: Optional[Callable[[], None]] - # Use the property setter since it has some more logic - self.search_text = search_text - - def _update_matches(self) -> None: - if self._search_regex is None: - self._matches = [] - else: - matches = [] - for i, menu_entry in enumerate(self._menu_entries): - match_obj = self._search_regex.search(menu_entry) - if match_obj: - matches.append((i, match_obj)) - self._matches = matches - - @property - def matches(self) -> List[Tuple[int, Match[str]]]: - return list(self._matches) - - @property - def search_regex(self) -> Optional[Pattern[str]]: - return self._search_regex - - @property - def search_text(self) -> Optional[str]: - return self._search_text - - @search_text.setter - def search_text(self, text: Optional[str]) -> None: - self._search_text = text - search_text = self._search_text - self._search_regex = None - while search_text and self._search_regex is None: - try: - self._search_regex = re.compile(search_text, flags=re.IGNORECASE if not self._case_sensitive else 0) - except re.error: - search_text = search_text[:-1] - self._update_matches() - if self._change_callback: - self._change_callback() - - @property - def change_callback(self) -> Optional[Callable[[], None]]: - return self._change_callback - - @change_callback.setter - def change_callback(self, callback: Optional[Callable[[], None]]) -> None: - self._change_callback = callback - - @property - def occupied_lines_count(self) -> int: - if not self and not self._show_search_hint: - return 0 - else: - return 1 - - def __bool__(self) -> bool: - return self._search_text is not None - - def __contains__(self, menu_index: int) -> bool: - return any(i == menu_index for i, _ in self._matches) - - def __len__(self) -> int: - return wcswidth(self._search_text) if self._search_text is not None else 0 - - class Selection: - def __init__(self, num_menu_entries: int, preselected_indices: Optional[Iterable[int]] = None): - self._num_menu_entries = num_menu_entries - self._selected_menu_indices = set(preselected_indices) if preselected_indices is not None else set() - - def clear(self) -> None: - self._selected_menu_indices.clear() - - def add(self, menu_index: int) -> None: - self[menu_index] = True - - def remove(self, menu_index: int) -> None: - self[menu_index] = False - - def toggle(self, menu_index: int) -> bool: - self[menu_index] = menu_index not in self._selected_menu_indices - return self[menu_index] - - def __bool__(self) -> bool: - return bool(self._selected_menu_indices) - - def __contains__(self, menu_index: int) -> bool: - return menu_index in self._selected_menu_indices - - def __getitem__(self, menu_index: int) -> bool: - return menu_index in self._selected_menu_indices - - def __setitem__(self, menu_index: int, is_selected: bool) -> None: - if is_selected: - self._selected_menu_indices.add(menu_index) - else: - self._selected_menu_indices.remove(menu_index) - - def __iter__(self) -> Iterator[int]: - return iter(self._selected_menu_indices) - - @property - def selected_menu_indices(self) -> Tuple[int, ...]: - return tuple(sorted(self._selected_menu_indices)) - - class View: - def __init__( - self, - menu_entries: Iterable[str], - search: "TerminalMenu.Search", - selection: "TerminalMenu.Selection", - viewport: "TerminalMenu.Viewport", - cycle_cursor: bool = True, - skip_indices: List[int] = [], - ): - self._menu_entries = list(menu_entries) - self._search = search - self._selection = selection - self._viewport = viewport - self._cycle_cursor = cycle_cursor - self._active_displayed_index = None # type: Optional[int] - self._skip_indices = skip_indices - self.update_view() - - def update_view(self) -> None: - if self._search and self._search.search_text != "": - self._displayed_index_to_menu_index = tuple(i for i, match_obj in self._search.matches) - else: - self._displayed_index_to_menu_index = tuple(range(len(self._menu_entries))) - self._menu_index_to_displayed_index = { - menu_index: displayed_index - for displayed_index, menu_index in enumerate(self._displayed_index_to_menu_index) - } - self._active_displayed_index = 0 if self._displayed_index_to_menu_index else None - self._viewport.search_lines_count = self._search.occupied_lines_count - self._viewport.keep_visible(self._active_displayed_index) - - def increment_active_index(self) -> None: - if self._active_displayed_index is not None: - if self._active_displayed_index + 1 < len(self._displayed_index_to_menu_index): - self._active_displayed_index += 1 - elif self._cycle_cursor: - self._active_displayed_index = 0 - self._viewport.keep_visible(self._active_displayed_index) - - if self._active_displayed_index in self._skip_indices: - self.increment_active_index() - - def decrement_active_index(self) -> None: - if self._active_displayed_index is not None: - if self._active_displayed_index > 0: - self._active_displayed_index -= 1 - elif self._cycle_cursor: - self._active_displayed_index = len(self._displayed_index_to_menu_index) - 1 - self._viewport.keep_visible(self._active_displayed_index) - - if self._active_displayed_index in self._skip_indices: - self.decrement_active_index() - - def is_visible(self, menu_index: int) -> bool: - return menu_index in self._menu_index_to_displayed_index and ( - self._viewport.lower_index - <= self._menu_index_to_displayed_index[menu_index] - <= self._viewport.upper_index - ) - - def convert_menu_index_to_displayed_index(self, menu_index: int) -> Optional[int]: - if menu_index in self._menu_index_to_displayed_index: - return self._menu_index_to_displayed_index[menu_index] - else: - return None - - def convert_displayed_index_to_menu_index(self, displayed_index: int) -> int: - return self._displayed_index_to_menu_index[displayed_index] - - @property - def active_menu_index(self) -> Optional[int]: - if self._active_displayed_index is not None: - return self._displayed_index_to_menu_index[self._active_displayed_index] - else: - return None - - @active_menu_index.setter - def active_menu_index(self, value: int) -> None: - self._selected_index = value - self._active_displayed_index = [ - displayed_index - for displayed_index, menu_index in enumerate(self._displayed_index_to_menu_index) - if menu_index == value - ][0] - self._viewport.keep_visible(self._active_displayed_index) - - @property - def active_displayed_index(self) -> Optional[int]: - return self._active_displayed_index - - @property - def displayed_selected_indices(self) -> List[int]: - return [ - self._menu_index_to_displayed_index[selected_index] - for selected_index in self._selection - if selected_index in self._menu_index_to_displayed_index - ] - - def __bool__(self) -> bool: - return self._active_displayed_index is not None - - def __iter__(self) -> Iterator[Tuple[int, int, str]]: - for displayed_index, menu_index in enumerate(self._displayed_index_to_menu_index): - if self._viewport.lower_index <= displayed_index <= self._viewport.upper_index: - yield (displayed_index, menu_index, self._menu_entries[menu_index]) - - class Viewport: - def __init__( - self, - num_menu_entries: int, - title_lines_count: int, - status_bar_lines_count: int, - preview_lines_count: int, - search_lines_count: int, - ): - self._num_menu_entries = num_menu_entries - self._title_lines_count = title_lines_count - self._status_bar_lines_count = status_bar_lines_count - # Use the property setter since it has some more logic - self.preview_lines_count = preview_lines_count - self.search_lines_count = search_lines_count - self._num_lines = self._calculate_num_lines() - self._viewport = (0, min(self._num_menu_entries, self._num_lines) - 1) - self.keep_visible(cursor_position=None, refresh_terminal_size=False) - - def _calculate_num_lines(self) -> int: - return ( - TerminalMenu._num_lines() - - self._title_lines_count - - self._status_bar_lines_count - - self._preview_lines_count - - self._search_lines_count - ) - - def keep_visible(self, cursor_position: Optional[int], refresh_terminal_size: bool = True) -> None: - # Treat `cursor_position=None` like `cursor_position=0` - if cursor_position is None: - cursor_position = 0 - if refresh_terminal_size: - self.update_terminal_size() - if self._viewport[0] <= cursor_position <= self._viewport[1]: - # Cursor is already visible - return - if cursor_position < self._viewport[0]: - scroll_num = cursor_position - self._viewport[0] - else: - scroll_num = cursor_position - self._viewport[1] - self._viewport = (self._viewport[0] + scroll_num, self._viewport[1] + scroll_num) - - def update_terminal_size(self) -> None: - num_lines = self._calculate_num_lines() - if num_lines != self._num_lines: - # First let the upper index grow or shrink - upper_index = min(num_lines, self._num_menu_entries) - 1 - # Then, use as much space as possible for the `lower_index` - lower_index = max(0, upper_index - num_lines) - self._viewport = (lower_index, upper_index) - self._num_lines = num_lines - - @property - def lower_index(self) -> int: - return self._viewport[0] - - @property - def upper_index(self) -> int: - return self._viewport[1] - - @property - def viewport(self) -> Tuple[int, int]: - return self._viewport - - @property - def size(self) -> int: - return self._viewport[1] - self._viewport[0] + 1 - - @property - def num_menu_entries(self) -> int: - return self._num_menu_entries - - @property - def title_lines_count(self) -> int: - return self._title_lines_count - - @property - def status_bar_lines_count(self) -> int: - return self._status_bar_lines_count - - @status_bar_lines_count.setter - def status_bar_lines_count(self, value: int) -> None: - self._status_bar_lines_count = value - - @property - def preview_lines_count(self) -> int: - return self._preview_lines_count - - @preview_lines_count.setter - def preview_lines_count(self, value: int) -> None: - self._preview_lines_count = min( - value if value >= 3 else 0, - TerminalMenu._num_lines() - - self._title_lines_count - - self._status_bar_lines_count - - MIN_VISIBLE_MENU_ENTRIES_COUNT, - ) - - @property - def search_lines_count(self) -> int: - return self._search_lines_count - - @search_lines_count.setter - def search_lines_count(self, value: int) -> None: - self._search_lines_count = value - - @property - def must_scroll(self) -> bool: - return self._num_menu_entries > self._num_lines - - _codename_to_capname = { - "bg_black": "setab 0", - "bg_blue": "setab 4", - "bg_cyan": "setab 6", - "bg_gray": "setab 7", - "bg_green": "setab 2", - "bg_purple": "setab 5", - "bg_red": "setab 1", - "bg_yellow": "setab 3", - "bold": "bold", - "clear": "clear", - "colors": "colors", - "cursor_down": "cud1", - "cursor_invisible": "civis", - "cursor_left": "cub1", - "cursor_right": "cuf1", - "cursor_up": "cuu1", - "cursor_visible": "cnorm", - "delete_line": "dl1", - "down": "kcud1", - "enter_application_mode": "smkx", - "exit_application_mode": "rmkx", - "fg_black": "setaf 0", - "fg_blue": "setaf 4", - "fg_cyan": "setaf 6", - "fg_gray": "setaf 7", - "fg_green": "setaf 2", - "fg_purple": "setaf 5", - "fg_red": "setaf 1", - "fg_yellow": "setaf 3", - "italics": "sitm", - "reset_attributes": "sgr0", - "standout": "smso", - "underline": "smul", - "up": "kcuu1", - } - _name_to_control_character = { - "backspace": "", # Is assigned later in `self._init_backspace_control_character` - "ctrl-j": "\012", - "ctrl-k": "\013", - "enter": "\015", - "escape": "\033", - "tab": "\t", - } - _codenames = tuple(_codename_to_capname.keys()) - _codename_to_terminal_code = None # type: Optional[Dict[str, str]] - _terminal_code_to_codename = None # type: Optional[Dict[str, str]] - - def __init__( - self, - menu_entries: Iterable[str], - *, - accept_keys: Iterable[str] = DEFAULT_ACCEPT_KEYS, - clear_menu_on_exit: bool = DEFAULT_CLEAR_MENU_ON_EXIT, - clear_screen: bool = DEFAULT_CLEAR_SCREEN, - cursor_index: Optional[int] = None, - cycle_cursor: bool = DEFAULT_CYCLE_CURSOR, - exit_on_shortcut: bool = DEFAULT_EXIT_ON_SHORTCUT, - menu_cursor: Optional[str] = DEFAULT_MENU_CURSOR, - menu_cursor_style: Optional[Iterable[str]] = DEFAULT_MENU_CURSOR_STYLE, - menu_highlight_style: Optional[Iterable[str]] = DEFAULT_MENU_HIGHLIGHT_STYLE, - multi_select: bool = DEFAULT_MULTI_SELECT, - multi_select_cursor: str = DEFAULT_MULTI_SELECT_CURSOR, - multi_select_cursor_brackets_style: Optional[Iterable[str]] = DEFAULT_MULTI_SELECT_CURSOR_BRACKETS_STYLE, - multi_select_cursor_style: Optional[Iterable[str]] = DEFAULT_MULTI_SELECT_CURSOR_STYLE, - multi_select_empty_ok: bool = False, - multi_select_keys: Optional[Iterable[str]] = DEFAULT_MULTI_SELECT_KEYS, - multi_select_select_on_accept: bool = DEFAULT_MULTI_SELECT_SELECT_ON_ACCEPT, - preselected_entries: Optional[Iterable[Union[str, int]]] = None, - preview_border: bool = DEFAULT_PREVIEW_BORDER, - preview_command: Optional[Union[str, Callable[[str], str]]] = None, - preview_size: float = DEFAULT_PREVIEW_SIZE, - preview_title: str = DEFAULT_PREVIEW_TITLE, - quit_keys: Iterable[str] = DEFAULT_QUIT_KEYS, - raise_error_on_interrupt: bool = False, - search_case_sensitive: bool = DEFAULT_SEARCH_CASE_SENSITIVE, - search_highlight_style: Optional[Iterable[str]] = DEFAULT_SEARCH_HIGHLIGHT_STYLE, - search_key: Optional[str] = DEFAULT_SEARCH_KEY, - shortcut_brackets_highlight_style: Optional[Iterable[str]] = DEFAULT_SHORTCUT_BRACKETS_HIGHLIGHT_STYLE, - shortcut_key_highlight_style: Optional[Iterable[str]] = DEFAULT_SHORTCUT_KEY_HIGHLIGHT_STYLE, - show_multi_select_hint: bool = DEFAULT_SHOW_MULTI_SELECT_HINT, - show_multi_select_hint_text: Optional[str] = None, - show_search_hint: bool = DEFAULT_SHOW_SEARCH_HINT, - show_search_hint_text: Optional[str] = None, - show_shortcut_hints: bool = DEFAULT_SHOW_SHORTCUT_HINTS, - show_shortcut_hints_in_status_bar: bool = DEFAULT_SHOW_SHORTCUT_HINTS_IN_STATUS_BAR, - skip_empty_entries: bool = False, - status_bar: Optional[Union[str, Iterable[str], Callable[[str], str]]] = None, - status_bar_below_preview: bool = DEFAULT_STATUS_BAR_BELOW_PREVIEW, - status_bar_style: Optional[Iterable[str]] = DEFAULT_STATUS_BAR_STYLE, - title: Optional[Union[str, Iterable[str]]] = None - ): - def extract_shortcuts_menu_entries_and_preview_arguments( - entries: Iterable[str], - ) -> Tuple[List[str], List[Optional[str]], List[Optional[str]], List[int]]: - separator_pattern = re.compile(r"([^\\])\|") - escaped_separator_pattern = re.compile(r"\\\|") - menu_entry_pattern = re.compile(r"^(?:\[(\S)\]\s*)?([^\x1F]+)(?:\x1F([^\x1F]*))?") - shortcut_keys = [] # type: List[Optional[str]] - menu_entries = [] # type: List[str] - preview_arguments = [] # type: List[Optional[str]] - skip_indices = [] # type: List[int] - - for idx, entry in enumerate(entries): - if entry is None or (entry == "" and skip_empty_entries): - shortcut_keys.append(None) - menu_entries.append("") - preview_arguments.append(None) - skip_indices.append(idx) - else: - unit_separated_entry = escaped_separator_pattern.sub("|", separator_pattern.sub("\\1\x1F", entry)) - match_obj = menu_entry_pattern.match(unit_separated_entry) - # this is none in case the entry was an emtpy string which - # will be interpreted as a separator - assert match_obj is not None - shortcut_key = match_obj.group(1) - display_text = match_obj.group(2) - preview_argument = match_obj.group(3) - shortcut_keys.append(shortcut_key) - menu_entries.append(display_text) - preview_arguments.append(preview_argument) - - return menu_entries, shortcut_keys, preview_arguments, skip_indices - - def convert_preselected_entries_to_indices( - preselected_indices_or_entries: Iterable[Union[str, int]] - ) -> Set[int]: - menu_entry_to_indices = {} # type: Dict[str, Set[int]] - for menu_index, menu_entry in enumerate(self._menu_entries): - menu_entry_to_indices.setdefault(menu_entry, set()) - menu_entry_to_indices[menu_entry].add(menu_index) - preselected_indices = set() - for item in preselected_indices_or_entries: - if isinstance(item, int): - if 0 <= item < len(self._menu_entries): - preselected_indices.add(item) - else: - raise IndexError( - "Error: {} is outside the allowable range of 0..{}.".format( - item, len(self._menu_entries) - 1 - ) - ) - elif isinstance(item, str): - try: - preselected_indices.update(menu_entry_to_indices[item]) - except KeyError as e: - raise UnknownMenuEntryError('Pre-selection "{}" is not a valid menu entry.'.format(item)) from e - else: - raise ValueError('"preselected_entries" must either contain integers or strings.') - return preselected_indices - - def setup_title_or_status_bar_lines( - title_or_status_bar: Optional[Union[str, Iterable[str]]], - show_shortcut_hints: bool, - menu_entries: Iterable[str], - shortcut_keys: Iterable[Optional[str]], - shortcut_hints_in_parentheses: bool, - ) -> Tuple[str, ...]: - if title_or_status_bar is None: - lines = [] # type: List[str] - elif isinstance(title_or_status_bar, str): - lines = title_or_status_bar.split("\n") - else: - lines = list(title_or_status_bar) - if show_shortcut_hints: - shortcut_hints_line = self._get_shortcut_hints_line( - menu_entries, shortcut_keys, shortcut_hints_in_parentheses - ) - if shortcut_hints_line is not None: - lines.append(shortcut_hints_line) - return tuple(lines) - - ( - self._menu_entries, - self._shortcut_keys, - self._preview_arguments, - self._skip_indices, - ) = extract_shortcuts_menu_entries_and_preview_arguments(menu_entries) - self._shortcuts_defined = any(key is not None for key in self._shortcut_keys) - self._accept_keys = tuple(accept_keys) - self._clear_menu_on_exit = clear_menu_on_exit - self._clear_screen = clear_screen - self._cycle_cursor = cycle_cursor - self._multi_select_empty_ok = multi_select_empty_ok - self._exit_on_shortcut = exit_on_shortcut - self._menu_cursor = menu_cursor if menu_cursor is not None else "" - self._menu_cursor_style = tuple(menu_cursor_style) if menu_cursor_style is not None else () - self._menu_highlight_style = tuple(menu_highlight_style) if menu_highlight_style is not None else () - self._multi_select = multi_select - self._multi_select_cursor = multi_select_cursor - self._multi_select_cursor_brackets_style = ( - tuple(multi_select_cursor_brackets_style) if multi_select_cursor_brackets_style is not None else () - ) - self._multi_select_cursor_style = ( - tuple(multi_select_cursor_style) if multi_select_cursor_style is not None else () - ) - self._multi_select_keys = tuple(multi_select_keys) if multi_select_keys is not None else () - self._multi_select_select_on_accept = multi_select_select_on_accept - if preselected_entries and not self._multi_select: - raise InvalidParameterCombinationError( - "Multi-select mode must be enabled when preselected entries are given." - ) - self._preselected_indices = ( - convert_preselected_entries_to_indices(preselected_entries) if preselected_entries is not None else None - ) - self._preview_border = preview_border - self._preview_command = preview_command - self._preview_size = preview_size - self._preview_title = preview_title - self._quit_keys = tuple(quit_keys) - self._raise_error_on_interrupt = raise_error_on_interrupt - self._search_case_sensitive = search_case_sensitive - self._search_highlight_style = tuple(search_highlight_style) if search_highlight_style is not None else () - self._search_key = search_key - self._shortcut_brackets_highlight_style = ( - tuple(shortcut_brackets_highlight_style) if shortcut_brackets_highlight_style is not None else () - ) - self._shortcut_key_highlight_style = ( - tuple(shortcut_key_highlight_style) if shortcut_key_highlight_style is not None else () - ) - self._show_search_hint = show_search_hint - self._show_search_hint_text = show_search_hint_text - self._show_shortcut_hints = show_shortcut_hints - self._show_shortcut_hints_in_status_bar = show_shortcut_hints_in_status_bar - self._status_bar_func = None # type: Optional[Callable[[str], str]] - self._status_bar_lines = None # type: Optional[Tuple[str, ...]] - if callable(status_bar): - self._status_bar_func = status_bar - else: - self._status_bar_lines = setup_title_or_status_bar_lines( - status_bar, - show_shortcut_hints and show_shortcut_hints_in_status_bar, - self._menu_entries, - self._shortcut_keys, - False, - ) - self._status_bar_below_preview = status_bar_below_preview - self._status_bar_style = tuple(status_bar_style) if status_bar_style is not None else () - self._title_lines = setup_title_or_status_bar_lines( - title, - show_shortcut_hints and not show_shortcut_hints_in_status_bar, - self._menu_entries, - self._shortcut_keys, - True, - ) - self._show_multi_select_hint = show_multi_select_hint - self._show_multi_select_hint_text = show_multi_select_hint_text - self._chosen_accept_key = None # type: Optional[str] - self._chosen_menu_index = None # type: Optional[int] - self._chosen_menu_indices = None # type: Optional[Tuple[int, ...]] - self._paint_before_next_read = False - self._previous_displayed_menu_height = None # type: Optional[int] - self._reading_next_key = False - self._search = self.Search( - self._menu_entries, - case_senitive=self._search_case_sensitive, - show_search_hint=self._show_search_hint, - ) - self._selection = self.Selection(len(self._menu_entries), self._preselected_indices) - self._viewport = self.Viewport( - len(self._menu_entries), - len(self._title_lines), - len(self._status_bar_lines) if self._status_bar_lines is not None else 0, - 0, - 0, - ) - self._view = self.View( - self._menu_entries, self._search, self._selection, self._viewport, self._cycle_cursor, self._skip_indices - ) - if cursor_index and 0 < cursor_index < len(self._menu_entries): - self._view.active_menu_index = cursor_index - self._search.change_callback = self._view.update_view - self._old_term = None # type: Optional[List[Union[int, List[bytes]]]] - self._new_term = None # type: Optional[List[Union[int, List[bytes]]]] - self._tty_in = None # type: Optional[TextIO] - self._tty_out = None # type: Optional[TextIO] - self._user_locale = get_locale() - self._check_for_valid_styles() - # backspace can be queried from the terminal database but is unreliable, query the terminal directly instead - self._init_backspace_control_character() - self._add_missing_control_characters_for_keys(self._accept_keys) - self._add_missing_control_characters_for_keys(self._quit_keys) - self._init_terminal_codes() - - @staticmethod - def _get_shortcut_hints_line( - menu_entries: Iterable[str], - shortcut_keys: Iterable[Optional[str]], - shortcut_hints_in_parentheses: bool, - ) -> Optional[str]: - shortcut_hints_line = ", ".join( - "[{}]: {}".format(shortcut_key, menu_entry) - for shortcut_key, menu_entry in zip(shortcut_keys, menu_entries) - if shortcut_key is not None - ) - if shortcut_hints_line != "": - if shortcut_hints_in_parentheses: - return "(" + shortcut_hints_line + ")" - else: - return shortcut_hints_line - return None - - @staticmethod - def _get_keycode_for_key(key: str) -> str: - if len(key) == 1: - # One letter keys represent themselves - return key - alt_modified_regex = re.compile(r"[Aa]lt-(\S)") - ctrl_modified_regex = re.compile(r"[Cc]trl-(\S)") - match_obj = alt_modified_regex.match(key) - if match_obj: - return "\033" + match_obj.group(1) - match_obj = ctrl_modified_regex.match(key) - if match_obj: - # Ctrl + key is interpreted by terminals as the ascii code of that key minus 64 - ctrl_code_ascii = ord(match_obj.group(1).upper()) - 64 - if ctrl_code_ascii < 0: - # Interpret negative ascii codes as unsigned 7-Bit integers - ctrl_code_ascii = ctrl_code_ascii & 0x80 - 1 - return chr(ctrl_code_ascii) - raise ValueError('Cannot interpret the given key "{}".'.format(key)) - - @classmethod - def _init_backspace_control_character(self) -> None: - try: - with open("/dev/tty", "r") as tty: - stty_output = subprocess.check_output(["stty", "-a"], universal_newlines=True, stdin=tty) - name_to_keycode_regex = re.compile(r"^\s*(\S+)\s*=\s*\^(\S+)\s*$") - for field in stty_output.split(";"): - match_obj = name_to_keycode_regex.match(field) - if not match_obj: - continue - name, ctrl_code = match_obj.group(1), match_obj.group(2) - if name != "erase": - continue - self._name_to_control_character["backspace"] = self._get_keycode_for_key("ctrl-" + ctrl_code) - return - except subprocess.CalledProcessError: - pass - # Backspace control character could not be queried, assume `` (is most often used) - self._name_to_control_character["backspace"] = "\177" - - @classmethod - def _add_missing_control_characters_for_keys(cls, keys: Iterable[str]) -> None: - for key in keys: - if key not in cls._name_to_control_character and key not in string.ascii_letters: - cls._name_to_control_character[key] = cls._get_keycode_for_key(key) - - @classmethod - def _init_terminal_codes(cls) -> None: - if cls._codename_to_terminal_code is not None: - return - supported_colors = int(cls._query_terminfo_database("colors")) - cls._codename_to_terminal_code = { - codename: cls._query_terminfo_database(codename) - if not (codename.startswith("bg_") or codename.startswith("fg_")) or supported_colors >= 8 - else "" - for codename in cls._codenames - } - cls._codename_to_terminal_code.update(cls._name_to_control_character) - cls._terminal_code_to_codename = { - terminal_code: codename for codename, terminal_code in cls._codename_to_terminal_code.items() - } - - @classmethod - def _query_terminfo_database(cls, codename: str) -> str: - if codename in cls._codename_to_capname: - capname = cls._codename_to_capname[codename] - else: - capname = codename - try: - return subprocess.check_output(["tput"] + capname.split(), universal_newlines=True) - except subprocess.CalledProcessError as e: - # The return code 1 indicates a missing terminal capability - if e.returncode == 1: - return "" - raise e - - @classmethod - def _num_lines(self) -> int: - return int(self._query_terminfo_database("lines")) - - @classmethod - def _num_cols(self) -> int: - return int(self._query_terminfo_database("cols")) - - def _check_for_valid_styles(self) -> None: - invalid_styles = [] - for style_tuple in ( - self._menu_cursor_style, - self._menu_highlight_style, - self._search_highlight_style, - self._shortcut_key_highlight_style, - self._shortcut_brackets_highlight_style, - self._status_bar_style, - self._multi_select_cursor_brackets_style, - self._multi_select_cursor_style, - ): - for style in style_tuple: - if style not in self._codename_to_capname: - invalid_styles.append(style) - if invalid_styles: - if len(invalid_styles) == 1: - raise InvalidStyleError('The style "{}" does not exist.'.format(invalid_styles[0])) - else: - raise InvalidStyleError('The styles ("{}") do not exist.'.format('", "'.join(invalid_styles))) - - def _init_term(self) -> None: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - self._tty_in = open("/dev/tty", "r", encoding=self._user_locale) - self._tty_out = open("/dev/tty", "w", encoding=self._user_locale, errors="replace") - self._old_term = termios.tcgetattr(self._tty_in.fileno()) - self._new_term = termios.tcgetattr(self._tty_in.fileno()) - # set the terminal to: unbuffered, no echo and no to translation (so sends instead of - # and since generates ) - self._new_term[3] = cast(int, self._new_term[3]) & ~termios.ICANON & ~termios.ECHO & ~termios.ICRNL - self._new_term[0] = cast(int, self._new_term[0]) & ~termios.ICRNL - termios.tcsetattr( - self._tty_in.fileno(), termios.TCSAFLUSH, cast(List[Union[int, List[Union[bytes, int]]]], self._new_term) - ) - # Enter terminal application mode to get expected escape codes for arrow keys - self._tty_out.write(self._codename_to_terminal_code["enter_application_mode"]) - self._tty_out.write(self._codename_to_terminal_code["cursor_invisible"]) - if self._clear_screen: - self._tty_out.write(self._codename_to_terminal_code["clear"]) - - def _reset_term(self) -> None: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._tty_in is not None - assert self._tty_out is not None - assert self._old_term is not None - termios.tcsetattr( - self._tty_out.fileno(), termios.TCSAFLUSH, cast(List[Union[int, List[Union[bytes, int]]]], self._old_term) - ) - self._tty_out.write(self._codename_to_terminal_code["cursor_visible"]) - self._tty_out.write(self._codename_to_terminal_code["exit_application_mode"]) - if self._clear_screen: - self._tty_out.write(self._codename_to_terminal_code["clear"]) - self._tty_in.close() - self._tty_out.close() - - def _paint_menu(self) -> None: - def get_status_bar_lines() -> Tuple[str, ...]: - def get_multi_select_hint() -> str: - def get_string_from_keys(keys: Sequence[str]) -> str: - string_to_key = { - " ": "space", - } - keys_string = ", ".join( - "<" + string_to_key.get(accept_key, accept_key) + ">" for accept_key in keys - ) - return keys_string - - accept_keys_string = get_string_from_keys(self._accept_keys) - multi_select_keys_string = get_string_from_keys(self._multi_select_keys) - if self._show_multi_select_hint_text is not None: - return self._show_multi_select_hint_text.format( - multi_select_keys=multi_select_keys_string, accept_keys=accept_keys_string - ) - else: - return "Press {} for multi-selection and {} to {}accept".format( - multi_select_keys_string, - accept_keys_string, - "select and " if self._multi_select_select_on_accept else "", - ) - - if self._status_bar_func is not None and self._view.active_menu_index is not None: - status_bar_lines = tuple( - self._status_bar_func(self._menu_entries[self._view.active_menu_index]).strip().split("\n") - ) - if self._show_shortcut_hints and self._show_shortcut_hints_in_status_bar: - shortcut_hints_line = self._get_shortcut_hints_line(self._menu_entries, self._shortcut_keys, False) - if shortcut_hints_line is not None: - status_bar_lines += (shortcut_hints_line,) - elif self._status_bar_lines is not None: - status_bar_lines = self._status_bar_lines - else: - status_bar_lines = tuple() - if self._multi_select and self._show_multi_select_hint: - status_bar_lines += (get_multi_select_hint(),) - return status_bar_lines - - def apply_style( - style_iterable: Optional[Iterable[str]] = None, reset: bool = True, file: Optional[TextIO] = None - ) -> None: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._tty_out is not None - if file is None: - file = self._tty_out - if reset or style_iterable is None: - file.write(self._codename_to_terminal_code["reset_attributes"]) - if style_iterable is not None: - for style in style_iterable: - file.write(self._codename_to_terminal_code[style]) - - def print_menu_entries() -> int: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._tty_out is not None - all_cursors_width = wcswidth(self._menu_cursor) + ( - wcswidth(self._multi_select_cursor) if self._multi_select else 0 - ) - current_menu_block_displayed_height = 0 # sum all written lines - num_cols = self._num_cols() - if self._title_lines: - self._tty_out.write( - len(self._title_lines) * self._codename_to_terminal_code["cursor_up"] - + "\r" - + "\n".join( - (title_line[:num_cols] + (num_cols - wcswidth(title_line)) * " ") - for title_line in self._title_lines - ) - + "\n" - ) - shortcut_string_len = 4 if self._shortcuts_defined else 0 - displayed_index = -1 - for displayed_index, menu_index, menu_entry in self._view: - current_shortcut_key = self._shortcut_keys[menu_index] - self._tty_out.write(all_cursors_width * self._codename_to_terminal_code["cursor_right"]) - if self._shortcuts_defined: - if current_shortcut_key is not None: - apply_style(self._shortcut_brackets_highlight_style) - self._tty_out.write("[") - apply_style(self._shortcut_key_highlight_style) - self._tty_out.write(current_shortcut_key) - apply_style(self._shortcut_brackets_highlight_style) - self._tty_out.write("]") - apply_style() - else: - self._tty_out.write(3 * " ") - self._tty_out.write(" ") - if menu_index == self._view.active_menu_index: - apply_style(self._menu_highlight_style) - if self._search and self._search.search_text != "": - match_obj = self._search.matches[displayed_index][1] - self._tty_out.write( - menu_entry[: min(match_obj.start(), num_cols - all_cursors_width - shortcut_string_len)] - ) - apply_style(self._search_highlight_style) - self._tty_out.write( - menu_entry[ - match_obj.start() : min(match_obj.end(), num_cols - all_cursors_width - shortcut_string_len) - ] - ) - apply_style() - if menu_index == self._view.active_menu_index: - apply_style(self._menu_highlight_style) - self._tty_out.write( - menu_entry[match_obj.end() : num_cols - all_cursors_width - shortcut_string_len] - ) - else: - self._tty_out.write(menu_entry[: num_cols - all_cursors_width - shortcut_string_len]) - if menu_index == self._view.active_menu_index: - apply_style() - self._tty_out.write((num_cols - wcswidth(menu_entry) - all_cursors_width - shortcut_string_len) * " ") - if displayed_index < self._viewport.upper_index: - self._tty_out.write("\n") - empty_menu_lines = self._viewport.upper_index - displayed_index - self._tty_out.write( - max(0, empty_menu_lines - 1) * (num_cols * " " + "\n") + min(1, empty_menu_lines) * (num_cols * " ") - ) - self._tty_out.write("\r" + (self._viewport.size - 1) * self._codename_to_terminal_code["cursor_up"]) - current_menu_block_displayed_height += self._viewport.size - 1 # sum all written lines - return current_menu_block_displayed_height - - def print_search_line(current_menu_height: int) -> int: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._tty_out is not None - current_menu_block_displayed_height = 0 - num_cols = self._num_cols() - if self._search or self._show_search_hint: - self._tty_out.write((current_menu_height + 1) * self._codename_to_terminal_code["cursor_down"]) - if self._search: - assert self._search.search_text is not None - self._tty_out.write( - ( - (self._search_key if self._search_key is not None else DEFAULT_SEARCH_KEY) - + self._search.search_text - )[:num_cols] - ) - self._tty_out.write((num_cols - len(self._search) - 1) * " ") - elif self._show_search_hint: - if self._show_search_hint_text is not None: - search_hint = self._show_search_hint_text.format(key=self._search_key)[:num_cols] - elif self._search_key is not None: - search_hint = '(Press "{key}" to search)'.format(key=self._search_key)[:num_cols] - else: - search_hint = "(Press any letter key to search)"[:num_cols] - self._tty_out.write(search_hint) - self._tty_out.write((num_cols - wcswidth(search_hint)) * " ") - if self._search or self._show_search_hint: - self._tty_out.write("\r" + (current_menu_height + 1) * self._codename_to_terminal_code["cursor_up"]) - current_menu_block_displayed_height = 1 - return current_menu_block_displayed_height - - def print_status_bar(current_menu_height: int, status_bar_lines: Tuple[str, ...]) -> int: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._tty_out is not None - current_menu_block_displayed_height = 0 # sum all written lines - num_cols = self._num_cols() - if status_bar_lines: - self._tty_out.write((current_menu_height + 1) * self._codename_to_terminal_code["cursor_down"]) - apply_style(self._status_bar_style) - self._tty_out.write( - "\r" - + "\n".join( - (status_bar_line[:num_cols] + (num_cols - wcswidth(status_bar_line)) * " ") - for status_bar_line in status_bar_lines - ) - + "\r" - ) - apply_style() - self._tty_out.write( - (current_menu_height + len(status_bar_lines)) * self._codename_to_terminal_code["cursor_up"] - ) - current_menu_block_displayed_height += len(status_bar_lines) - return current_menu_block_displayed_height - - def print_preview(current_menu_height: int, preview_max_num_lines: int) -> int: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._tty_out is not None - if self._preview_command is None or preview_max_num_lines < 3: - return 0 - - def get_preview_string() -> Optional[str]: - assert self._preview_command is not None - if self._view.active_menu_index is None: - return None - preview_argument = ( - self._preview_arguments[self._view.active_menu_index] - if self._preview_arguments[self._view.active_menu_index] is not None - else self._menu_entries[self._view.active_menu_index] - ) - if preview_argument == "": - return None - if isinstance(self._preview_command, str): - try: - preview_process = subprocess.Popen( - [cmd_part.format(preview_argument) for cmd_part in shlex.split(self._preview_command)], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - assert preview_process.stdout is not None - preview_string = ( - io.TextIOWrapper(preview_process.stdout, encoding=self._user_locale, errors="replace") - .read() - .strip() - ) - except subprocess.CalledProcessError as e: - raise PreviewCommandFailedError( - e.stderr.decode(encoding=self._user_locale, errors="replace").strip() - ) from e - else: - preview_string = self._preview_command(preview_argument) if preview_argument is not None else "" - return preview_string - - @static_variables( - # Regex taken from https://stackoverflow.com/a/14693789/5958465 - ansi_escape_regex=re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"), - # Modified version of https://stackoverflow.com/a/2188410/5958465 - ansi_sgr_regex=re.compile(r"\x1B\[[;\d]*m"), - ) - def strip_ansi_codes_except_styling(string: str) -> str: - stripped_string = strip_ansi_codes_except_styling.ansi_escape_regex.sub( # type: ignore - lambda match_obj: match_obj.group(0) - if strip_ansi_codes_except_styling.ansi_sgr_regex.match(match_obj.group(0)) # type: ignore - else "", - string, - ) - return cast(str, stripped_string) - - @static_variables( - regular_text_regex=re.compile(r"([^\x1B]+)(.*)"), - ansi_escape_regex=re.compile(r"(\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))(.*)"), - ) - def limit_string_with_escape_codes(string: str, max_len: int) -> Tuple[str, int]: - if max_len <= 0: - return "", 0 - string_parts = [] - string_len = 0 - while string: - regular_text_match = limit_string_with_escape_codes.regular_text_regex.match(string) # type: ignore - if regular_text_match is not None: - regular_text = regular_text_match.group(1) - regular_text_len = wcswidth(regular_text) - if string_len + regular_text_len > max_len: - string_parts.append(regular_text[: max_len - string_len]) - string_len = max_len - break - string_parts.append(regular_text) - string_len += regular_text_len - string = regular_text_match.group(2) - else: - ansi_escape_match = limit_string_with_escape_codes.ansi_escape_regex.match( # type: ignore - string - ) - if ansi_escape_match is not None: - # Adopt the ansi escape code but do not count its length - ansi_escape_code_text = ansi_escape_match.group(1) - string_parts.append(ansi_escape_code_text) - string = ansi_escape_match.group(2) - else: - # It looks like an escape code (starts with escape), but it is something else - # -> skip the escape character and continue the loop - string_parts.append("\x1B") - string = string[1:] - return "".join(string_parts), string_len - - num_cols = self._num_cols() - try: - preview_string = get_preview_string() - if preview_string is not None: - preview_string = strip_ansi_codes_except_styling(preview_string) - except PreviewCommandFailedError as e: - preview_string = "The preview command failed with error message:\n\n" + str(e) - self._tty_out.write(current_menu_height * self._codename_to_terminal_code["cursor_down"]) - if preview_string is not None: - self._tty_out.write(self._codename_to_terminal_code["cursor_down"] + "\r") - if self._preview_border: - self._tty_out.write( - ( - BoxDrawingCharacters.upper_left - + (2 * BoxDrawingCharacters.horizontal + " " + self._preview_title)[: num_cols - 3] - + " " - + (num_cols - len(self._preview_title) - 6) * BoxDrawingCharacters.horizontal - + BoxDrawingCharacters.upper_right - )[:num_cols] - + "\n" - ) - # `finditer` can be used as a generator version of `str.join` - for i, line in enumerate( - match.group(0) for match in re.finditer(r"^.*$", preview_string, re.MULTILINE) - ): - if i >= preview_max_num_lines - (2 if self._preview_border else 0): - preview_num_lines = preview_max_num_lines - break - limited_line, limited_line_len = limit_string_with_escape_codes( - line, num_cols - (3 if self._preview_border else 0) - ) - self._tty_out.write( - ( - ((BoxDrawingCharacters.vertical + " ") if self._preview_border else "") - + limited_line - + self._codename_to_terminal_code["reset_attributes"] - + max(num_cols - limited_line_len - (3 if self._preview_border else 0), 0) * " " - + (BoxDrawingCharacters.vertical if self._preview_border else "") - ) - ) - else: - preview_num_lines = i + (3 if self._preview_border else 1) - if self._preview_border: - self._tty_out.write( - "\n" - + ( - BoxDrawingCharacters.lower_left - + (num_cols - 2) * BoxDrawingCharacters.horizontal - + BoxDrawingCharacters.lower_right - )[:num_cols] - ) - self._tty_out.write("\r") - else: - preview_num_lines = 0 - self._tty_out.write( - (current_menu_height + preview_num_lines) * self._codename_to_terminal_code["cursor_up"] - ) - return preview_num_lines - - def delete_old_menu_lines(displayed_menu_height: int) -> None: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._tty_out is not None - if ( - self._previous_displayed_menu_height is not None - and self._previous_displayed_menu_height > displayed_menu_height - ): - self._tty_out.write((displayed_menu_height + 1) * self._codename_to_terminal_code["cursor_down"]) - self._tty_out.write( - (self._previous_displayed_menu_height - displayed_menu_height) - * self._codename_to_terminal_code["delete_line"] - ) - self._tty_out.write((displayed_menu_height + 1) * self._codename_to_terminal_code["cursor_up"]) - - def position_cursor() -> None: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._tty_out is not None - if self._view.active_displayed_index is None: - return - - cursor_width = wcswidth(self._menu_cursor) - for displayed_index in range(self._viewport.lower_index, self._viewport.upper_index + 1): - if displayed_index == self._view.active_displayed_index: - apply_style(self._menu_cursor_style) - self._tty_out.write(self._menu_cursor) - apply_style() - else: - self._tty_out.write(cursor_width * " ") - self._tty_out.write("\r") - if displayed_index < self._viewport.upper_index: - self._tty_out.write(self._codename_to_terminal_code["cursor_down"]) - self._tty_out.write((self._viewport.size - 1) * self._codename_to_terminal_code["cursor_up"]) - - def print_multi_select_column() -> None: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._tty_out is not None - if not self._multi_select: - return - - def prepare_multi_select_cursors() -> Tuple[str, str]: - bracket_characters = "([{<)]}>" - bracket_style_escape_codes_io = io.StringIO() - multi_select_cursor_style_escape_codes_io = io.StringIO() - reset_codes_io = io.StringIO() - apply_style(self._multi_select_cursor_brackets_style, file=bracket_style_escape_codes_io) - apply_style(self._multi_select_cursor_style, file=multi_select_cursor_style_escape_codes_io) - apply_style(file=reset_codes_io) - bracket_style_escape_codes = bracket_style_escape_codes_io.getvalue() - multi_select_cursor_style_escape_codes = multi_select_cursor_style_escape_codes_io.getvalue() - reset_codes = reset_codes_io.getvalue() - - cursor_with_brackets_only = re.sub( - r"[^{}]".format(re.escape(bracket_characters)), " ", self._multi_select_cursor - ) - cursor_with_brackets_only_styled = re.sub( - r"[{}]+".format(re.escape(bracket_characters)), - lambda match_obj: bracket_style_escape_codes + match_obj.group(0) + reset_codes, - cursor_with_brackets_only, - ) - cursor_styled = re.sub( - r"[{brackets}]+|[^{brackets}\s]+".format(brackets=re.escape(bracket_characters)), - lambda match_obj: ( - bracket_style_escape_codes - if match_obj.group(0)[0] in bracket_characters - else multi_select_cursor_style_escape_codes - ) - + match_obj.group(0) - + reset_codes, - self._multi_select_cursor, - ) - return cursor_styled, cursor_with_brackets_only_styled - - if not self._view: - return - checked_multi_select_cursor, unchecked_multi_select_cursor = prepare_multi_select_cursors() - cursor_width = wcswidth(self._menu_cursor) - displayed_selected_indices = self._view.displayed_selected_indices - displayed_index = 0 - for displayed_index, _, _ in self._view: - self._tty_out.write("\r" + cursor_width * self._codename_to_terminal_code["cursor_right"]) - if displayed_index in self._skip_indices: - self._tty_out.write("") - elif displayed_index in displayed_selected_indices: - self._tty_out.write(checked_multi_select_cursor) - else: - self._tty_out.write(unchecked_multi_select_cursor) - if displayed_index < self._viewport.upper_index: - self._tty_out.write(self._codename_to_terminal_code["cursor_down"]) - self._tty_out.write("\r") - self._tty_out.write( - (displayed_index + (1 if displayed_index < self._viewport.upper_index else 0)) - * self._codename_to_terminal_code["cursor_up"] - ) - - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._tty_out is not None - displayed_menu_height = 0 # sum all written lines - status_bar_lines = get_status_bar_lines() - self._viewport.status_bar_lines_count = len(status_bar_lines) - if self._preview_command is not None: - self._viewport.preview_lines_count = int(self._preview_size * self._num_lines()) - preview_max_num_lines = self._viewport.preview_lines_count - self._viewport.keep_visible(self._view.active_displayed_index) - displayed_menu_height += print_menu_entries() - displayed_menu_height += print_search_line(displayed_menu_height) - if not self._status_bar_below_preview: - displayed_menu_height += print_status_bar(displayed_menu_height, status_bar_lines) - if self._preview_command is not None: - displayed_menu_height += print_preview(displayed_menu_height, preview_max_num_lines) - if self._status_bar_below_preview: - displayed_menu_height += print_status_bar(displayed_menu_height, status_bar_lines) - delete_old_menu_lines(displayed_menu_height) - position_cursor() - if self._multi_select: - print_multi_select_column() - self._previous_displayed_menu_height = displayed_menu_height - self._tty_out.flush() - - def _clear_menu(self) -> None: - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - assert self._previous_displayed_menu_height is not None - assert self._tty_out is not None - if self._clear_menu_on_exit: - if self._title_lines: - self._tty_out.write(len(self._title_lines) * self._codename_to_terminal_code["cursor_up"]) - self._tty_out.write(len(self._title_lines) * self._codename_to_terminal_code["delete_line"]) - self._tty_out.write( - (self._previous_displayed_menu_height + 1) * self._codename_to_terminal_code["delete_line"] - ) - else: - self._tty_out.write( - (self._previous_displayed_menu_height + 1) * self._codename_to_terminal_code["cursor_down"] - ) - self._tty_out.flush() - - def _read_next_key(self, ignore_case: bool = True) -> str: - # pylint: disable=unsubscriptable-object,unsupported-membership-test - assert self._terminal_code_to_codename is not None - assert self._tty_in is not None - # Needed for asynchronous handling of terminal resize events - self._reading_next_key = True - if self._paint_before_next_read: - self._paint_menu() - self._paint_before_next_read = False - # blocks until any amount of bytes is available - code = os.read(self._tty_in.fileno(), 80).decode("ascii", errors="ignore") - self._reading_next_key = False - if code in self._terminal_code_to_codename: - return self._terminal_code_to_codename[code] - elif ignore_case: - return code.lower() - else: - return code - - def show(self) -> Optional[Union[int, Tuple[int, ...]]]: - def init_signal_handling() -> None: - # `SIGWINCH` is send on terminal resizes - def handle_sigwinch(signum: signal.Signals, frame: FrameType) -> None: - # pylint: disable=unused-argument - if self._reading_next_key: - self._paint_menu() - else: - self._paint_before_next_read = True - - signal.signal(signal.SIGWINCH, handle_sigwinch) - - def reset_signal_handling() -> None: - signal.signal(signal.SIGWINCH, signal.SIG_DFL) - - def remove_letter_keys(menu_action_to_keys: Dict[str, Set[Optional[str]]]) -> None: - letter_keys = frozenset(string.ascii_lowercase) | frozenset(" ") - for keys in menu_action_to_keys.values(): - keys -= letter_keys - - # pylint: disable=unsubscriptable-object - assert self._codename_to_terminal_code is not None - self._init_term() - if self._preselected_indices is None: - self._selection.clear() - self._chosen_accept_key = None - self._chosen_menu_indices = None - self._chosen_menu_index = None - assert self._tty_out is not None - if self._title_lines: - # `print_menu` expects the cursor on the first menu item -> reserve one line for the title - self._tty_out.write(len(self._title_lines) * self._codename_to_terminal_code["cursor_down"]) - menu_was_interrupted = False - try: - init_signal_handling() - menu_action_to_keys = { - "menu_up": set(("up", "ctrl-k", "k")), - "menu_down": set(("down", "ctrl-j", "j")), - "accept": set(self._accept_keys), - "multi_select": set(self._multi_select_keys), - "quit": set(self._quit_keys), - "search_start": set((self._search_key,)), - "backspace": set(("backspace",)), - } # type: Dict[str, Set[Optional[str]]] - while True: - self._paint_menu() - current_menu_action_to_keys = copy.deepcopy(menu_action_to_keys) - next_key = self._read_next_key(ignore_case=False) - if self._search or self._search_key is None: - remove_letter_keys(current_menu_action_to_keys) - else: - next_key = next_key.lower() - if self._search_key is not None and not self._search and next_key in self._shortcut_keys: - shortcut_menu_index = self._shortcut_keys.index(next_key) - if self._exit_on_shortcut: - self._selection.add(shortcut_menu_index) - break - else: - if self._multi_select: - self._selection.toggle(shortcut_menu_index) - else: - self._view.active_menu_index = shortcut_menu_index - elif next_key in current_menu_action_to_keys["menu_up"]: - self._view.decrement_active_index() - elif next_key in current_menu_action_to_keys["menu_down"]: - self._view.increment_active_index() - elif self._multi_select and next_key in current_menu_action_to_keys["multi_select"]: - if self._view.active_menu_index is not None: - self._selection.toggle(self._view.active_menu_index) - elif next_key in current_menu_action_to_keys["accept"]: - if self._view.active_menu_index is not None: - if self._multi_select_select_on_accept or ( - not self._selection and self._multi_select_empty_ok is False - ): - self._selection.add(self._view.active_menu_index) - self._chosen_accept_key = next_key - break - elif next_key in current_menu_action_to_keys["quit"]: - if not self._search: - menu_was_interrupted = True - break - else: - self._search.search_text = None - elif not self._search: - if next_key in current_menu_action_to_keys["search_start"] or ( - self._search_key is None and next_key == DEFAULT_SEARCH_KEY - ): - self._search.search_text = "" - elif self._search_key is None: - self._search.search_text = next_key - else: - assert self._search.search_text is not None - if next_key in ("backspace",): - if self._search.search_text != "": - self._search.search_text = self._search.search_text[:-1] - else: - self._search.search_text = None - elif wcswidth(next_key) >= 0 and not ( - next_key in current_menu_action_to_keys["search_start"] and self._search.search_text == "" - ): - # Only append `next_key` if it is a printable character and the first character is not the - # `search_start` key - self._search.search_text += next_key - except KeyboardInterrupt as e: - if self._raise_error_on_interrupt: - raise e - menu_was_interrupted = True - finally: - reset_signal_handling() - self._clear_menu() - self._reset_term() - if not menu_was_interrupted: - chosen_menu_indices = self._selection.selected_menu_indices - if chosen_menu_indices: - if self._multi_select: - self._chosen_menu_indices = chosen_menu_indices - else: - self._chosen_menu_index = chosen_menu_indices[0] - return self._chosen_menu_indices if self._multi_select else self._chosen_menu_index - - @property - def chosen_accept_key(self) -> Optional[str]: - return self._chosen_accept_key - - @property - def chosen_menu_entry(self) -> Optional[str]: - return self._menu_entries[self._chosen_menu_index] if self._chosen_menu_index is not None else None - - @property - def chosen_menu_entries(self) -> Optional[Tuple[str, ...]]: - return ( - tuple(self._menu_entries[menu_index] for menu_index in self._chosen_menu_indices) - if self._chosen_menu_indices is not None - else None - ) - - @property - def chosen_menu_index(self) -> Optional[int]: - return self._chosen_menu_index - - @property - def chosen_menu_indices(self) -> Optional[Tuple[int, ...]]: - return self._chosen_menu_indices - - -class AttributeDict(dict): # type: ignore - def __getattr__(self, attr: str) -> Any: - return self[attr] - - def __setattr__(self, attr: str, value: Any) -> None: - self[attr] = value - - -def get_argumentparser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - description=""" -%(prog)s creates simple interactive menus in the terminal and returns the selected entry as exit code. -""", - ) - parser.add_argument( - "-s", "--case-sensitive", action="store_true", dest="case_sensitive", help="searches are case sensitive" - ) - parser.add_argument( - "-X", - "--no-clear-menu-on-exit", - action="store_false", - dest="clear_menu_on_exit", - help="do not clear the menu on exit", - ) - parser.add_argument( - "-l", - "--clear-screen", - action="store_true", - dest="clear_screen", - help="clear the screen before the menu is shown", - ) - parser.add_argument( - "--cursor", - action="store", - dest="cursor", - default=DEFAULT_MENU_CURSOR, - help='menu cursor (default: "%(default)s")', - ) - parser.add_argument( - "-i", - "--cursor-index", - action="store", - dest="cursor_index", - type=int, - default=0, - help="initially selected item index", - ) - parser.add_argument( - "--cursor-style", - action="store", - dest="cursor_style", - default=",".join(DEFAULT_MENU_CURSOR_STYLE), - help='style for the menu cursor as comma separated list (default: "%(default)s")', - ) - parser.add_argument("-C", "--no-cycle", action="store_false", dest="cycle", help="do not cycle the menu selection") - parser.add_argument( - "-E", - "--no-exit-on-shortcut", - action="store_false", - dest="exit_on_shortcut", - help="do not exit on shortcut keys", - ) - parser.add_argument( - "--highlight-style", - action="store", - dest="highlight_style", - default=",".join(DEFAULT_MENU_HIGHLIGHT_STYLE), - help='style for the selected menu entry as comma separated list (default: "%(default)s")', - ) - parser.add_argument( - "-m", - "--multi-select", - action="store_true", - dest="multi_select", - help="Allow the selection of multiple entries (implies `--stdout`)", - ) - parser.add_argument( - "--multi-select-cursor", - action="store", - dest="multi_select_cursor", - default=DEFAULT_MULTI_SELECT_CURSOR, - help='multi-select menu cursor (default: "%(default)s")', - ) - parser.add_argument( - "--multi-select-cursor-brackets-style", - action="store", - dest="multi_select_cursor_brackets_style", - default=",".join(DEFAULT_MULTI_SELECT_CURSOR_BRACKETS_STYLE), - help='style for brackets of the multi-select menu cursor as comma separated list (default: "%(default)s")', - ) - parser.add_argument( - "--multi-select-cursor-style", - action="store", - dest="multi_select_cursor_style", - default=",".join(DEFAULT_MULTI_SELECT_CURSOR_STYLE), - help='style for the multi-select menu cursor as comma separated list (default: "%(default)s")', - ) - parser.add_argument( - "--multi-select-keys", - action="store", - dest="multi_select_keys", - default=",".join(DEFAULT_MULTI_SELECT_KEYS), - help=('key for toggling a selected item in a multi-selection (default: "%(default)s", '), - ) - parser.add_argument( - "--multi-select-no-select-on-accept", - action="store_false", - dest="multi_select_select_on_accept", - help=( - "do not select the currently highlighted menu item when the accept key is pressed " - "(it is still selected if no other item was selected before)" - ), - ) - parser.add_argument( - "--multi-select-empty-ok", - action="store_true", - dest="multi_select_empty_ok", - help=("when used together with --multi-select-no-select-on-accept allows returning no selection at all"), - ) - parser.add_argument( - "-p", - "--preview", - action="store", - dest="preview_command", - help=( - "Command to generate a preview for the selected menu entry. " - '"{}" can be used as placeholder for the menu text. ' - 'If the menu entry has a data component (separated by "|"), this is used instead.' - ), - ) - parser.add_argument( - "--no-preview-border", - action="store_false", - dest="preview_border", - help="do not draw a border around the preview window", - ) - parser.add_argument( - "--preview-size", - action="store", - dest="preview_size", - type=float, - default=DEFAULT_PREVIEW_SIZE, - help='maximum height of the preview window in fractions of the terminal height (default: "%(default)s")', - ) - parser.add_argument( - "--preview-title", - action="store", - dest="preview_title", - default=DEFAULT_PREVIEW_TITLE, - help='title of the preview window (default: "%(default)s")', - ) - parser.add_argument( - "--search-highlight-style", - action="store", - dest="search_highlight_style", - default=",".join(DEFAULT_SEARCH_HIGHLIGHT_STYLE), - help='style of matched search patterns (default: "%(default)s")', - ) - parser.add_argument( - "--search-key", - action="store", - dest="search_key", - default=DEFAULT_SEARCH_KEY, - help=( - 'key to start a search (default: "%(default)s", ' - '"none" is treated a special value which activates the search on any letter key)' - ), - ) - parser.add_argument( - "--shortcut-brackets-highlight-style", - action="store", - dest="shortcut_brackets_highlight_style", - default=",".join(DEFAULT_SHORTCUT_BRACKETS_HIGHLIGHT_STYLE), - help='style of brackets enclosing shortcut keys (default: "%(default)s")', - ) - parser.add_argument( - "--shortcut-key-highlight-style", - action="store", - dest="shortcut_key_highlight_style", - default=",".join(DEFAULT_SHORTCUT_KEY_HIGHLIGHT_STYLE), - help='style of shortcut keys (default: "%(default)s")', - ) - parser.add_argument( - "--show-multi-select-hint", - action="store_true", - dest="show_multi_select_hint", - help="show a multi-select hint in the status bar", - ) - parser.add_argument( - "--show-multi-select-hint-text", - action="store", - dest="show_multi_select_hint_text", - help=( - "Custom text which will be shown as multi-select hint. Use the placeholders {multi_select_keys} and " - "{accept_keys} if appropriately." - ), - ) - parser.add_argument( - "--show-search-hint", - action="store_true", - dest="show_search_hint", - help="show a search hint in the search line", - ) - parser.add_argument( - "--show-search-hint-text", - action="store", - dest="show_search_hint_text", - help=( - "Custom text which will be shown as search hint. Use the placeholders {key} for the search key " - "if appropriately." - ), - ) - parser.add_argument( - "--show-shortcut-hints", - action="store_true", - dest="show_shortcut_hints", - help="show shortcut hints in the status bar", - ) - parser.add_argument( - "--show-shortcut-hints-in-title", - action="store_false", - dest="show_shortcut_hints_in_status_bar", - default=True, - help="show shortcut hints in the menu title", - ) - parser.add_argument( - "--skip-empty-entries", - action="store_true", - dest="skip_empty_entries", - help="Interpret an empty string in menu entries as an empty menu entry", - ) - parser.add_argument( - "-b", - "--status-bar", - action="store", - dest="status_bar", - help="status bar text", - ) - parser.add_argument( - "-d", - "--status-bar-below-preview", - action="store_true", - dest="status_bar_below_preview", - help="show the status bar below the preview window if any", - ) - parser.add_argument( - "--status-bar-style", - action="store", - dest="status_bar_style", - default=",".join(DEFAULT_STATUS_BAR_STYLE), - help='style of the status bar lines (default: "%(default)s")', - ) - parser.add_argument( - "--stdout", - action="store_true", - dest="stdout", - help=( - "Print the selected menu index or indices to stdout (in addition to the exit status). " - 'Multiple indices are separated by ";".' - ), - ) - parser.add_argument("-t", "--title", action="store", dest="title", help="menu title") - parser.add_argument( - "-V", "--version", action="store_true", dest="print_version", help="print the version number and exit" - ) - parser.add_argument("entries", action="store", nargs="*", help="the menu entries to show") - group = parser.add_mutually_exclusive_group() - group.add_argument( - "-r", - "--preselected_entries", - action="store", - dest="preselected_entries", - help="Comma separated list of strings matching menu items to start pre-selected in a multi-select menu.", - ) - group.add_argument( - "-R", - "--preselected_indices", - action="store", - dest="preselected_indices", - help="Comma separated list of numeric indexes of menu items to start pre-selected in a multi-select menu.", - ) - return parser - - -def parse_arguments() -> AttributeDict: - parser = get_argumentparser() - args = AttributeDict({key: value for key, value in vars(parser.parse_args()).items()}) - if not args.print_version and not args.entries: - raise NoMenuEntriesError("No menu entries given!") - if args.skip_empty_entries: - args.entries = [entry if entry != "None" else None for entry in args.entries] - if args.cursor_style != "": - args.cursor_style = tuple(args.cursor_style.split(",")) - else: - args.cursor_style = None - if args.highlight_style != "": - args.highlight_style = tuple(args.highlight_style.split(",")) - else: - args.highlight_style = None - if args.search_highlight_style != "": - args.search_highlight_style = tuple(args.search_highlight_style.split(",")) - else: - args.search_highlight_style = None - if args.shortcut_key_highlight_style != "": - args.shortcut_key_highlight_style = tuple(args.shortcut_key_highlight_style.split(",")) - else: - args.shortcut_key_highlight_style = None - if args.shortcut_brackets_highlight_style != "": - args.shortcut_brackets_highlight_style = tuple(args.shortcut_brackets_highlight_style.split(",")) - else: - args.shortcut_brackets_highlight_style = None - if args.status_bar_style != "": - args.status_bar_style = tuple(args.status_bar_style.split(",")) - else: - args.status_bar_style = None - if args.multi_select_cursor_brackets_style != "": - args.multi_select_cursor_brackets_style = tuple(args.multi_select_cursor_brackets_style.split(",")) - else: - args.multi_select_cursor_brackets_style = None - if args.multi_select_cursor_style != "": - args.multi_select_cursor_style = tuple(args.multi_select_cursor_style.split(",")) - else: - args.multi_select_cursor_style = None - if args.multi_select_keys != "": - args.multi_select_keys = tuple(args.multi_select_keys.split(",")) - else: - args.multi_select_keys = None - if args.search_key.lower() == "none": - args.search_key = None - if args.show_shortcut_hints_in_status_bar: - args.show_shortcut_hints = True - if args.multi_select: - args.stdout = True - if args.preselected_entries is not None: - args.preselected = list(args.preselected_entries.split(",")) - elif args.preselected_indices is not None: - args.preselected = list(map(int, args.preselected_indices.split(","))) - else: - args.preselected = None - return args - - -def main() -> None: - try: - args = parse_arguments() - except SystemExit: - sys.exit(0) # Error code 0 is the error case in this program - except NoMenuEntriesError as e: - print(str(e), file=sys.stderr) - sys.exit(0) - if args.print_version: - print("{}, version {}".format(os.path.basename(sys.argv[0]), __version__)) - sys.exit(0) - try: - terminal_menu = TerminalMenu( - menu_entries=args.entries, - clear_menu_on_exit=args.clear_menu_on_exit, - clear_screen=args.clear_screen, - cursor_index=args.cursor_index, - cycle_cursor=args.cycle, - exit_on_shortcut=args.exit_on_shortcut, - menu_cursor=args.cursor, - menu_cursor_style=args.cursor_style, - menu_highlight_style=args.highlight_style, - multi_select=args.multi_select, - multi_select_cursor=args.multi_select_cursor, - multi_select_cursor_brackets_style=args.multi_select_cursor_brackets_style, - multi_select_cursor_style=args.multi_select_cursor_style, - multi_select_empty_ok=args.multi_select_empty_ok, - multi_select_keys=args.multi_select_keys, - multi_select_select_on_accept=args.multi_select_select_on_accept, - preselected_entries=args.preselected, - preview_border=args.preview_border, - preview_command=args.preview_command, - preview_size=args.preview_size, - preview_title=args.preview_title, - search_case_sensitive=args.case_sensitive, - search_highlight_style=args.search_highlight_style, - search_key=args.search_key, - shortcut_brackets_highlight_style=args.shortcut_brackets_highlight_style, - shortcut_key_highlight_style=args.shortcut_key_highlight_style, - show_multi_select_hint=args.show_multi_select_hint, - show_multi_select_hint_text=args.show_multi_select_hint_text, - show_search_hint=args.show_search_hint, - show_search_hint_text=args.show_search_hint_text, - show_shortcut_hints=args.show_shortcut_hints, - show_shortcut_hints_in_status_bar=args.show_shortcut_hints_in_status_bar, - skip_empty_entries=args.skip_empty_entries, - status_bar=args.status_bar, - status_bar_below_preview=args.status_bar_below_preview, - status_bar_style=args.status_bar_style, - title=args.title, - ) - except (InvalidParameterCombinationError, InvalidStyleError, UnknownMenuEntryError) as e: - print(str(e), file=sys.stderr) - sys.exit(0) - chosen_entries = terminal_menu.show() - if chosen_entries is None: - sys.exit(0) - else: - if isinstance(chosen_entries, Iterable): - if args.stdout: - print(",".join(str(entry + 1) for entry in chosen_entries)) - sys.exit(chosen_entries[0] + 1) - else: - chosen_entry = chosen_entries - if args.stdout: - print(chosen_entry + 1) - sys.exit(chosen_entry + 1) - - -if __name__ == "__main__": - main() diff --git a/archinstall/lib/menu/table_selection_menu.py b/archinstall/lib/menu/table_selection_menu.py index 09cd6ee2..4cff7216 100644 --- a/archinstall/lib/menu/table_selection_menu.py +++ b/archinstall/lib/menu/table_selection_menu.py @@ -1,19 +1,24 @@ -from typing import Any, Tuple, List, Dict, Optional +from typing import Any, Tuple, List, Dict, Optional, Callable -from .menu import MenuSelectionType, MenuSelection +from .menu import MenuSelectionType, MenuSelection, Menu from ..output import FormattedOutput -from ..menu import Menu class TableMenu(Menu): def __init__( self, title: str, - data: List[Any] = [], + data: Optional[List[Any]] = None, table_data: Optional[Tuple[List[Any], str]] = None, + preset: List[Any] = [], custom_menu_options: List[str] = [], default: Any = None, - multi: bool = False + multi: bool = False, + preview_command: Optional[Callable] = None, + preview_title: str = 'Info', + preview_size: float = 0.0, + allow_reset: bool = True, + allow_reset_warning_msg: Optional[str] = None, ): """ param title: Text that will be displayed above the menu @@ -29,10 +34,10 @@ class TableMenu(Menu): param custom_options: List of custom options that will be displayed under the table :type custom_menu_options: List - """ - if not data and not table_data: - raise ValueError('Either "data" or "table_data" must be provided') + :param preview_command: A function that should return a string that will be displayed in a preview window when a menu selection item is in focus + :type preview_command: Callable + """ self._custom_options = custom_menu_options self._multi = multi @@ -41,7 +46,7 @@ class TableMenu(Menu): else: header_padding = 2 - if len(data): + if data is not None: table_text = FormattedOutput.as_table(data) rows = table_text.split('\n') table = self._create_table(data, rows, header_padding=header_padding) @@ -53,20 +58,53 @@ class TableMenu(Menu): data = table_data[0] rows = table_data[1].split('\n') table = self._create_table(data, rows, header_padding=header_padding) + else: + raise ValueError('Either "data" or "table_data" must be provided') self._options, header = self._prepare_selection(table) + preset_values = self._preset_values(preset) + + extra_bottom_space = True if preview_command else False + super().__init__( title, self._options, + preset_values=preset_values, header=header, skip_empty_entries=True, show_search_hint=False, - allow_reset=True, multi=multi, - default_option=default + default_option=default, + preview_command=lambda x: self._table_show_preview(preview_command, x), + preview_size=preview_size, + preview_title=preview_title, + extra_bottom_space=extra_bottom_space, + allow_reset=allow_reset, + allow_reset_warning_msg=allow_reset_warning_msg ) + def _preset_values(self, preset: List[Any]) -> List[str]: + # when we create the table of just the preset values it will + # be formatted a bit different due to spacing, so to determine + # correct rows lets remove all the spaces and compare apples with apples + preset_table = FormattedOutput.as_table(preset).strip() + data_rows = preset_table.split('\n')[2:] # get all data rows + pure_data_rows = [self._escape_row(row.replace(' ', '')) for row in data_rows] + + # the actual preset value has to be in non-escaped form + pure_option_rows = {o.replace(' ', ''): self._unescape_row(o) for o in self._options.keys()} + preset_rows = [row for pure, row in pure_option_rows.items() if pure in pure_data_rows] + + return preset_rows + + def _table_show_preview(self, preview_command: Optional[Callable], selection: Any) -> Optional[str]: + if preview_command: + row = self._escape_row(selection) + obj = self._options[row] + return preview_command(obj) + return None + def run(self) -> MenuSelection: choice = super().run() @@ -79,6 +117,12 @@ class TableMenu(Menu): return choice + def _escape_row(self, row: str) -> str: + return row.replace('|', '\\|') + + def _unescape_row(self, row: str) -> str: + return row.replace('\\|', '|') + def _create_table(self, data: List[Any], rows: List[str], header_padding: int = 2) -> Dict[str, Any]: # these are the header rows of the table and do not map to any data obviously # we're adding 2 spaces as prefix because the menu selector '> ' will be put before @@ -87,7 +131,7 @@ class TableMenu(Menu): display_data = {f'{padding}{rows[0]}': None, f'{padding}{rows[1]}': None} for row, entry in zip(rows[2:], data): - row = row.replace('|', '\\|') + row = self._escape_row(row) display_data[row] = entry return display_data diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index d76e0473..4bae6d8b 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -2,7 +2,7 @@ import logging import pathlib import urllib.error import urllib.request -from typing import Union, Mapping, Iterable, Dict, Any, List +from typing import Union, Iterable, Dict, Any, List from .general import SysCommand from .output import log @@ -121,7 +121,7 @@ def insert_mirrors(mirrors :Dict[str, Any], *args :str, **kwargs :str) -> bool: def use_mirrors( - regions: Mapping[str, Iterable[str]], + regions: Dict[str, Iterable[str]], destination: str = '/etc/pacman.d/mirrorlist' ) -> None: log(f'A new package mirror-list has been created: {destination}', level=logging.INFO) diff --git a/archinstall/lib/models/__init__.py b/archinstall/lib/models/__init__.py index 4a018b2c..8cc49ea0 100644 --- a/archinstall/lib/models/__init__.py +++ b/archinstall/lib/models/__init__.py @@ -1 +1,4 @@ -from .network_configuration import NetworkConfiguration as NetworkConfiguration \ No newline at end of file +from .network_configuration import NetworkConfiguration, NicType, NetworkConfigurationHandler +from .bootloader import Bootloader +from .gen import VersionDef, PackageSearchResult, PackageSearch, LocalPackage +from .users import PasswordStrength, User diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py new file mode 100644 index 00000000..38254c99 --- /dev/null +++ b/archinstall/lib/models/bootloader.py @@ -0,0 +1,40 @@ +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 + + +class Bootloader(Enum): + Systemd = 'Systemd-boot' + Grub = 'Grub' + Efistub = 'Efistub' + + def json(self): + return self.value + + @classmethod + def values(cls) -> List[str]: + return [e.value for e in cls] + + @classmethod + def get_default(cls) -> Bootloader: + if has_uefi(): + return Bootloader.Systemd + else: + return Bootloader.Grub + + @classmethod + def from_arg(cls, bootloader: str) -> Bootloader: + # to support old configuration files + bootloader = bootloader.capitalize() + + if bootloader not in cls.values(): + values = ', '.join(cls.values()) + log(f'Invalid bootloader value "{bootloader}". Allowed values: {values}', level=logging.WARN) + sys.exit(1) + return Bootloader(bootloader) diff --git a/archinstall/lib/models/dataclasses.py b/archinstall/lib/models/dataclasses.py deleted file mode 100644 index 99221fe3..00000000 --- a/archinstall/lib/models/dataclasses.py +++ /dev/null @@ -1,136 +0,0 @@ -from dataclasses import dataclass -from typing import Optional, List - -@dataclass -class VersionDef: - version_string: str - - @classmethod - def parse_version(self) -> List[str]: - if '.' in self.version_string: - versions = self.version_string.split('.') - else: - versions = [self.version_string] - - return versions - - @classmethod - def major(self) -> str: - return self.parse_version()[0] - - @classmethod - def minor(self) -> str: - versions = self.parse_version() - if len(versions) >= 2: - return versions[1] - - @classmethod - def patch(self) -> str: - versions = self.parse_version() - if '-' in versions[-1]: - _, patch_version = versions[-1].split('-', 1) - return patch_version - - def __eq__(self, other :'VersionDef') -> bool: - if other.major == self.major and \ - other.minor == self.minor and \ - other.patch == self.patch: - - return True - return False - - def __lt__(self, other :'VersionDef') -> bool: - if self.major > other.major: - return False - elif self.minor and other.minor and self.minor > other.minor: - return False - elif self.patch and other.patch and self.patch > other.patch: - return False - - def __str__(self) -> str: - return self.version_string - -@dataclass -class PackageSearchResult: - pkgname: str - pkgbase: str - repo: str - arch: str - pkgver: str - pkgrel: str - epoch: int - pkgdesc: str - url: str - filename: str - compressed_size: int - installed_size: int - build_date: str - last_update: str - flag_date: Optional[str] - maintainers: List[str] - packager: str - groups: List[str] - licenses: List[str] - conflicts: List[str] - provides: List[str] - replaces: List[str] - depends: List[str] - optdepends: List[str] - makedepends: List[str] - checkdepends: List[str] - - @property - def pkg_version(self) -> str: - return self.pkgver - - def __eq__(self, other :'VersionDef') -> bool: - return self.pkg_version == other.pkg_version - - def __lt__(self, other :'VersionDef') -> bool: - return self.pkg_version < other.pkg_version - -@dataclass -class PackageSearch: - version: int - limit: int - valid: bool - num_pages: int - page: int - results: List[PackageSearchResult] - - def __post_init__(self): - self.results = [PackageSearchResult(**x) for x in self.results] - -@dataclass -class LocalPackage: - name: str - version: str - description:str - architecture: str - url: str - licenses: str - groups: str - depends_on: str - optional_deps: str - required_by: str - optional_for: str - conflicts_with: str - replaces: str - installed_size: str - packager: str - build_date: str - install_date: str - install_reason: str - install_script: str - validated_by: str - provides: str - - @property - def pkg_version(self) -> str: - return self.version - - def __eq__(self, other :'VersionDef') -> bool: - return self.pkg_version == other.pkg_version - - def __lt__(self, other :'VersionDef') -> bool: - return self.pkg_version < other.pkg_version \ No newline at end of file diff --git a/archinstall/lib/models/disk_encryption.py b/archinstall/lib/models/disk_encryption.py deleted file mode 100644 index a4a501d9..00000000 --- a/archinstall/lib/models/disk_encryption.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from enum import Enum -from typing import Optional, List, Dict, TYPE_CHECKING, Any - -from ..hsm.fido import Fido2Device - -if TYPE_CHECKING: - _: Any - - -class EncryptionType(Enum): - Partition = 'partition' - - @classmethod - def _encryption_type_mapper(cls) -> Dict[str, 'EncryptionType']: - return { - # str(_('Full disk encryption')): EncryptionType.FullDiskEncryption, - str(_('Partition encryption')): EncryptionType.Partition - } - - @classmethod - def text_to_type(cls, text: str) -> 'EncryptionType': - mapping = cls._encryption_type_mapper() - return mapping[text] - - @classmethod - def type_to_text(cls, type_: 'EncryptionType') -> str: - mapping = cls._encryption_type_mapper() - type_to_text = {type_: text for text, type_ in mapping.items()} - return type_to_text[type_] - - -@dataclass -class DiskEncryption: - encryption_type: EncryptionType = EncryptionType.Partition - encryption_password: str = '' - partitions: Dict[str, List[Dict[str, Any]]] = field(default_factory=dict) - hsm_device: Optional[Fido2Device] = None - - @property - def all_partitions(self) -> List[Dict[str, Any]]: - _all: List[Dict[str, Any]] = [] - for parts in self.partitions.values(): - _all += parts - return _all - - def generate_encryption_file(self, partition) -> bool: - return partition in self.all_partitions and partition['mountpoint'] != '/' - - def json(self) -> Dict[str, Any]: - obj = { - 'encryption_type': self.encryption_type.value, - 'partitions': self.partitions - } - - if self.hsm_device: - obj['hsm_device'] = self.hsm_device.json() - - return obj - - @classmethod - def parse_arg( - cls, - disk_layout: Dict[str, Any], - arg: Dict[str, Any], - password: str = '' - ) -> 'DiskEncryption': - # we have to map the enc partition config to the disk layout objects - # they both need to point to the same object as it will get modified - # during the installation process - enc_partitions: Dict[str, List[Dict[str, Any]]] = {} - - for path, partitions in disk_layout.items(): - conf_partitions = arg['partitions'].get(path, []) - for part in partitions['partitions']: - if part in conf_partitions: - enc_partitions.setdefault(path, []).append(part) - - enc = DiskEncryption( - EncryptionType(arg['encryption_type']), - password, - enc_partitions - ) - - if hsm := arg.get('hsm_device', None): - enc.hsm_device = Fido2Device.parse_arg(hsm) - - return enc diff --git a/archinstall/lib/models/gen.py b/archinstall/lib/models/gen.py new file mode 100644 index 00000000..cc8d7605 --- /dev/null +++ b/archinstall/lib/models/gen.py @@ -0,0 +1,146 @@ +from dataclasses import dataclass +from typing import Optional, List + + +@dataclass +class VersionDef: + version_string: str + + @classmethod + def parse_version(cls) -> List[str]: + if '.' in cls.version_string: + versions = cls.version_string.split('.') + else: + versions = [cls.version_string] + + return versions + + @classmethod + def major(self) -> str: + return self.parse_version()[0] + + @classmethod + def minor(cls) -> Optional[str]: + versions = cls.parse_version() + if len(versions) >= 2: + return versions[1] + + return None + + @classmethod + def patch(cls) -> Optional[str]: + versions = cls.parse_version() + if '-' in versions[-1]: + _, patch_version = versions[-1].split('-', 1) + return patch_version + + return None + + def __eq__(self, other) -> bool: + if other.major == self.major and \ + other.minor == self.minor and \ + other.patch == self.patch: + + return True + return False + + def __lt__(self, other) -> bool: + if self.major() > other.major(): + return False + elif self.minor() and other.minor() and self.minor() > other.minor(): + return False + elif self.patch() and other.patch() and self.patch() > other.patch(): + return False + + return True + + def __str__(self) -> str: + return self.version_string + + +@dataclass +class PackageSearchResult: + pkgname: str + pkgbase: str + repo: str + arch: str + pkgver: str + pkgrel: str + epoch: int + pkgdesc: str + url: str + filename: str + compressed_size: int + installed_size: int + build_date: str + last_update: str + flag_date: Optional[str] + maintainers: List[str] + packager: str + groups: List[str] + licenses: List[str] + conflicts: List[str] + provides: List[str] + replaces: List[str] + depends: List[str] + optdepends: List[str] + makedepends: List[str] + checkdepends: List[str] + + @property + def pkg_version(self) -> str: + return self.pkgver + + def __eq__(self, other) -> bool: + return self.pkg_version == other.pkg_version + + def __lt__(self, other) -> bool: + return self.pkg_version < other.pkg_version + + +@dataclass +class PackageSearch: + version: int + limit: int + valid: bool + num_pages: int + page: int + results: List[PackageSearchResult] + + def __post_init__(self): + self.results = [PackageSearchResult(**x) for x in self.results] + + +@dataclass +class LocalPackage: + name: str + version: str + description:str + architecture: str + url: str + licenses: str + groups: str + depends_on: str + optional_deps: str + required_by: str + optional_for: str + conflicts_with: str + replaces: str + installed_size: str + packager: str + build_date: str + install_date: str + install_reason: str + install_script: str + validated_by: str + provides: str + + @property + def pkg_version(self) -> str: + return self.version + + def __eq__(self, other) -> bool: + return self.pkg_version == other.pkg_version + + def __lt__(self, other) -> bool: + return self.pkg_version < other.pkg_version diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index e026e97b..b7ab690d 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -93,7 +93,7 @@ class NetworkConfigurationHandler: enable_services=True) # Sources the ISO network configuration to the install medium. elif self._configuration.is_network_manager(): installation.add_additional_packages(["networkmanager"]) - if (profile := storage['arguments'].get('profile')) and profile.is_desktop_profile: + if (profile := storage['arguments'].get('profile_config')) and profile.is_desktop_type_profile: installation.add_additional_packages(["network-manager-applet"]) installation.enable_service('NetworkManager.service') diff --git a/archinstall/lib/models/password_strength.py b/archinstall/lib/models/password_strength.py deleted file mode 100644 index 61986bf0..00000000 --- a/archinstall/lib/models/password_strength.py +++ /dev/null @@ -1,85 +0,0 @@ -from enum import Enum - - -class PasswordStrength(Enum): - VERY_WEAK = 'very weak' - WEAK = 'weak' - MODERATE = 'moderate' - STRONG = 'strong' - - @property - def value(self): - match self: - case PasswordStrength.VERY_WEAK: return str(_('very weak')) - case PasswordStrength.WEAK: return str(_('weak')) - case PasswordStrength.MODERATE: return str(_('moderate')) - case PasswordStrength.STRONG: return str(_('strong')) - - def color(self): - match self: - case PasswordStrength.VERY_WEAK: return 'red' - case PasswordStrength.WEAK: return 'red' - case PasswordStrength.MODERATE: return 'yellow' - case PasswordStrength.STRONG: return 'green' - - @classmethod - def strength(cls, password: str) -> 'PasswordStrength': - digit = any(character.isdigit() for character in password) - upper = any(character.isupper() for character in password) - lower = any(character.islower() for character in password) - symbol = any(not character.isalnum() for character in password) - return cls._check_password_strength(digit, upper, lower, symbol, len(password)) - - @classmethod - def _check_password_strength( - cls, - digit: bool, - upper: bool, - lower: bool, - symbol: bool, - length: int - ) -> 'PasswordStrength': - # suggested evaluation - # https://github.com/archlinux/archinstall/issues/1304#issuecomment-1146768163 - if digit and upper and lower and symbol: - match length: - case num if 13 <= num: - return PasswordStrength.STRONG - case num if 11 <= num <= 12: - return PasswordStrength.MODERATE - case num if 7 <= num <= 10: - return PasswordStrength.WEAK - case num if num <= 6: - return PasswordStrength.VERY_WEAK - elif digit and upper and lower: - match length: - case num if 14 <= num: - return PasswordStrength.STRONG - case num if 11 <= num <= 13: - return PasswordStrength.MODERATE - case num if 7 <= num <= 10: - return PasswordStrength.WEAK - case num if num <= 6: - return PasswordStrength.VERY_WEAK - elif upper and lower: - match length: - case num if 15 <= num: - return PasswordStrength.STRONG - case num if 12 <= num <= 14: - return PasswordStrength.MODERATE - case num if 7 <= num <= 11: - return PasswordStrength.WEAK - case num if num <= 6: - return PasswordStrength.VERY_WEAK - elif lower or upper: - match length: - case num if 18 <= num: - return PasswordStrength.STRONG - case num if 14 <= num <= 17: - return PasswordStrength.MODERATE - case num if 9 <= num <= 13: - return PasswordStrength.WEAK - case num if num <= 8: - return PasswordStrength.VERY_WEAK - - return PasswordStrength.VERY_WEAK diff --git a/archinstall/lib/models/pydantic.py b/archinstall/lib/models/pydantic.py deleted file mode 100644 index 799e92af..00000000 --- a/archinstall/lib/models/pydantic.py +++ /dev/null @@ -1,134 +0,0 @@ -from typing import Optional, List -from pydantic import BaseModel - -""" -This python file is not in use. -Pydantic is not a builtin, and we use the dataclasses.py instead! -""" - -class VersionDef(BaseModel): - version_string: str - - @classmethod - def parse_version(self) -> List[str]: - if '.' in self.version_string: - versions = self.version_string.split('.') - else: - versions = [self.version_string] - - return versions - - @classmethod - def major(self) -> str: - return self.parse_version()[0] - - @classmethod - def minor(self) -> str: - versions = self.parse_version() - if len(versions) >= 2: - return versions[1] - - @classmethod - def patch(self) -> str: - versions = self.parse_version() - if '-' in versions[-1]: - _, patch_version = versions[-1].split('-', 1) - return patch_version - - def __eq__(self, other :'VersionDef') -> bool: - if other.major == self.major and \ - other.minor == self.minor and \ - other.patch == self.patch: - - return True - return False - - def __lt__(self, other :'VersionDef') -> bool: - if self.major > other.major: - return False - elif self.minor and other.minor and self.minor > other.minor: - return False - elif self.patch and other.patch and self.patch > other.patch: - return False - - def __str__(self) -> str: - return self.version_string - - -class PackageSearchResult(BaseModel): - pkgname: str - pkgbase: str - repo: str - arch: str - pkgver: str - pkgrel: str - epoch: int - pkgdesc: str - url: str - filename: str - compressed_size: int - installed_size: int - build_date: str - last_update: str - flag_date: Optional[str] - maintainers: List[str] - packager: str - groups: List[str] - licenses: List[str] - conflicts: List[str] - provides: List[str] - replaces: List[str] - depends: List[str] - optdepends: List[str] - makedepends: List[str] - checkdepends: List[str] - - @property - def pkg_version(self) -> str: - return self.pkgver - - def __eq__(self, other :'VersionDef') -> bool: - return self.pkg_version == other.pkg_version - - def __lt__(self, other :'VersionDef') -> bool: - return self.pkg_version < other.pkg_version - - -class PackageSearch(BaseModel): - version: int - limit: int - valid: bool - results: List[PackageSearchResult] - - -class LocalPackage(BaseModel): - name: str - version: str - description:str - architecture: str - url: str - licenses: str - groups: str - depends_on: str - optional_deps: str - required_by: str - optional_for: str - conflicts_with: str - replaces: str - installed_size: str - packager: str - build_date: str - install_date: str - install_reason: str - install_script: str - validated_by: str - - @property - def pkg_version(self) -> str: - return self.version - - def __eq__(self, other :'VersionDef') -> bool: - return self.pkg_version == other.pkg_version - - def __lt__(self, other :'VersionDef') -> bool: - return self.pkg_version < other.pkg_version \ No newline at end of file diff --git a/archinstall/lib/models/subvolume.py b/archinstall/lib/models/subvolume.py deleted file mode 100644 index 34a09227..00000000 --- a/archinstall/lib/models/subvolume.py +++ /dev/null @@ -1,68 +0,0 @@ -from dataclasses import dataclass -from typing import List, Any, Dict - - -@dataclass -class Subvolume: - name: str - mountpoint: str - compress: bool = False - nodatacow: bool = False - - def display(self) -> str: - options_str = ','.join(self.options) - return f'{_("Subvolume")}: {self.name:15} {_("Mountpoint")}: {self.mountpoint:20} {_("Options")}: {options_str}' - - @property - def options(self) -> List[str]: - options = [ - 'compress' if self.compress else '', - 'nodatacow' if self.nodatacow else '' - ] - return [o for o in options if len(o)] - - def json(self) -> Dict[str, Any]: - return { - 'name': self.name, - 'mountpoint': self.mountpoint, - 'compress': self.compress, - 'nodatacow': self.nodatacow - } - - @classmethod - def _parse(cls, config_subvolumes: List[Dict[str, Any]]) -> List['Subvolume']: - subvolumes = [] - for entry in config_subvolumes: - if not entry.get('name', None) or not entry.get('mountpoint', None): - continue - - subvolumes.append( - Subvolume( - entry['name'], - entry['mountpoint'], - entry.get('compress', False), - entry.get('nodatacow', False) - ) - ) - - return subvolumes - - @classmethod - def _parse_backwards_compatible(cls, config_subvolumes) -> List['Subvolume']: - subvolumes = [] - for name, mountpoint in config_subvolumes.items(): - if not name or not mountpoint: - continue - - subvolumes.append(Subvolume(name, mountpoint)) - - return subvolumes - - @classmethod - def parse_arguments(cls, config_subvolumes: Any) -> List['Subvolume']: - if isinstance(config_subvolumes, list): - return cls._parse(config_subvolumes) - elif isinstance(config_subvolumes, dict): - return cls._parse_backwards_compatible(config_subvolumes) - - raise ValueError('Unknown disk layout btrfs subvolume format') diff --git a/archinstall/lib/models/users.py b/archinstall/lib/models/users.py index a8feb9ef..9ed70eef 100644 --- a/archinstall/lib/models/users.py +++ b/archinstall/lib/models/users.py @@ -1,12 +1,95 @@ from dataclasses import dataclass from typing import Dict, List, Union, Any, TYPE_CHECKING - -from .password_strength import PasswordStrength +from enum import Enum if TYPE_CHECKING: _: Any +class PasswordStrength(Enum): + VERY_WEAK = 'very weak' + WEAK = 'weak' + MODERATE = 'moderate' + STRONG = 'strong' + + @property + def value(self): + match self: + case PasswordStrength.VERY_WEAK: return str(_('very weak')) + case PasswordStrength.WEAK: return str(_('weak')) + case PasswordStrength.MODERATE: return str(_('moderate')) + case PasswordStrength.STRONG: return str(_('strong')) + + def color(self): + match self: + case PasswordStrength.VERY_WEAK: return 'red' + case PasswordStrength.WEAK: return 'red' + case PasswordStrength.MODERATE: return 'yellow' + case PasswordStrength.STRONG: return 'green' + + @classmethod + def strength(cls, password: str) -> 'PasswordStrength': + digit = any(character.isdigit() for character in password) + upper = any(character.isupper() for character in password) + lower = any(character.islower() for character in password) + symbol = any(not character.isalnum() for character in password) + return cls._check_password_strength(digit, upper, lower, symbol, len(password)) + + @classmethod + def _check_password_strength( + cls, + digit: bool, + upper: bool, + lower: bool, + symbol: bool, + length: int + ) -> 'PasswordStrength': + # suggested evaluation + # https://github.com/archlinux/archinstall/issues/1304#issuecomment-1146768163 + if digit and upper and lower and symbol: + match length: + case num if 13 <= num: + return PasswordStrength.STRONG + case num if 11 <= num <= 12: + return PasswordStrength.MODERATE + case num if 7 <= num <= 10: + return PasswordStrength.WEAK + case num if num <= 6: + return PasswordStrength.VERY_WEAK + elif digit and upper and lower: + match length: + case num if 14 <= num: + return PasswordStrength.STRONG + case num if 11 <= num <= 13: + return PasswordStrength.MODERATE + case num if 7 <= num <= 10: + return PasswordStrength.WEAK + case num if num <= 6: + return PasswordStrength.VERY_WEAK + elif upper and lower: + match length: + case num if 15 <= num: + return PasswordStrength.STRONG + case num if 12 <= num <= 14: + return PasswordStrength.MODERATE + case num if 7 <= num <= 11: + return PasswordStrength.WEAK + case num if num <= 6: + return PasswordStrength.VERY_WEAK + elif lower or upper: + match length: + case num if 18 <= num: + return PasswordStrength.STRONG + case num if 14 <= num <= 17: + return PasswordStrength.MODERATE + case num if 9 <= num <= 13: + return PasswordStrength.WEAK + case num if num <= 8: + return PasswordStrength.VERY_WEAK + + return PasswordStrength.VERY_WEAK + + @dataclass class User: username: str @@ -26,13 +109,6 @@ class User: 'sudo': self.sudo } - def display(self) -> str: - password = '*' * (len(self.password) if self.password else 0) - if password: - strength = PasswordStrength.strength(self.password) - password += f' ({strength.value})' - return f'{_("Username")}: {self.username:16} {_("Password")}: {password:20} sudo: {str(self.sudo)}' - @classmethod def _parse(cls, config_users: List[Dict[str, Any]]) -> List['User']: users = [] diff --git a/archinstall/lib/networking.py b/archinstall/lib/networking.py index 96e8f3a1..3516aac4 100644 --- a/archinstall/lib/networking.py +++ b/archinstall/lib/networking.py @@ -1,8 +1,12 @@ import logging import os import socket +import ssl import struct -from typing import Union, Dict, Any, List +from typing import Union, Dict, Any, List, Optional +from urllib.error import URLError +from urllib.parse import urlencode +from urllib.request import urlopen from .exceptions import HardwareIncompatibilityError, SysCallError from .general import SysCommand @@ -39,7 +43,7 @@ def check_mirror_reachable() -> bool: elif os.geteuid() != 0: log("check_mirror_reachable() uses 'pacman -Sy' which requires root.", level=logging.ERROR, fg="red") except SysCallError as err: - log(err, level=logging.DEBUG) + log(f'exit_code: {err.exit_code}, Error: {err.message}', level=logging.DEBUG) return False @@ -75,12 +79,8 @@ def enrich_iface_types(interfaces: Union[Dict[str, Any], List[str]]) -> Dict[str return result -def get_interface_from_mac(mac :str) -> str: - return list_interfaces().get(mac.lower(), None) - - def wireless_scan(interface :str) -> None: - interfaces = enrich_iface_types(list_interfaces().values()) + interfaces = enrich_iface_types(list(list_interfaces().values())) if interfaces[interface] != 'WIRELESS': raise HardwareIncompatibilityError(f"Interface {interface} is not a wireless interface: {interfaces}") @@ -107,3 +107,22 @@ def get_wireless_networks(interface :str) -> None: 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 + ssl_context.verify_mode = ssl.CERT_NONE + + if params is not None: + encoded = urlencode(params) + full_url = f'{url}?{encoded}' + else: + full_url = url + + try: + response = urlopen(full_url, context=ssl_context) + data = response.read().decode('UTF-8') + return data + except URLError: + raise ValueError(f'Unable to fetch data from url: {url}') diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index 709a7382..d65f835f 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -2,7 +2,7 @@ import logging import os import sys from pathlib import Path -from typing import Dict, Union, List, Any, Callable +from typing import Dict, Union, List, Any, Callable, Optional from .storage import storage from dataclasses import asdict, is_dataclass @@ -11,7 +11,12 @@ from dataclasses import asdict, is_dataclass class FormattedOutput: @classmethod - def values(cls, o: Any, class_formatter: str = None, filter_list: List[str] = None) -> Dict[str, Any]: + def values( + cls, + o: Any, + class_formatter: Optional[Union[str, Callable]] = None, + filter_list: List[str] = [] + ) -> 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. Can transmit a filter list to the class_formatter, @@ -25,7 +30,8 @@ class FormattedOutput: elif hasattr(o, class_formatter) and callable(getattr(o, class_formatter)): func = getattr(o, class_formatter) return func(filter_list) - # kept as to make it backward compatible + + raise ValueError('Unsupported formatting call') elif hasattr(o, 'as_json'): return o.as_json() elif hasattr(o, 'json'): @@ -36,7 +42,13 @@ class FormattedOutput: return o.__dict__ @classmethod - def as_table(cls, obj: List[Any], class_formatter: Union[str, Callable] = None, filter_list: List[str] = None) -> str: + def as_table( + cls, + obj: List[Any], + class_formatter: Optional[Union[str, Callable]] = None, + filter_list: List[str] = [], + capitalize: bool = False + ) -> str: """ variant of as_table (subtly different code) which has two additional parameters filter which is a list of fields which will be shon class_formatter a special method to format the outgoing data @@ -46,6 +58,7 @@ class FormattedOutput: As_table_filter can be a drop in replacement for as_table """ raw_data = [cls.values(o, class_formatter, filter_list) for o in obj] + # determine the maximum column size column_width: Dict[str, int] = {} for o in raw_data: @@ -55,14 +68,20 @@ class FormattedOutput: column_width[k] = max([column_width[k], len(str(v)), len(k)]) if not filter_list: - filter_list = (column_width.keys()) + filter_list = list(column_width.keys()) + # create the header lines output = '' key_list = [] for key in filter_list: width = column_width[key] - key = key.replace('!', '') + key = key.replace('!', '').replace('_', ' ') + + if capitalize: + key = key.capitalize() + key_list.append(key.ljust(width)) + output += ' | '.join(key_list) + '\n' output += '-' * len(output) + '\n' @@ -82,6 +101,20 @@ class FormattedOutput: return output + @classmethod + def as_columns(cls, entries: List[str], cols: int) -> str: + chunks = [] + output = '' + + for i in range(0, len(entries), cols): + chunks.append(entries[i:i + cols]) + + for row in chunks: + out_fmt = '{: <30} ' * len(row) + output += out_fmt.format(*row) + '\n' + + return output + class Journald: @staticmethod @@ -204,6 +237,6 @@ def log(*args :str, **kwargs :Union[str, int, Dict[str, Union[str, int]]]) -> No # 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['arguments'].get('verbose', False): + if kwargs.get('level', logging.INFO) != logging.DEBUG or storage.get('arguments', {}).get('verbose', False): sys.stdout.write(f"{string}\n") sys.stdout.flush() diff --git a/archinstall/lib/packages/__init__.py b/archinstall/lib/packages/__init__.py index e69de29b..e2aab577 100644 --- a/archinstall/lib/packages/__init__.py +++ b/archinstall/lib/packages/__init__.py @@ -0,0 +1,4 @@ +from .packages import ( + group_search, package_search, find_package, + find_packages, validate_package_list, installed_package +) diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index 0743e83b..71818ca5 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -7,7 +7,7 @@ from urllib.parse import urlencode from urllib.request import urlopen from ..exceptions import PackageError, SysCallError -from ..models.dataclasses import PackageSearch, PackageSearchResult, LocalPackage +from ..models.gen import PackageSearch, PackageSearchResult, LocalPackage from ..pacman import run_pacman BASE_URL_PKG_SEARCH = 'https://archlinux.org/packages/search/json/' @@ -113,4 +113,4 @@ def installed_package(package :str) -> LocalPackage: except SysCallError: pass - return LocalPackage({field.name: package_info.get(field.name) for field in dataclasses.fields(LocalPackage)}) + return LocalPackage({field.name: package_info.get(field.name) for field in dataclasses.fields(LocalPackage)}) # type: ignore diff --git a/archinstall/lib/pacman.py b/archinstall/lib/pacman.py index 9c427aff..0dfd5afa 100644 --- a/archinstall/lib/pacman.py +++ b/archinstall/lib/pacman.py @@ -1,10 +1,14 @@ import logging import pathlib import time +from typing import TYPE_CHECKING, Any from .general import SysCommand from .output import log +if TYPE_CHECKING: + _: Any + def run_pacman(args :str, default_cmd :str = 'pacman') -> SysCommand: """ diff --git a/archinstall/lib/profile/__init__.py b/archinstall/lib/profile/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/archinstall/lib/profile/profile_menu.py b/archinstall/lib/profile/profile_menu.py new file mode 100644 index 00000000..6462685a --- /dev/null +++ b/archinstall/lib/profile/profile_menu.py @@ -0,0 +1,203 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Optional, Dict + +from archinstall.default_profiles.profile import Profile, GreeterType +from .profile_model import ProfileConfiguration +from ..hardware import AVAILABLE_GFX_DRIVERS +from ..menu import Menu, MenuSelectionType, AbstractSubMenu, Selector +from ..user_interaction.system_conf import select_driver + +if TYPE_CHECKING: + _: Any + + +class ProfileMenu(AbstractSubMenu): + def __init__( + self, + data_store: Dict[str, Any], + preset: Optional[ProfileConfiguration] = None + ): + if preset: + self._preset = preset + else: + self._preset = ProfileConfiguration() + + super().__init__(data_store=data_store) + + def setup_selection_menu_options(self): + self._menu_options['profile'] = Selector( + _('Profile'), + lambda x: self._select_profile(x), + display_func=lambda x: x.name if x else None, + preview_func=self._preview_profile, + default=self._preset.profile, + enabled=True + ) + + self._menu_options['gfx_driver'] = Selector( + _('Graphics driver'), + lambda preset: self._select_gfx_driver(preset), + display_func=lambda x: x if x else None, + dependencies=['profile'], + default=self._preset.gfx_driver if self._preset.profile and self._preset.profile.is_graphic_driver_supported() else None, + enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False + ) + + self._menu_options['greeter'] = Selector( + _('Greeter'), + lambda preset: select_greeter(self._menu_options['profile'].current_selection, preset), + display_func=lambda x: x.value if x else None, + dependencies=['profile'], + default=self._preset.greeter if self._preset.profile and self._preset.profile.is_greeter_supported() else None, + enabled=self._preset.profile.is_greeter_supported() if self._preset.profile else False + ) + + def run(self, allow_reset: bool = True) -> Optional[ProfileConfiguration]: + super().run(allow_reset=allow_reset) + + if self._data_store.get('profile', None): + return ProfileConfiguration( + self._menu_options['profile'].current_selection, + self._menu_options['gfx_driver'].current_selection, + self._menu_options['greeter'].current_selection + ) + + return None + + 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) + self._menu_options['gfx_driver'].set_current_selection(None) + else: + self._menu_options['gfx_driver'].set_enabled(True) + self._menu_options['gfx_driver'].set_current_selection('All open-source (default)') + + if not profile.is_greeter_supported(): + self._menu_options['greeter'].set_enabled(False) + self._menu_options['greeter'].set_current_selection(None) + else: + self._menu_options['greeter'].set_enabled(True) + self._menu_options['greeter'].set_current_selection(profile.default_greeter_type) + else: + self._menu_options['gfx_driver'].set_current_selection(None) + self._menu_options['greeter'].set_current_selection(None) + + return profile + + def _select_gfx_driver(self, preset: Optional[str] = None) -> Optional[str]: + driver = preset + profile: Optional[Profile] = self._menu_options['profile'].current_selection + + if profile: + if profile.is_graphic_driver_supported(): + driver = select_driver(current_value=preset) + + if driver and 'Sway' in profile.current_selection_names(): + packages = AVAILABLE_GFX_DRIVERS[driver] + + if packages and "nvidia" in packages: + prompt = str( + _('The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?')) + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run() + + if choice.value == Menu.no(): + return None + + return driver + + def _preview_profile(self) -> Optional[str]: + profile: Optional[Profile] = self._menu_options['profile'].current_selection + + if profile: + names = profile.current_selection_names() + return '\n'.join(names) + + return None + + +def select_greeter( + profile: Optional[Profile] = None, + preset: Optional[GreeterType] = None +) -> Optional[GreeterType]: + if not profile or profile.is_greeter_supported(): + title = str(_('Please chose which greeter to install')) + greeter_options = [greeter.value for greeter in GreeterType] + + default: Optional[GreeterType] = None + + if preset is not None: + default = preset + elif profile is not None: + default_greeter = profile.default_greeter_type + default = default_greeter if default_greeter else None + + choice = Menu( + title, + greeter_options, + skip=True, + default_option=default.value if default else None + ).run() + + match choice.type_: + case MenuSelectionType.Skip: + return default + + return GreeterType(choice.single_value) + + return None + + +def select_profile( + current_profile: Optional[Profile] = None, + title: Optional[str] = None, + allow_reset: bool = True, + multi: bool = False +) -> Optional[Profile]: + from archinstall.lib.profile.profiles_handler import profile_handler + top_level_profiles = profile_handler.get_top_level_profiles() + + display_title = title + if not display_title: + display_title = str(_('This is a list of pre-programmed default_profiles')) + + choice = profile_handler.select_profile( + top_level_profiles, + current_profile=current_profile, + title=display_title, + allow_reset=allow_reset, + multi=multi + ) + + match choice.type_: + case MenuSelectionType.Selection: + profile_selection: Profile = choice.single_value + select_result = profile_selection.do_on_select() + + if not select_result: + return select_profile( + current_profile=current_profile, + title=title, + allow_reset=allow_reset, + multi=multi + ) + + # we're going to reset the currently selected profile(s) to avoid + # any stale data laying around + match select_result: + case select_result.NewSelection: + profile_handler.reset_top_level_profiles(exclude=[profile_selection]) + current_profile = profile_selection + case select_result.ResetCurrent: + profile_handler.reset_top_level_profiles() + current_profile = None + case select_result.SameSelection: + pass + + return current_profile + case MenuSelectionType.Reset: + return None + case MenuSelectionType.Skip: + return current_profile diff --git a/archinstall/lib/profile/profile_model.py b/archinstall/lib/profile/profile_model.py new file mode 100644 index 00000000..ad3015ae --- /dev/null +++ b/archinstall/lib/profile/profile_model.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Optional, Dict + +from archinstall.default_profiles.profile import Profile, GreeterType + +if TYPE_CHECKING: + _: Any + + +@dataclass +class ProfileConfiguration: + profile: Optional[Profile] = None + gfx_driver: Optional[str] = None + greeter: Optional[GreeterType] = None + + def json(self) -> Dict[str, Any]: + from .profiles_handler import profile_handler + return { + 'profile': profile_handler.to_json(self.profile), + 'gfx_driver': self.gfx_driver, + 'greeter': self.greeter.value if self.greeter else None + } + + @classmethod + def parse_arg(cls, arg: Dict[str, Any]) -> 'ProfileConfiguration': + from .profiles_handler import profile_handler + greeter = arg.get('greeter', None) + + return ProfileConfiguration( + profile_handler.parse_profile_config(arg['profile']), + arg.get('gfx_driver', None), + GreeterType(greeter) if greeter else None + ) diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py new file mode 100644 index 00000000..063b12ea --- /dev/null +++ b/archinstall/lib/profile/profiles_handler.py @@ -0,0 +1,391 @@ +from __future__ import annotations + +import importlib.util +import logging +import sys +from collections import Counter +from functools import cached_property +from pathlib import Path +from tempfile import NamedTemporaryFile +from types import ModuleType +from typing import List, TYPE_CHECKING, Any, Optional, Dict, Union + +from archinstall.default_profiles.profile import Profile, TProfile, GreeterType +from .profile_model import ProfileConfiguration +from ..hardware import AVAILABLE_GFX_DRIVERS +from ..menu import MenuSelectionType, Menu, MenuSelection +from ..networking import list_interfaces, fetch_data_from_url +from ..output import log +from ..storage import storage + +if TYPE_CHECKING: + from ..installer import Installer + _: Any + + +class ProfileHandler: + def __init__(self): + self._profiles_path: Path = storage['PROFILE'] + self._profiles = None + + # special variable to keep track of a profile url configuration + # it is merely used to be able to export the path again when a user + # wants to save the configuration + self._url_path = None + + def to_json(self, profile: Optional[Profile]) -> Dict[str, Any]: + """ + Serialize the selected profile setting to JSON + """ + data: Dict[str, Any] = {} + + if profile is not None: + data = { + 'main': profile.name, + 'details': [profile.name for profile in profile.current_selection], + } + + if self._url_path is not None: + data['path'] = self._url_path + + return data + + def parse_profile_config(self, profile_config: Dict[str, Any]) -> Optional[Profile]: + """ + Deserialize JSON configuration + """ + profile = None + + # the order of these is important, we want to + # load all the default_profiles from url and custom + # so that we can then apply whatever was specified + # in the main/detail sections + if url_path := profile_config.get('path', None): + self._url_path = url_path + local_path = Path(url_path) + + if local_path.is_file(): + profiles = self._process_profile_file(local_path) + self.remove_custom_profiles(profiles) + self.add_custom_profiles(profiles) + 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 main := profile_config.get('main', None): + 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] + invalid = ', '.join([k for k, v in resolved.items() if v is None]) + + if invalid: + log(f'No profile definition found: {invalid}') + + if profile is not None: + profile.set_current_selection(valid) + + return profile + + @property + def profiles(self) -> List[Profile]: + """ + List of all available default_profiles + """ + if self._profiles is None: + self._profiles = self._find_available_profiles() + return self._profiles + + @cached_property + def _local_mac_addresses(self) -> List[str]: + ifaces = list_interfaces() + return list(ifaces.keys()) + + def add_custom_profiles(self, profiles: Union[TProfile, List[TProfile]]): + if not isinstance(profiles, list): + profiles = [profiles] + + for profile in profiles: + self._profiles.append(profile) + + self._verify_unique_profile_names(self._profiles) + + def remove_custom_profiles(self, profiles: Union[TProfile, List[TProfile]]): + if not isinstance(profiles, list): + profiles = [profiles] + + remove_names = [p.name for p in profiles] + self._profiles = [p for p in self._profiles if p.name not in remove_names] + + def get_profile_by_name(self, name: str) -> Optional[Profile]: + return next(filter(lambda x: x.name == name, self.profiles), None) # type: ignore + + def get_top_level_profiles(self) -> List[Profile]: + return list(filter(lambda x: x.is_top_level_profile(), self.profiles)) + + def get_server_profiles(self) -> List[Profile]: + return list(filter(lambda x: x.is_server_type_profile(), self.profiles)) + + def get_desktop_profiles(self) -> List[Profile]: + return list(filter(lambda x: x.is_desktop_type_profile(), self.profiles)) + + def get_custom_profiles(self) -> List[Profile]: + return list(filter(lambda x: x.is_custom_type_profile(), self.profiles)) + + def get_mac_addr_profiles(self) -> List[Profile]: + tailored = list(filter(lambda x: x.is_tailored(), self.profiles)) + match_mac_addr_profiles = list(filter(lambda x: x.name in self._local_mac_addresses, tailored)) + return match_mac_addr_profiles + + def install_greeter(self, install_session: 'Installer', greeter: GreeterType): + packages = [] + service = None + + match greeter: + case GreeterType.Lightdm: + packages = ['lightdm', 'lightdm-gtk-greeter'] + service = ['lightdm'] + case GreeterType.Sddm: + packages = ['sddm'] + service = ['sddm'] + case GreeterType.Gdm: + packages = ['gdm'] + service = ['gdm'] + + if packages: + install_session.add_additional_packages(packages) + if service: + install_session.enable_service(service) + + def install_gfx_driver(self, install_session: 'Installer', driver: str): + try: + driver_pkgs = AVAILABLE_GFX_DRIVERS[driver] if driver else [] + additional_pkg = ' '.join(['xorg-server', 'xorg-xinit'] + driver_pkgs) + + if driver is not None: + if 'nvidia' in driver: + if "linux-zen" in install_session.base_packages or "linux-lts" in install_session.base_packages: + for kernel in install_session.kernels: + # Fixes https://github.com/archlinux/archinstall/issues/585 + install_session.add_additional_packages(f"{kernel}-headers") + + # I've had kernel regen fail if it wasn't installed before nvidia-dkms + install_session.add_additional_packages("dkms xorg-server xorg-xinit nvidia-dkms") + return + elif 'amdgpu' in driver_pkgs: + # The order of these two are important if amdgpu is installed #808 + if 'amdgpu' in install_session.MODULES: + install_session.MODULES.remove('amdgpu') + install_session.MODULES.append('amdgpu') + + if 'radeon' in install_session.MODULES: + install_session.MODULES.remove('radeon') + install_session.MODULES.append('radeon') + + install_session.add_additional_packages(additional_pkg) + except Exception as err: + log(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}", level=logging.WARNING, fg="yellow") + # Prep didn't run, so there's no driver to install + install_session.add_additional_packages("xorg-server xorg-xinit") + + def install_profile_config(self, install_session: 'Installer', profile_config: ProfileConfiguration): + profile = profile_config.profile + + if profile: + profile.install(install_session) + + if profile and profile_config.gfx_driver: + if profile.is_xorg_type_profile() or profile.is_desktop_type_profile(): + self.install_gfx_driver(install_session, profile_config.gfx_driver) + + if profile and profile_config.greeter: + self.install_greeter(install_session, profile_config.greeter) + + def _import_profile_from_url(self, url: str): + """ + Import default_profiles from a url path + """ + try: + data = fetch_data_from_url(url) + b_data = bytes(data, 'utf-8') + + with NamedTemporaryFile(delete=False, suffix='.py') as fp: + fp.write(b_data) + filepath = Path(fp.name) + + profiles = self._process_profile_file(filepath) + self.remove_custom_profiles(profiles) + 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") + + def _load_profile_class(self, module: ModuleType) -> List[Profile]: + """ + Load all default_profiles defined in a module + """ + profiles = [] + for k, v in module.__dict__.items(): + if isinstance(v, type) and v.__module__ == module.__name__: + try: + cls_ = v() + 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) + + return profiles + + def _verify_unique_profile_names(self, profiles: List[Profile]): + """ + All profile names have to be unique, this function will verify + that the provided list contains only default_profiles with unique names + """ + counter = Counter([p.name for p in profiles]) + duplicates = list(filter(lambda x: x[1] != 1, counter.items())) + + 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") + sys.exit(1) + + def _is_legacy(self, file: Path) -> bool: + """ + Check if the provided profile file contains a + legacy profile definition + """ + with open(file, 'r') as fp: + for line in fp.readlines(): + if '__packages__' in line: + return True + return False + + def _process_profile_file(self, file: Path) -> List[Profile]: + """ + 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') + return [] + + if not file.is_file(): + log(f'Cannot find profile file {file}') + return [] + + name = file.name.removesuffix(file.suffix) + log(f'Importing profile: {file}', level=logging.DEBUG) + + try: + spec = importlib.util.spec_from_file_location(name, file) + if spec is not None: + imported = importlib.util.module_from_spec(spec) + if spec.loader is not None: + 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) + + return [] + + def _find_available_profiles(self) -> List[Profile]: + """ + Search the profile path for profile definitions + """ + profiles = [] + for file in self._profiles_path.glob('**/*.py'): + # ignore the abstract default_profiles class + if 'profile.py' in file.name: + continue + profiles += self._process_profile_file(file) + + self._verify_unique_profile_names(profiles) + return profiles + + def reset_top_level_profiles(self, exclude: List[Profile] = []): + """ + Reset all top level profile configurations, this is usually necessary + when a new top level profile is selected + """ + excluded_profiles = [p.name for p in exclude] + for profile in self.get_top_level_profiles(): + if profile.name not in excluded_profiles: + profile.reset() + + def select_profile( + self, + selectable_profiles: List[Profile], + current_profile: Optional[Union[TProfile, List[TProfile]]] = None, + title: str = '', + allow_reset: bool = True, + multi: bool = False, + ) -> MenuSelection: + """ + Helper function to perform a profile selection + """ + options = {p.name: p for p in selectable_profiles} + + warning = str(_('Are you sure you want to reset this setting?')) + + preset_value: Optional[Union[str, List[str]]] = None + if current_profile is not None: + if isinstance(current_profile, list): + preset_value = [p.name for p in current_profile] + else: + preset_value = current_profile.name + + choice = Menu( + title=title, + preset_values=preset_value, + p_options=options, + allow_reset=allow_reset, + allow_reset_warning_msg=warning, + multi=multi, + sort=True, + preview_command=self.preview_text, + preview_size=0.5 + ).run() + + if choice.type_ == MenuSelectionType.Selection: + value = choice.value + if multi: + # this is quite dirty and should eb switched to a + # dedicated return type instead + choice.value = [options[val] for val in value] # type: ignore + else: + choice.value = options[value] # type: ignore + + return choice + + def preview_text(self, selection: str) -> Optional[str]: + """ + Callback for preview display on profile selection + """ + profile = self.get_profile_by_name(selection) + return profile.preview_text() if profile is not None else None + + +profile_handler = ProfileHandler() diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py deleted file mode 100644 index a4fbe490..00000000 --- a/archinstall/lib/profiles.py +++ /dev/null @@ -1,340 +0,0 @@ -from __future__ import annotations -import hashlib -import importlib.util -import json -import os -import re -import ssl -import sys -import urllib.error -import urllib.parse -import urllib.request -from typing import Optional, Dict, Union, TYPE_CHECKING, Any -from types import ModuleType -# https://stackoverflow.com/a/39757388/929999 -if TYPE_CHECKING: - from .installer import Installer - _: Any - -from .general import multisplit -from .networking import list_interfaces -from .storage import storage -from .exceptions import ProfileNotFound - - -def grab_url_data(path :str) -> str: - safe_path = path[: path.find(':') + 1] + ''.join([item if item in ('/', '?', '=', '&') else urllib.parse.quote(item) for item in multisplit(path[path.find(':') + 1:], ('/', '?', '=', '&'))]) - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - response = urllib.request.urlopen(safe_path, context=ssl_context) - return response.read() # bytes? - - -def is_desktop_profile(profile :str) -> bool: - if str(profile) == 'Profile(desktop)': - return True - - desktop_profile = Profile(None, "desktop") - with open(desktop_profile.path, 'r') as source: - source_data = source.read() - - if '__name__' in source_data and '__supported__' in source_data: - with desktop_profile.load_instructions(namespace=f"{desktop_profile.namespace}.py") as imported: - if hasattr(imported, '__supported__'): - desktop_profiles = imported.__supported__ - return str(profile) in [f"Profile({s})" for s in desktop_profiles] - - return False - - -def list_profiles( - filter_irrelevant_macs :bool = True, - subpath :str = '', - filter_top_level_profiles :bool = False -) -> Dict[str, Dict[str, Union[str, bool]]]: - # TODO: Grab from github page as well, not just local static files - - if filter_irrelevant_macs: - local_macs = list_interfaces() - - cache = {} - # Grab all local profiles found in PROFILE_PATH - for PATH_ITEM in storage['PROFILE_PATH']: - for root, folders, files in os.walk(os.path.abspath(os.path.expanduser(PATH_ITEM + subpath))): - for file in files: - if file == '__init__.py': - continue - if os.path.splitext(file)[1] == '.py': - tailored = False - if len(mac := re.findall('(([a-zA-z0-9]{2}[-:]){5}([a-zA-z0-9]{2}))', file)): - if filter_irrelevant_macs and mac[0][0].lower() not in local_macs: - continue - tailored = True - - description = '' - with open(os.path.join(root, file), 'r') as fh: - first_line = fh.readline() - if len(first_line) and first_line[0] == '#': - description = first_line[1:].strip() - - cache[file[:-3]] = {'path': os.path.join(root, file), 'description': description, 'tailored': tailored} - break - - # Grab profiles from upstream URL - if storage['PROFILE_DB']: - profiles_url = os.path.join(storage["UPSTREAM_URL"] + subpath, storage['PROFILE_DB']) - try: - profile_list = json.loads(grab_url_data(profiles_url)) - except urllib.error.HTTPError as err: - print(_('Error: Listing profiles on URL "{}" resulted in:').format(profiles_url), err) - return cache - except json.decoder.JSONDecodeError as err: - print(_('Error: Could not decode "{}" result as JSON:').format(profiles_url), err) - return cache - - for profile in profile_list: - if os.path.splitext(profile)[1] == '.py': - tailored = False - if len(mac := re.findall('(([a-zA-z0-9]{2}[-:]){5}([a-zA-z0-9]{2}))', profile)): - if filter_irrelevant_macs and mac[0][0].lower() not in local_macs: - continue - tailored = True - - cache[profile[:-3]] = {'path': os.path.join(storage["UPSTREAM_URL"] + subpath, profile), 'description': profile_list[profile], 'tailored': tailored} - - if filter_top_level_profiles: - for profile in list(cache.keys()): - if Profile(None, profile).is_top_level_profile() is False: - del cache[profile] - - return cache - - -class Script: - def __init__(self, profile :str, installer :Optional[Installer] = None): - """ - :param profile: A string representing either a boundled profile, a local python file - or a remote path (URL) to a python script-profile. Three examples: - * profile: https://archlinux.org/some_profile.py - * profile: desktop - * profile: /path/to/profile.py - """ - self.profile = profile - self.installer = installer # TODO: Appears not to be used anymore? - self.converted_path = None - self.spec = None - self.examples = {} - self.namespace = os.path.splitext(os.path.basename(self.path))[0] - self.original_namespace = self.namespace - - def __enter__(self, *args :str, **kwargs :str) -> ModuleType: - self.execute() - return sys.modules[self.namespace] - - def __exit__(self, *args :str, **kwargs :str) -> None: - # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager - if len(args) >= 2 and args[1]: - raise args[1] - - if self.original_namespace: - self.namespace = self.original_namespace - - def localize_path(self, profile_path :str) -> str: - if (url := urllib.parse.urlparse(profile_path)).scheme and url.scheme in ('https', 'http'): - if not self.converted_path: - self.converted_path = f"/tmp/{os.path.basename(self.profile).replace('.py', '')}_{hashlib.md5(os.urandom(12)).hexdigest()}.py" - - with open(self.converted_path, "w") as temp_file: - temp_file.write(urllib.request.urlopen(url.geturl()).read().decode('utf-8')) - - return self.converted_path - else: - return profile_path - - @property - def path(self) -> str: - parsed_url = urllib.parse.urlparse(self.profile) - - # The Profile was not a direct match on a remote URL - if not parsed_url.scheme: - # Try to locate all local or known URL's - if not self.examples: - self.examples = list_profiles() - - if f"{self.profile}" in self.examples: - return self.localize_path(self.examples[self.profile]['path']) - # TODO: Redundant, the below block shouldn't be needed as profiles are stripped of their .py, but just in case for now: - elif f"{self.profile}.py" in self.examples: - return self.localize_path(self.examples[f"{self.profile}.py"]['path']) - - # Path was not found in any known examples, check if it's an absolute path - if os.path.isfile(self.profile): - return self.profile - - raise ProfileNotFound(f"File {self.profile} does not exist in {storage['PROFILE_PATH']}") - elif parsed_url.scheme in ('https', 'http'): - return self.localize_path(self.profile) - else: - raise ProfileNotFound(f"Cannot handle scheme {parsed_url.scheme}") - - def load_instructions(self, namespace :Optional[str] = None) -> 'Script': - if namespace: - self.namespace = namespace - - self.spec = importlib.util.spec_from_file_location(self.namespace, self.path) - imported = importlib.util.module_from_spec(self.spec) - sys.modules[self.namespace] = imported - - return self - - def execute(self) -> ModuleType: - if self.namespace not in sys.modules or self.spec is None: - self.load_instructions() - - self.spec.loader.exec_module(sys.modules[self.namespace]) - - return sys.modules[self.namespace] - - -class Profile(Script): - def __init__(self, installer :Optional[Installer], path :str): - super(Profile, self).__init__(path, installer) - - def __dump__(self, *args :str, **kwargs :str) -> Dict[str, str]: - return {'path': self.path} - - def __repr__(self, *args :str, **kwargs :str) -> str: - return f'Profile({os.path.basename(self.profile)})' - - @property - def name(self) -> str: - return os.path.basename(self.profile) - - @property - def is_desktop_profile(self) -> bool: - return is_desktop_profile(repr(self)) - - def install(self) -> ModuleType: - # Before installing, revert any temporary changes to the namespace. - # This ensures that the namespace during installation is the original initiation namespace. - # (For instance awesome instead of aweosme.py or app-awesome.py) - self.namespace = self.original_namespace - return self.execute() - - def has_prep_function(self) -> bool: - with open(self.path, 'r') as source: - source_data = source.read() - - # Some crude safety checks, make sure the imported profile has - # a __name__ check and if so, check if it's got a _prep_function() - # we can call to ask for more user input. - # - # If the requirements are met, import with .py in the namespace to not - # trigger a traditional: - # if __name__ == 'moduleName' - if '__name__' in source_data and '_prep_function' in source_data: - with self.load_instructions(namespace=f"{self.namespace}.py") as imported: - if hasattr(imported, '_prep_function'): - return True - return False - - def has_post_install(self) -> bool: - with open(self.path, 'r') as source: - source_data = source.read() - - # Some crude safety checks, make sure the imported profile has - # a __name__ check and if so, check if it's got a _prep_function() - # we can call to ask for more user input. - # - # If the requirements are met, import with .py in the namespace to not - # trigger a traditional: - # if __name__ == 'moduleName' - if '__name__' in source_data and '_post_install' in source_data: - with self.load_instructions(namespace=f"{self.namespace}.py") as imported: - if hasattr(imported, '_post_install'): - return True - - def is_top_level_profile(self) -> bool: - with open(self.path, 'r') as source: - source_data = source.read() - - if '__name__' in source_data and 'is_top_level_profile' in source_data: - with self.load_instructions(namespace=f"{self.namespace}.py") as imported: - if hasattr(imported, 'is_top_level_profile'): - return imported.is_top_level_profile - - # Default to True if nothing is specified, - # since developers like less code - omitting it should assume they want to present it. - return True - - def get_profile_description(self) -> str: - with open(self.path, 'r') as source: - source_data = source.read() - - if '__description__' in source_data: - with self.load_instructions(namespace=f"{self.namespace}.py") as imported: - if hasattr(imported, '__description__'): - return imported.__description__ - - # Default to this string if the profile does not have a description. - return "This profile does not have the __description__ attribute set." - - @property - def packages(self) -> Optional[list]: - """ - Returns a list of packages baked into the profile definition. - If no package definition has been done, .packages() will return None. - """ - with open(self.path, 'r') as source: - source_data = source.read() - - # Some crude safety checks, make sure the imported profile has - # a __name__ check before importing. - # - # If the requirements are met, import with .py in the namespace to not - # trigger a traditional: - # if __name__ == 'moduleName' - if '__name__' in source_data and '__packages__' in source_data: - with self.load_instructions(namespace=f"{self.namespace}.py") as imported: - if hasattr(imported, '__packages__'): - return imported.__packages__ - return None - - -class Application(Profile): - def __repr__(self, *args :str, **kwargs :str): - return f'Application({os.path.basename(self.profile)})' - - @property - def path(self) -> str: - parsed_url = urllib.parse.urlparse(self.profile) - - # The Profile was not a direct match on a remote URL - if not parsed_url.scheme: - # Try to locate all local or known URL's - if not self.examples: - self.examples = list_profiles(subpath='/applications') - - if f"{self.profile}" in self.examples: - return self.localize_path(self.examples[self.profile]['path']) - # TODO: Redundant, the below block shouldn't be needed as profiles are stripped of their .py, but just in case for now: - elif f"{self.profile}.py" in self.examples: - return self.localize_path(self.examples[f"{self.profile}.py"]['path']) - - # Path was not found in any known examples, check if it's an absolute path - if os.path.isfile(self.profile): - return os.path.basename(self.profile) - - raise ProfileNotFound(f"Application file {self.profile} does not exist in {storage['PROFILE_PATH']}") - elif parsed_url.scheme in ('https', 'http'): - return self.localize_path(self.profile) - else: - raise ProfileNotFound(f"Application cannot handle scheme {parsed_url.scheme}") - - def install(self) -> ModuleType: - # Before installing, revert any temporary changes to the namespace. - # This ensures that the namespace during installation is the original initiation namespace. - # (For instance awesome instead of aweosme.py or app-awesome.py) - self.namespace = self.original_namespace - return self.execute() diff --git a/archinstall/lib/storage.py b/archinstall/lib/storage.py index 8c358161..5a54d816 100644 --- a/archinstall/lib/storage.py +++ b/archinstall/lib/storage.py @@ -1,26 +1,19 @@ -import os - # There's a few scenarios of execution: -# 1. In the git repository, where ./profiles/ exist +# 1. In the git repository, where ./profiles_bck/ exist # 2. When executing from a remote directory, but targeted a script that starts from the git repository -# 3. When executing as a python -m archinstall module where profiles exist one step back for library reasons. +# 3. When executing as a python -m archinstall module where profiles_bck exist one step back for library reasons. # (4. Added the ~/.config directory as an additional option for future reasons) # # And Keeping this in dict ensures that variables are shared across imports. from typing import Any, Dict +from pathlib import Path + storage: Dict[str, Any] = { - 'PROFILE_PATH': [ - './profiles', - '~/.config/archinstall/profiles', - os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'profiles'), - # os.path.abspath(f'{os.path.dirname(__file__)}/../examples') - ], - 'UPSTREAM_URL': 'https://raw.githubusercontent.com/archlinux/archinstall/master/profiles', - 'PROFILE_DB': None, # Used in cases when listing profiles is desired, not mandatory for direct profile grabbing. + 'PROFILE': Path(__file__).parent.parent.joinpath('default_profiles'), 'LOG_PATH': '/var/log/archinstall', 'LOG_FILE': 'install.log', - 'MOUNT_POINT': '/mnt/archinstall', + 'MOUNT_POINT': Path('/mnt/archinstall'), 'ENC_IDENTIFIER': 'ainst', 'DISK_TIMEOUTS' : 1, # seconds 'DISK_RETRY_ATTEMPTS' : 5, # RETRY_ATTEMPTS * DISK_TIMEOUTS is used in disk operations diff --git a/archinstall/lib/udev/__init__.py b/archinstall/lib/udev/__init__.py deleted file mode 100644 index 86c8cc29..00000000 --- a/archinstall/lib/udev/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .udevadm import udevadm_info \ No newline at end of file diff --git a/archinstall/lib/udev/udevadm.py b/archinstall/lib/udev/udevadm.py deleted file mode 100644 index 84ec9cfd..00000000 --- a/archinstall/lib/udev/udevadm.py +++ /dev/null @@ -1,17 +0,0 @@ -import typing -import pathlib -from ..general import SysCommand - -def udevadm_info(path :pathlib.Path) -> typing.Dict[str, str]: - if path.resolve().exists() is False: - return {} - - result = SysCommand(f"udevadm info {path.resolve()}") - data = {} - for line in result: - if b': ' in line and b'=' in line: - _, obj = line.split(b': ', 1) - key, value = obj.split(b'=', 1) - data[key.decode('UTF-8').lower()] = value.decode('UTF-8').strip() - - return data \ No newline at end of file diff --git a/archinstall/lib/user_interaction/__init__.py b/archinstall/lib/user_interaction/__init__.py index 2bc46759..5ee89de0 100644 --- a/archinstall/lib/user_interaction/__init__.py +++ b/archinstall/lib/user_interaction/__init__.py @@ -1,12 +1,10 @@ -from .save_conf import save_config from .manage_users_conf import ask_for_additional_users -from .backwards_compatible_conf import generic_select, generic_multi_select from .locale_conf import select_locale_lang, select_locale_enc -from .system_conf import select_kernel, select_harddrives, select_driver, ask_for_bootloader, ask_for_swap +from .system_conf import select_kernel, select_driver, ask_for_bootloader, ask_for_swap from .network_conf import ask_to_configure_network -from .partitioning_conf import select_partition -from .general_conf import (ask_ntp, ask_for_a_timezone, ask_for_audio_selection, select_language, select_mirror_regions, - select_profile, select_archinstall_language, ask_additional_packages_to_install, - select_additional_repositories, ask_hostname, add_number_of_parrallel_downloads) -from .disk_conf import ask_for_main_filesystem_format, select_individual_blockdevice_usage, select_disk_layout, select_disk -from .utils import get_password, do_countdown +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/backwards_compatible_conf.py b/archinstall/lib/user_interaction/backwards_compatible_conf.py deleted file mode 100644 index 296572d2..00000000 --- a/archinstall/lib/user_interaction/backwards_compatible_conf.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import annotations - -import logging -import sys -from collections.abc import Iterable -from typing import Any, Union, TYPE_CHECKING - -from ..exceptions import RequirementError -from ..menu import Menu -from ..output import log - -if TYPE_CHECKING: - _: Any - - -def generic_select( - p_options: Union[list, dict], - input_text: str = '', - allow_empty_input: bool = True, - options_output: bool = True, # function not available - sort: bool = False, - multi: bool = False, - default: Any = None) -> Any: - """ - A generic select function that does not output anything - other than the options and their indexes. As an example: - - generic_select(["first", "second", "third option"]) - > first - second - third option - When the user has entered the option correctly, - this function returns an item from list, a string, or None - - Options can be any iterable. - Duplicate entries are not checked, but the results with them are unreliable. Which element to choose from the duplicates depends on the return of the index() - Default value if not on the list of options will be added as the first element - sort will be handled by Menu() - """ - # 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 generic_select", fg="red") - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>", level=logging.WARNING) - raise RequirementError("generic_select() requires an iterable as option.") - - input_text = input_text if input_text else _('Select one of the values shown below: ') - - if isinstance(p_options, dict): - options = list(p_options.values()) - else: - options = list(p_options) - # check that the default value is in the list. If not it will become the first entry - if default and default not in options: - options.insert(0, default) - - # one of the drawbacks of the new interface is that in only allows string like options, so we do a conversion - # also for the default value if it exists - soptions = list(map(str, options)) - default_value = options[options.index(default)] if default else None - - selected_option = Menu(input_text, - soptions, - skip=allow_empty_input, - multi=multi, - default_option=default_value, - sort=sort).run() - # we return the original objects, not the strings. - # options is the list with the original objects and soptions the list with the string values - # thru the map, we get from the value selected in soptions it index, and thu it the original object - if not selected_option: - return selected_option - elif isinstance(selected_option, list): # for multi True - selected_option = list(map(lambda x: options[soptions.index(x)], selected_option)) - else: # for multi False - selected_option = options[soptions.index(selected_option)] - return selected_option - - -def generic_multi_select(p_options: Union[list, dict], - text: str = '', - sort: bool = False, - default: Any = None, - allow_empty: bool = False) -> Any: - - text = text if text else _("Select one or more of the options below: ") - - return generic_select(p_options, - input_text=text, - allow_empty_input=allow_empty, - sort=sort, - multi=True, - default=default) diff --git a/archinstall/lib/user_interaction/disk_conf.py b/archinstall/lib/user_interaction/disk_conf.py index 554d13ef..a77e950a 100644 --- a/archinstall/lib/user_interaction/disk_conf.py +++ b/archinstall/lib/user_interaction/disk_conf.py @@ -1,86 +1,391 @@ from __future__ import annotations -from typing import Any, Dict, TYPE_CHECKING, Optional +import logging +from pathlib import Path +from typing import Any, TYPE_CHECKING, Optional, List, Tuple -from .partitioning_conf import manage_new_and_existing_partitions, get_default_partition_layout -from ..disk import BlockDevice -from ..exceptions import DiskError -from ..menu import Menu -from ..menu.menu import MenuSelectionType +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 ask_for_main_filesystem_format(advanced_options=False) -> str: - options = {'btrfs': 'btrfs', 'ext4': 'ext4', 'xfs': 'xfs', 'f2fs': 'f2fs'} +def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]: + """ + Asks the user to select one or multiple devices - advanced = {'ntfs': 'ntfs'} + :return: List of selected devices + :rtype: list + """ - if advanced_options: - options.update(advanced) + 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 - prompt = _('Select which filesystem your main partition should use') - choice = Menu(prompt, options, skip=False).run() - return choice.value + 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 select_individual_blockdevice_usage(block_devices: list) -> Dict[str, Any]: - result = {} +def get_default_partition_layout( + devices: List[disk.BDevice], + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_option: bool = False +) -> List[disk.DeviceModification]: - for device in block_devices: - layout = manage_new_and_existing_partitions(device) - result[device.path] = layout + 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 + ) - return result +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) -def select_disk_layout(preset: Optional[Dict[str, Any]], block_devices: list, advanced_options=False) -> Optional[Dict[str, Any]]: - wipe_mode = str(_('Wipe all selected drives and use a best-effort default partition layout')) - custome_mode = str(_('Select what to do with each individual drive (followed by partition usage)')) - modes = [wipe_mode, custome_mode] + 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 what you wish to do with the selected block devices'), - modes, + _('Select a partitioning option'), + options, allow_reset=True, - allow_reset_warning_msg=warning + 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.value == wipe_mode: - return get_default_partition_layout(block_devices, advanced_options) + 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: - return select_individual_blockdevice_usage(block_devices) + using_home_partition = False + # root partition + start = disk.Size(513, disk.Unit.MiB) if has_uefi() else disk.Size(206, disk.Unit.MiB) -def select_disk(dict_o_disks: Dict[str, BlockDevice]) -> Optional[BlockDevice]: - """ - Asks the user to select a harddrive from the `dict_o_disks` selection. - Usually this is combined with :ref:`archinstall.list_drives`. + # 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) - :param dict_o_disks: A `dict` where keys are the drive-name, value should be a dict containing drive information. - :type dict_o_disks: dict + 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) - :return: The name/path (the dictionary key) of the selected drive - :rtype: str - """ - drives = sorted(list(dict_o_disks.keys())) - if len(drives) >= 1: - title = str(_('You can skip selecting a drive and partitioning and use whatever drive-setup is mounted at /mnt (experimental)')) + '\n' - title += str(_('Select one of the disks or skip and use /mnt as default')) + 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) - choice = Menu(title, drives).run() + # add boot partition to the root device + boot_partition = _boot_partition() + root_device_modification.add_partition(boot_partition) - if choice.type_ == MenuSelectionType.Skip: - return None + # 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) - drive = dict_o_disks[choice.value] - return drive + # 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) - raise DiskError('select_disk() requires a non-empty dictionary of disks to select from.') + 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 index fc7ded45..7a6bb358 100644 --- a/archinstall/lib/user_interaction/general_conf.py +++ b/archinstall/lib/user_interaction/general_conf.py @@ -3,15 +3,13 @@ from __future__ import annotations import logging import pathlib from typing import List, Any, Optional, Dict, TYPE_CHECKING +from typing import Union from ..locale_helpers import list_keyboard_languages, list_timezones -from ..menu import Menu -from ..menu.menu import MenuSelectionType -from ..menu.text_input import TextInput +from ..menu import MenuSelectionType, Menu, TextInput from ..mirrors import list_mirrors from ..output import log from ..packages.packages import validate_package_list -from ..profiles import Profile, list_profiles from ..storage import storage from ..translationhandler import Language @@ -32,9 +30,10 @@ def ask_ntp(preset: bool = True) -> bool: def ask_hostname(preset: str = None) -> str: - hostname = TextInput(_('Desired hostname for the installation: '), preset).run().strip(' ') - return hostname - + while True: + hostname = TextInput(_('Desired hostname for the installation: '), preset).run().strip() + if hostname: + return hostname def ask_for_a_timezone(preset: str = None) -> str: timezones = list_timezones() @@ -52,7 +51,7 @@ def ask_for_a_timezone(preset: str = None) -> str: case MenuSelectionType.Selection: return choice.value -def ask_for_audio_selection(desktop: bool = True, preset: str = None) -> str: +def ask_for_audio_selection(desktop: bool = True, preset: Union[str, None] = None) -> Union[str, None]: no_audio = str(_('No audio server')) choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] default = 'pipewire' if desktop else no_audio @@ -140,50 +139,6 @@ def select_archinstall_language(languages: List[Language], preset_value: Languag return options[choice.value] -def select_profile(preset) -> Optional[Profile]: - """ - # Asks the user to select a profile from the available profiles. - # - # :return: The name/dictionary key of the selected profile - # :rtype: str - # """ - top_level_profiles = sorted(list(list_profiles(filter_top_level_profiles=True))) - options = {} - - for profile in top_level_profiles: - profile = Profile(None, profile) - description = profile.get_profile_description() - - option = f'{profile.profile}: {description}' - options[option] = profile - - title = _('This is a list of pre-programmed profiles, they might make it easier to install things like desktop environments') - warning = str(_('Are you sure you want to reset this setting?')) - - selection = Menu( - title=title, - p_options=list(options.keys()), - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match selection.type_: - case MenuSelectionType.Selection: - return options[selection.value] if selection.value is not None else None - case MenuSelectionType.Reset: - storage['profile_minimal'] = False - storage['_selected_servers'] = [] - storage['_desktop_profile'] = None - storage['sway_sys_priv_ctrl'] = None - storage['arguments']['sway_sys_priv_ctrl'] = None - storage['arguments']['desktop-environment'] = None - storage['arguments']['gfx_driver'] = None - storage['arguments']['gfx_driver_packages'] = None - return None - case MenuSelectionType.Skip: - return None - - 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.')) diff --git a/archinstall/lib/user_interaction/locale_conf.py b/archinstall/lib/user_interaction/locale_conf.py index bbbe070b..88aec64e 100644 --- a/archinstall/lib/user_interaction/locale_conf.py +++ b/archinstall/lib/user_interaction/locale_conf.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import Any, TYPE_CHECKING from ..locale_helpers import list_locales -from ..menu import Menu -from ..menu.menu import MenuSelectionType +from ..menu import Menu, MenuSelectionType if TYPE_CHECKING: _: Any diff --git a/archinstall/lib/user_interaction/manage_users_conf.py b/archinstall/lib/user_interaction/manage_users_conf.py index 84ce3556..879578da 100644 --- a/archinstall/lib/user_interaction/manage_users_conf.py +++ b/archinstall/lib/user_interaction/manage_users_conf.py @@ -4,8 +4,7 @@ import re from typing import Any, Dict, TYPE_CHECKING, List, Optional from .utils import get_password -from ..menu import Menu -from ..menu.list_manager import ListManager +from ..menu import Menu, ListManager from ..models.users import User from ..output import FormattedOutput @@ -27,14 +26,14 @@ class UserList(ListManager): ] super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) - def reformat(self, data: List[User]) -> Dict[str, User]: + 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 = {f' {rows[0]}': None, f' {rows[1]}': None} + 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('|', '\\|') @@ -53,16 +52,16 @@ class UserList(ListManager): # 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]: # change password + 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]: # promote/demote + 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]: # delete + elif action == self._actions[3] and entry: # delete data = [d for d in data if d != entry] return data @@ -80,16 +79,20 @@ class UserList(ListManager): if not username: return None if not self._check_for_correct_username(username): - prompt = str(_("The username you entered is invalid. Try again")) + '\n' + prompt + 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.no(), + default_option=Menu.yes(), clear_screen=False, show_search_hint=False ).run() diff --git a/archinstall/lib/user_interaction/network_conf.py b/archinstall/lib/user_interaction/network_conf.py index 5e637f23..b682c1d2 100644 --- a/archinstall/lib/user_interaction/network_conf.py +++ b/archinstall/lib/user_interaction/network_conf.py @@ -4,14 +4,12 @@ import ipaddress import logging from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict -from ..menu.menu import MenuSelectionType -from ..menu.text_input import TextInput +from ..menu import MenuSelectionType, TextInput from ..models.network_configuration import NetworkConfiguration, NicType from ..networking import list_interfaces -from ..menu import Menu from ..output import log, FormattedOutput -from ..menu.list_manager import ListManager +from ..menu import ListManager, Menu if TYPE_CHECKING: _: Any diff --git a/archinstall/lib/user_interaction/partitioning_conf.py b/archinstall/lib/user_interaction/partitioning_conf.py deleted file mode 100644 index 0a5ede51..00000000 --- a/archinstall/lib/user_interaction/partitioning_conf.py +++ /dev/null @@ -1,362 +0,0 @@ -from __future__ import annotations - -import copy -from typing import List, Any, Dict, Union, TYPE_CHECKING, Callable, Optional - -from ..menu import Menu -from ..menu.menu import MenuSelectionType -from ..output import log, FormattedOutput - -from ..disk.validators import fs_types - -if TYPE_CHECKING: - from ..disk import BlockDevice - from ..disk.partition import Partition - _: Any - - -def partition_overlap(partitions: list, start: str, end: str) -> bool: - # TODO: Implement sanity check - return False - - -def current_partition_layout(partitions: List[Dict[str, Any]], with_idx: bool = False, with_title: bool = True) -> str: - - def do_padding(name: str, max_len: int): - spaces = abs(len(str(name)) - max_len) + 2 - pad_left = int(spaces / 2) - pad_right = spaces - pad_left - return f'{pad_right * " "}{name}{pad_left * " "}|' - - def flatten_data(data: Dict[str, Any]) -> Dict[str, Any]: - flattened = {} - for k, v in data.items(): - if k == 'filesystem': - flat = flatten_data(v) - flattened.update(flat) - elif k == 'btrfs': - # we're going to create a separate table for the btrfs subvolumes - pass - else: - flattened[k] = v - return flattened - - display_data: List[Dict[str, Any]] = [flatten_data(entry) for entry in partitions] - - column_names = {} - - # this will add an initial index to the table for each partition - if with_idx: - column_names['index'] = max([len(str(len(display_data))), len('index')]) - - # determine all attribute names and the max length - # of the value among all display_data to know the width - # of the table cells - for p in display_data: - for attribute, value in p.items(): - if attribute in column_names.keys(): - column_names[attribute] = max([column_names[attribute], len(str(value)), len(attribute)]) - else: - column_names[attribute] = max([len(str(value)), len(attribute)]) - - current_layout = '' - for name, max_len in column_names.items(): - current_layout += do_padding(name, max_len) - - current_layout = f'{current_layout[:-1]}\n{"-" * len(current_layout)}\n' - - for idx, p in enumerate(display_data): - row = '' - for name, max_len in column_names.items(): - if name == 'index': - row += do_padding(str(idx), max_len) - elif name in p: - row += do_padding(p[name], max_len) - else: - row += ' ' * (max_len + 2) + '|' - - current_layout += f'{row[:-1]}\n' - - # we'll create a separate table for the btrfs subvolumes - btrfs_subvolumes = [partition['btrfs']['subvolumes'] for partition in partitions if partition.get('btrfs', None)] - if len(btrfs_subvolumes) > 0: - for subvolumes in btrfs_subvolumes: - output = FormattedOutput.as_table(subvolumes) - current_layout += f'\n{output}' - - if with_title: - title = str(_('Current partition layout')) - return f'\n\n{title}:\n\n{current_layout}' - - return current_layout - - -def _get_partitions(partitions :List[Partition], filter_ :Callable = None) -> List[str]: - """ - filter allows to filter out the indexes once they are set. Should return True if element is to be included - """ - partition_indexes = [] - for i in range(len(partitions)): - if filter_: - if filter_(partitions[i]): - partition_indexes.append(str(i)) - else: - partition_indexes.append(str(i)) - - return partition_indexes - - -def select_partition( - title :str, - partitions :List[Partition], - multiple :bool = False, - filter_ :Callable = None -) -> Optional[int, List[int]]: - partition_indexes = _get_partitions(partitions, filter_) - - if len(partition_indexes) == 0: - return None - - choice = Menu(title, partition_indexes, multi=multiple).run() - - if choice.type_ == MenuSelectionType.Skip: - return None - - if isinstance(choice.value, list): - return [int(p) for p in choice.value] - else: - return int(choice.value) - - -def get_default_partition_layout( - block_devices: Union['BlockDevice', List['BlockDevice']], - advanced_options: bool = False -) -> Optional[Dict[str, Any]]: - from ..disk import suggest_single_disk_layout, suggest_multi_disk_layout - - if len(block_devices) == 1: - return suggest_single_disk_layout(block_devices[0], advanced_options=advanced_options) - else: - return suggest_multi_disk_layout(block_devices, advanced_options=advanced_options) - - -def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, Any]: # noqa: max-complexity: 50 - block_device_struct = {"partitions": [partition.__dump__() for partition in block_device.partitions.values()]} - original_layout = copy.deepcopy(block_device_struct) - - new_partition = str(_('Create a new partition')) - suggest_partition_layout = str(_('Suggest partition layout')) - delete_partition = str(_('Delete a partition')) - delete_all_partitions = str(_('Clear/Delete all partitions')) - assign_mount_point = str(_('Assign mount-point for a partition')) - mark_formatted = str(_('Mark/Unmark a partition to be formatted (wipes data)')) - mark_compressed = str(_('Mark/Unmark a partition as compressed (btrfs only)')) - mark_bootable = str(_('Mark/Unmark a partition as bootable (automatic for /boot)')) - set_filesystem_partition = str(_('Set desired filesystem for a partition')) - set_btrfs_subvolumes = str(_('Set desired subvolumes on a btrfs partition')) - save_and_exit = str(_('Save and exit')) - cancel = str(_('Cancel')) - - while True: - modes = [new_partition, suggest_partition_layout] - - if len(block_device_struct['partitions']) > 0: - modes += [ - delete_partition, - delete_all_partitions, - assign_mount_point, - mark_formatted, - mark_bootable, - mark_compressed, - set_filesystem_partition, - ] - - indexes = _get_partitions( - block_device_struct["partitions"], - filter_=lambda x: True if x.get('filesystem', {}).get('format') == 'btrfs' else False - ) - - if len(indexes) > 0: - modes += [set_btrfs_subvolumes] - - title = _('Select what to do with\n{}').format(block_device) - - # show current partition layout: - if len(block_device_struct["partitions"]): - title += current_partition_layout(block_device_struct['partitions']) + '\n' - - modes += [save_and_exit, cancel] - - task = Menu(title, modes, sort=False, skip=False).run() - task = task.value - - if task == cancel: - return original_layout - elif task == save_and_exit: - break - - if task == new_partition: - from ..disk import valid_parted_position - - # if partition_type == 'gpt': - # # https://www.gnu.org/software/parted/manual/html_node/mkpart.html - # # https://www.gnu.org/software/parted/manual/html_node/mklabel.html - # name = input("Enter a desired name for the partition: ").strip() - - fs_choice = Menu(_('Enter a desired filesystem type for the partition'), fs_types()).run() - - if fs_choice.type_ == MenuSelectionType.Skip: - continue - - prompt = str(_('Enter the start location (in parted units: s, GB, %, etc. ; default: {}): ')).format( - block_device.first_free_sector - ) - start = input(prompt).strip() - - if not start.strip(): - start = block_device.first_free_sector - end_suggested = block_device.first_end_sector - else: - end_suggested = '100%' - - prompt = str(_('Enter the end location (in parted units: s, GB, %, etc. ; ex: {}): ')).format( - end_suggested - ) - end = input(prompt).strip() - - if not end.strip(): - end = end_suggested - - if valid_parted_position(start) and valid_parted_position(end): - if partition_overlap(block_device_struct["partitions"], start, end): - log(f"This partition overlaps with other partitions on the drive! Ignoring this partition creation.", - fg="red") - continue - - block_device_struct["partitions"].append({ - "type": "primary", # Strictly only allowed under MS-DOS, but GPT accepts it so it's "safe" to inject - "start": start, - "size": end, - "mountpoint": None, - "wipe": True, - "filesystem": { - "format": fs_choice.value - } - }) - else: - log(f"Invalid start ({valid_parted_position(start)}) or end ({valid_parted_position(end)}) for this partition. Ignoring this partition creation.", - fg="red") - continue - elif task == suggest_partition_layout: - from ..disk import suggest_single_disk_layout - - if len(block_device_struct["partitions"]): - prompt = _('{}\ncontains queued partitions, this will remove those, are you sure?').format(block_device) - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run() - - if choice.value == Menu.no(): - continue - - block_device_struct.update(suggest_single_disk_layout(block_device)[block_device.path]) - else: - current_layout = current_partition_layout(block_device_struct['partitions'], with_idx=True) - - if task == delete_partition: - title = _('{}\n\nSelect by index which partitions to delete').format(current_layout) - to_delete = select_partition(title, block_device_struct["partitions"], multiple=True) - - if to_delete: - block_device_struct['partitions'] = [ - p for idx, p in enumerate(block_device_struct['partitions']) if idx not in to_delete - ] - elif task == mark_compressed: - title = _('{}\n\nSelect which partition to mark as bootable').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - if "filesystem" not in block_device_struct["partitions"][partition]: - block_device_struct["partitions"][partition]["filesystem"] = {} - if "mount_options" not in block_device_struct["partitions"][partition]["filesystem"]: - block_device_struct["partitions"][partition]["filesystem"]["mount_options"] = [] - - if "compress=zstd" not in block_device_struct["partitions"][partition]["filesystem"]["mount_options"]: - block_device_struct["partitions"][partition]["filesystem"]["mount_options"].append("compress=zstd") - elif task == delete_all_partitions: - block_device_struct["partitions"] = [] - block_device_struct["wipe"] = True - elif task == assign_mount_point: - title = _('{}\n\nSelect by index which partition to mount where').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - print(_(' * Partition mount-points are relative to inside the installation, the boot would be /boot as an example.')) - mountpoint = input(_('Select where to mount partition (leave blank to remove mountpoint): ')).strip() - - if len(mountpoint): - block_device_struct["partitions"][partition]['mountpoint'] = mountpoint - if mountpoint == '/boot': - log(f"Marked partition as bootable because mountpoint was set to /boot.", fg="yellow") - block_device_struct["partitions"][partition]['boot'] = True - else: - del (block_device_struct["partitions"][partition]['mountpoint']) - - elif task == mark_formatted: - title = _('{}\n\nSelect which partition to mask for formatting').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - # If we mark a partition for formatting, but the format is CRYPTO LUKS, there's no point in formatting it really - # without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set, - # it's safe to change the filesystem for this partition. - if block_device_struct["partitions"][partition].get('filesystem',{}).get('format', 'crypto_LUKS') == 'crypto_LUKS': - if not block_device_struct["partitions"][partition].get('filesystem', None): - block_device_struct["partitions"][partition]['filesystem'] = {} - - fs_choice = Menu(_('Enter a desired filesystem type for the partition'), fs_types()).run() - - if fs_choice.type_ == MenuSelectionType.Selection: - block_device_struct["partitions"][partition]['filesystem']['format'] = fs_choice.value - - # Negate the current wipe marking - block_device_struct["partitions"][partition]['wipe'] = not block_device_struct["partitions"][partition].get('wipe', False) - - elif task == mark_bootable: - title = _('{}\n\nSelect which partition to mark as bootable').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - block_device_struct["partitions"][partition]['boot'] = \ - not block_device_struct["partitions"][partition].get('boot', False) - - elif task == set_filesystem_partition: - title = _('{}\n\nSelect which partition to set a filesystem on').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - if not block_device_struct["partitions"][partition].get('filesystem', None): - block_device_struct["partitions"][partition]['filesystem'] = {} - - fstype_title = _('Enter a desired filesystem type for the partition: ') - fs_choice = Menu(fstype_title, fs_types()).run() - - if fs_choice.type_ == MenuSelectionType.Selection: - block_device_struct["partitions"][partition]['filesystem']['format'] = fs_choice.value - - elif task == set_btrfs_subvolumes: - from .subvolume_config import SubvolumeList - - # TODO get preexisting partitions - title = _('{}\n\nSelect which partition to set subvolumes on').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"],filter_=lambda x:True if x.get('filesystem',{}).get('format') == 'btrfs' else False) - - if partition is not None: - if not block_device_struct["partitions"][partition].get('btrfs', {}): - block_device_struct["partitions"][partition]['btrfs'] = {} - if not block_device_struct["partitions"][partition]['btrfs'].get('subvolumes', []): - block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = [] - - prev = block_device_struct["partitions"][partition]['btrfs']['subvolumes'] - result = SubvolumeList(_("Manage btrfs subvolumes for current partition"), prev).run() - block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = result - - return block_device_struct diff --git a/archinstall/lib/user_interaction/save_conf.py b/archinstall/lib/user_interaction/save_conf.py index 5b4ae2b3..e05b9afe 100644 --- a/archinstall/lib/user_interaction/save_conf.py +++ b/archinstall/lib/user_interaction/save_conf.py @@ -5,38 +5,30 @@ import logging from pathlib import Path from typing import Any, Dict, TYPE_CHECKING -from ..configuration import ConfigurationOutput 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: - json_config = config_output.user_config_to_json() - return f'{config_output.user_configuration_file}\n{json_config}' + serialized = config_output.user_config_to_json() + return f'{config_output.user_configuration_file}\n{serialized}' elif options['user_creds'] == selection: - if json_config := config_output.user_credentials_to_json(): - return f'{config_output.user_credentials_file}\n{json_config}' - else: - return str(_('No configuration')) - elif options['disk_layout'] == selection: - if json_config := config_output.disk_layout_to_json(): - return f'{config_output.disk_layout_file}\n{json_config}' + 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 json_config := config_output.user_credentials_to_json(): + if config_output.user_credentials_to_json(): output += f'{config_output.user_credentials_file}\n' - if json_config := config_output.disk_layout_to_json(): - output += f'{config_output.disk_layout_file}\n' return output[:-1] return None @@ -61,6 +53,9 @@ def save_config(config: Dict): 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', @@ -76,19 +71,19 @@ def save_config(config: Dict): '/usr', '/var', ] - log( - _('When picking a directory to save configuration files to,' - ' by default we will ignore the following folders: ') + ','.join(dirs_to_exclude), - level=logging.DEBUG - ) + 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' - possible_save_dirs = list( - filter(None, SysCommand(file_picker_command).decode().split('\x00')) - ) + + 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'), @@ -101,35 +96,18 @@ def save_config(config: Dict): match selection.type_: case MenuSelectionType.Skip: return - case _: - save_dirs = selection.value - - prompt = _('Do you want to save {} configuration file(s) in the following locations?\n\n{}').format( - list(options.keys())[list(options.values()).index(choice.value)], - save_dirs - ) - save_confirmation = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run() - if save_confirmation == Menu.no(): - return - - log( - _('Saving {} configuration files to {}').format( - list(options.keys())[list(options.values()).index(choice.value)], - save_dirs - ), - level=logging.DEBUG - ) - + + 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'] == choice.value: + if options['user_config'] == save_config_value: config_output.save_user_config(save_dir) - elif options['user_creds'] == choice.value: + elif options['user_creds'] == save_config_value: config_output.save_user_creds(save_dir) - elif options['disk_layout'] == choice.value: - config_output.save_disk_layout(save_dir) - elif options['all'] == choice.value: + elif options['all'] == save_config_value: config_output.save_user_config(save_dir) config_output.save_user_creds(save_dir) - config_output.save_disk_layout(save_dir) diff --git a/archinstall/lib/user_interaction/subvolume_config.py b/archinstall/lib/user_interaction/subvolume_config.py deleted file mode 100644 index 94150dee..00000000 --- a/archinstall/lib/user_interaction/subvolume_config.py +++ /dev/null @@ -1,98 +0,0 @@ -from typing import Dict, List, Optional, Any, TYPE_CHECKING - -from ..menu.list_manager import ListManager -from ..menu.menu import MenuSelectionType -from ..menu.text_input import TextInput -from ..menu import Menu -from ..models.subvolume import Subvolume -from ... import FormattedOutput - -if TYPE_CHECKING: - _: Any - - -class SubvolumeList(ListManager): - def __init__(self, prompt: str, subvolumes: List[Subvolume]): - self._actions = [ - str(_('Add subvolume')), - str(_('Edit subvolume')), - str(_('Delete subvolume')) - ] - super().__init__(prompt, subvolumes, [self._actions[0]], self._actions[1:]) - - def reformat(self, data: List[Subvolume]) -> Dict[str, Optional[Subvolume]]: - 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[Subvolume]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, subvol in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = subvol - - return display_data - - def selected_action_display(self, subvolume: Subvolume) -> str: - return subvolume.name - - def _prompt_options(self, editing: Optional[Subvolume] = None) -> List[str]: - preset_options = [] - if editing: - preset_options = editing.options - - choice = Menu( - str(_("Select the desired subvolume options ")), - ['nodatacow','compress'], - skip=True, - preset_values=preset_options, - multi=True - ).run() - - if choice.type_ == MenuSelectionType.Selection: - return choice.value # type: ignore - - return [] - - def _add_subvolume(self, editing: Optional[Subvolume] = None) -> Optional[Subvolume]: - name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run() - - if not name: - return None - - mountpoint = TextInput(f'\n{_("Subvolume mountpoint")}: ', editing.mountpoint if editing else '').run() - - if not mountpoint: - return None - - options = self._prompt_options(editing) - - subvolume = Subvolume(name, mountpoint) - subvolume.compress = 'compress' in options - subvolume.nodatacow = 'nodatacow' in options - - return subvolume - - def handle_action(self, action: str, entry: Optional[Subvolume], data: List[Subvolume]) -> List[Subvolume]: - if action == self._actions[0]: # add - new_subvolume = self._add_subvolume() - - if new_subvolume is not None: - # in case a user with the same username as an existing user - # was created we'll replace the existing one - data = [d for d in data if d.name != new_subvolume.name] - data += [new_subvolume] - elif entry is not None: - if action == self._actions[1]: # edit subvolume - new_subvolume = self._add_subvolume(entry) - - if new_subvolume is not None: - # we'll remove the original subvolume and add the modified version - data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name] - data += [new_subvolume] - elif action == self._actions[2]: # delete - data = [d for d in data if d != entry] - - return data diff --git a/archinstall/lib/user_interaction/system_conf.py b/archinstall/lib/user_interaction/system_conf.py index e1581677..3f57d0e7 100644 --- a/archinstall/lib/user_interaction/system_conf.py +++ b/archinstall/lib/user_interaction/system_conf.py @@ -1,19 +1,16 @@ from __future__ import annotations -from typing import List, Any, Dict, TYPE_CHECKING +from typing import List, Any, Dict, TYPE_CHECKING, Optional -from ..disk import all_blockdevices -from ..exceptions import RequirementError from ..hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics -from ..menu import Menu -from ..menu.menu import MenuSelectionType -from ..storage import storage +from ..menu import MenuSelectionType, Menu +from ..models.bootloader import Bootloader if TYPE_CHECKING: _: Any -def select_kernel(preset: List[str] = None) -> List[str]: +def select_kernel(preset: List[str] = []) -> List[str]: """ Asks the user to select a kernel for system. @@ -39,39 +36,36 @@ def select_kernel(preset: List[str] = None) -> List[str]: match choice.type_: case MenuSelectionType.Skip: return preset case MenuSelectionType.Reset: return [] - case MenuSelectionType.Selection: return choice.value + case MenuSelectionType.Selection: return choice.value # type: ignore -def select_harddrives(preset: List[str] = []) -> List[str]: - """ - Asks the user to select one or multiple hard drives - - :return: List of selected hard drives - :rtype: list - """ - hard_drives = all_blockdevices(partitions=False).values() - options = {f'{option}': option for option in hard_drives} - - title = str(_('Select one or more hard drives to use and configure\n')) - title += str(_('Any modifications to the existing setting will reset the disk layout!')) +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 - warning = str(_('If you reset the harddrive selection this will also reset the current disk layout. Are you sure?')) + preset_value = preset.value if preset else None - selected_harddrive = Menu( - title, - list(options.keys()), - multi=True, - allow_reset=True, - allow_reset_warning_msg=warning + choice = Menu( + _('Choose a bootloader'), + options, + preset_values=preset_value, + sort=False, + default_option=default ).run() - match selected_harddrive.type_: - case MenuSelectionType.Reset: return [] + match choice.type_: case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return [options[i] for i in selected_harddrive.value] + case MenuSelectionType.Selection: return Bootloader(choice.value) + + return preset -def select_driver(options: Dict[str, Any] = AVAILABLE_GFX_DRIVERS) -> str: +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. @@ -80,78 +74,31 @@ def select_driver(options: Dict[str, Any] = AVAILABLE_GFX_DRIVERS) -> str: there for appeal to the general public first and edge cases later) """ - drivers = sorted(list(options)) + if not options: + options = AVAILABLE_GFX_DRIVERS + + drivers = sorted(list(options.keys())) if drivers: - arguments = storage.get('arguments', {}) 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' + 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' - )) + 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(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')) - title += str(_('\n\nSelect a graphics driver or leave blank to install all open-source drivers')) - choice = Menu(title, drivers).run() + title += str(_('\nSelect a graphics driver or leave blank to install all open-source drivers')) - if choice.type_ != MenuSelectionType.Selection: - return arguments.get('gfx_driver') + preset = current_value if current_value else None + choice = Menu(title, drivers, preset_values=preset).run() - arguments['gfx_driver'] = choice.value - return options.get(choice.value) - - raise RequirementError("Selecting drivers require a least one profile to be given as an option.") + if choice.type_ != MenuSelectionType.Selection: + return None + return choice.value # type: ignore -def ask_for_bootloader(advanced_options: bool = False, preset: str = None) -> str: - if preset == 'systemd-bootctl': - preset_val = 'systemd-boot' if advanced_options else Menu.no() - elif preset == 'grub-install': - preset_val = 'grub' if advanced_options else Menu.yes() - else: - preset_val = preset - - bootloader = "systemd-bootctl" if has_uefi() else "grub-install" - - if has_uefi(): - if not advanced_options: - selection = Menu( - _('Would you like to use GRUB as a bootloader instead of systemd-boot?'), - Menu.yes_no(), - preset_values=preset_val, - default_option=Menu.no() - ).run() - - match selection.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: bootloader = 'grub-install' if selection.value == Menu.yes() else bootloader - else: - # We use the common names for the bootloader as the selection, and map it back to the expected values. - choices = ['systemd-boot', 'grub', 'efistub'] - selection = Menu(_('Choose a bootloader'), choices, preset_values=preset_val).run() - - value = '' - match selection.type_: - case MenuSelectionType.Skip: value = preset_val - case MenuSelectionType.Selection: value = selection.value - - if value != "": - if value == 'systemd-boot': - bootloader = 'systemd-bootctl' - elif value == 'grub': - bootloader = 'grub-install' - else: - bootloader = value - - return bootloader + return current_value def ask_for_swap(preset: bool = True) -> bool: @@ -166,3 +113,5 @@ def ask_for_swap(preset: bool = True) -> bool: 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 index 7ee6fc07..918945c0 100644 --- a/archinstall/lib/user_interaction/utils.py +++ b/archinstall/lib/user_interaction/utils.py @@ -1,13 +1,9 @@ from __future__ import annotations import getpass -import signal -import sys -import time from typing import Any, Optional, TYPE_CHECKING -from ..menu import Menu -from ..models.password_strength import PasswordStrength +from ..models import PasswordStrength from ..output import log if TYPE_CHECKING: @@ -36,44 +32,3 @@ def get_password(prompt: str = '') -> Optional[str]: return password return None - - -def do_countdown() -> bool: - SIG_TRIGGER = False - - def kill_handler(sig: int, frame: Any) -> None: - print() - exit(0) - - def sig_handler(sig: int, frame: Any) -> None: - global SIG_TRIGGER - SIG_TRIGGER = True - signal.signal(signal.SIGINT, kill_handler) - - original_sigint_handler = signal.getsignal(signal.SIGINT) - signal.signal(signal.SIGINT, sig_handler) - - for i in range(5, 0, -1): - print(f"{i}", end='') - - for x in range(4): - sys.stdout.flush() - time.sleep(0.25) - print(".", end='') - - if SIG_TRIGGER: - prompt = _('Do you really want to abort?') - choice = Menu(prompt, Menu.yes_no(), skip=False).run() - if choice.value == Menu.yes(): - exit(0) - - if SIG_TRIGGER is False: - sys.stdin.read() - - SIG_TRIGGER = False - signal.signal(signal.SIGINT, sig_handler) - - print() - signal.signal(signal.SIGINT, original_sigint_handler) - - return True diff --git a/archinstall/lib/utils/__init__.py b/archinstall/lib/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/archinstall/lib/utils/singleton.py b/archinstall/lib/utils/singleton.py new file mode 100644 index 00000000..55be70eb --- /dev/null +++ b/archinstall/lib/utils/singleton.py @@ -0,0 +1,15 @@ +from typing import Dict, Any + + +class _Singleton(type): + """ A metaclass that creates a Singleton base class when called. """ + _instances: Dict[Any, Any] = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + +class Singleton(_Singleton('SingletonMeta', (object,), {})): # type: ignore + pass diff --git a/archinstall/lib/utils/util.py b/archinstall/lib/utils/util.py new file mode 100644 index 00000000..ded480ae --- /dev/null +++ b/archinstall/lib/utils/util.py @@ -0,0 +1,30 @@ +from pathlib import Path +from typing import Any, TYPE_CHECKING, Optional + +from ..output import log + +if TYPE_CHECKING: + _: Any + + +def prompt_dir(text: str, header: Optional[str] = None) -> Path: + if header: + print(header) + + while True: + path = input(text).strip(' ') + 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') + + +def is_subpath(first: Path, second: Path): + """ + Check if _first_ a subpath of _second_ + """ + try: + first.relative_to(second) + return True + except ValueError: + return False diff --git a/archinstall/locales/ar/LC_MESSAGES/base.po b/archinstall/locales/ar/LC_MESSAGES/base.po index c9540b38..9c3a6bff 100644 --- a/archinstall/locales/ar/LC_MESSAGES/base.po +++ b/archinstall/locales/ar/LC_MESSAGES/base.po @@ -792,6 +792,108 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "حدِّد منطقة زمنية" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "حدِّد واجهة شبكة واحدة للإعداد" + +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "" + +msgid "Existing Partitions" +msgstr "" + +#, fuzzy +msgid "Select a partitioning option" +msgstr "حدِّد منطقة زمنية" + +msgid "Enter the root directory of the mounted devices: " +msgstr "" + +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "" + +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "" + +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "" + +msgid "Current profile selection" +msgstr "" + +msgid "Remove all newly added partitions" +msgstr "" + +msgid "Assign mountpoint" +msgstr "" + +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +msgid "Mark/Unmark as compressed" +msgstr "" + +msgid "Set subvolumes" +msgstr "" + +msgid "Delete partition" +msgstr "" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr "" + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +msgid "Total sectors: {}" +msgstr "" + +msgid "Enter the start sector (default: {}): " +msgstr "" + +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "" + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + msgid "Encryption type" msgstr "" @@ -810,17 +912,118 @@ msgstr "" msgid "Select a FIDO2 device to use for HSM" msgstr "" -msgid "All settings will be reset, are you sure?" +msgid "Use a best-effort default partition layout" msgstr "" -msgid "Back" +msgid "Manual Partitioning" +msgstr "" + +msgid "Pre-mounted configuration" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +msgid "Configuration" +msgstr "" + msgid "Password" msgstr "" -msgid "Partition encryption" +msgid "All settings will be reset, are you sure?" msgstr "" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +msgid "Installed packages" +msgstr "" + +msgid "Add profile" +msgstr "" + +msgid "Edit profile" +msgstr "" + +msgid "Delete profile" +msgstr "" + +msgid "Profile name: " +msgstr "" + +msgid "The profile name you entered is already in use. Try again" +msgstr "" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "اكتب حزمًا إضافية لتثبيتها (تُفصَل بالمسافات، اتركها فارغة للتخطي):" + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "اكتب حزمًا إضافية لتثبيتها (تُفصَل بالمسافات، اتركها فارغة للتخطي):" + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" + +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +msgid "Disk configuration" +msgstr "" + +msgid "Profiles" +msgstr "" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "حدِّد واجهة شبكة واحدة للإعداد" diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot index 651fbd58..009dc382 100644 --- a/archinstall/locales/base.pot +++ b/archinstall/locales/base.pot @@ -844,6 +844,119 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +msgid "Select an execution mode" +msgstr "" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "" +"Profiles must have unique name, but profile definitions with duplicate name " +"found: {}" +msgstr "" + +msgid "Select one or more devices to use and configure" +msgstr "" + +msgid "" +"If you reset the device selection this will also reset the current disk " +"layout. Are you sure?" +msgstr "" + +msgid "Existing Partitions" +msgstr "" + +msgid "Select a partitioning option" +msgstr "" + +msgid "Enter the root directory of the mounted devices: " +msgstr "" + +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "" + +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "" + +msgid "" +"This is a list of pre-programmed profiles_bck, they might make it easier to " +"install things like desktop environments" +msgstr "" + +msgid "Current profile selection" +msgstr "" + +msgid "Remove all newly added partitions" +msgstr "" + +msgid "Assign mountpoint" +msgstr "" + +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +msgid "Mark/Unmark as compressed" +msgstr "" + +msgid "Set subvolumes" +msgstr "" + +msgid "Delete partition" +msgstr "" + +msgid "Partition" +msgstr "" + +msgid "" +"This partition is currently encrypted, to format it a filesystem has to be " +"specified" +msgstr "" + +msgid "" +"Partition mount-points are relative to inside the installation, the boot " +"would be /boot as an example." +msgstr "" + +msgid "" +"If mountpoint /boot is set, then the partition will also be marked as " +"bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +msgid "Total sectors: {}" +msgstr "" + +msgid "Enter the start sector (default: {}): " +msgstr "" + +msgid "" +"Enter the end sector of the partition (percentage or block number, default: " +"{}): " +msgstr "" + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + msgid "Encryption type" msgstr "" @@ -862,17 +975,123 @@ msgstr "" msgid "Select a FIDO2 device to use for HSM" msgstr "" -msgid "All settings will be reset, are you sure?" +msgid "Use a best-effort default partition layout" msgstr "" -msgid "Back" +msgid "Manual Partitioning" +msgstr "" + +msgid "Pre-mounted configuration" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +msgid "Configuration" +msgstr "" + msgid "Password" msgstr "" -msgid "Partition encryption" +msgid "All settings will be reset, are you sure?" +msgstr "" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "" +"The proprietary Nvidia driver is not supported by Sway. It is likely that " +"you will run into issues, are you okay with that?" +msgstr "" + +msgid "Installed packages" +msgstr "" + +msgid "Add profile" +msgstr "" + +msgid "Edit profile" +msgstr "" + +msgid "Delete profile" +msgstr "" + +msgid "Profile name: " +msgstr "" + +msgid "The profile name you entered is already in use. Try again" +msgstr "" + +msgid "" +"Packages to be install with this profile (space separated, leave blank to " +"skip): " +msgstr "" + +msgid "" +"Services to be enabled with this profile (space separated, leave blank to " +"skip): " +msgstr "" + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" + +msgid "" +"Sway needs access to your seat (collection of hardware devices i.e. " +"keyboard, mouse, etc)" +msgstr "" + +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +msgid "Disk configuration" +msgstr "" + +msgid "Profiles" +msgstr "" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +msgid "Select directory (or directories) for saving configuration files" msgstr "" diff --git a/archinstall/locales/cs/LC_MESSAGES/base.po b/archinstall/locales/cs/LC_MESSAGES/base.po index a09c6001..76c885aa 100644 --- a/archinstall/locales/cs/LC_MESSAGES/base.po +++ b/archinstall/locales/cs/LC_MESSAGES/base.po @@ -831,6 +831,125 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Zvolte akci pro '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Zvolte jeden nebo více pevných disků k použití a konfiguraci" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Pokud resetujete výběr disku, také tím resetujete stávající rozdělení. Přejete si pokračovat?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Přidávání nového oddílu..." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Smazat oddíl" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Zadejte adresář pro uložení konfigurace (konfigurací): " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Minimální kapacita pro oddíl /home: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Minimální kapacita pro oddíl s Arch Linux: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Toto je seznam před-programovaných profilů, které by mohly usnadnit instalaci věcí jako jsou desktopová prostředí" + +#, fuzzy +msgid "Current profile selection" +msgstr "Aktuální rozdělení disku" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Vytvořit nový oddíl" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Přiřaďte přípojný bod k oddílu" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Označení/Odznačení oddílu ke zformátování (vymaže data)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Označit/Odznačit oddíl s kompresí (jen pro btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Smazat podsvazek" + +#, fuzzy +msgid "Delete partition" +msgstr "Smazat oddíl" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Přípojné body diskových oddílů jsou relativní uvnitř instalace, například spouštěcí bod by byl /boot." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Adresář není validní: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Zadejte počáteční sektor (procenta nebo číslo bloku, výchozí: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Zadejte koncový sektor oddílu (procenta nebo číslo bloku, např. {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Šifrovací heslo" @@ -852,24 +971,135 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} obsahuje oddíly ve frontě, toto je odstraní, jste si jisti?" +msgid "Use a best-effort default partition layout" +msgstr "Vymazat všechny vybrané disky a použít chytré výchozí rozdělení oddílů" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Ruční konfigurace" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Žádná konfigurace" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Žádná konfigurace" + #, fuzzy msgid "Password" msgstr "Heslo správce (root)" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} obsahuje oddíly ve frontě, toto je odstraní, jste si jisti?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Dodatečné balíčky" + +#, fuzzy +msgid "Add profile" +msgstr "Profil" + +#, fuzzy +msgid "Edit profile" +msgstr "Profil" + +#, fuzzy +msgid "Delete profile" +msgstr "Smazat rozhraní" + +#, fuzzy +msgid "Profile name: " +msgstr "Profil" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "Zadané uživatelské jméno není platné. Zkuste to znovu" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Zadejte další balíčky k instalaci (oddělené mezerou, ponechte prázdné k přeskočení): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Zadejte další balíčky k instalaci (oddělené mezerou, ponechte prázdné k přeskočení): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Zvolte ovladač grafické karty nebo ponechte prázdné k instalaci všech open-source ovladačů" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Zadejte počáteční sektor (procenta nebo číslo bloku, výchozí: {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Žádná konfigurace" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Zadejte koncový sektor oddílu (procenta nebo číslo bloku, např. {}): " +#, fuzzy +msgid "Profiles" +msgstr "Profil" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Zvolte jeden nebo více pevných disků k použití a konfiguraci" diff --git a/archinstall/locales/de/LC_MESSAGES/base.po b/archinstall/locales/de/LC_MESSAGES/base.po index ac677188..e1d01ab8 100644 --- a/archinstall/locales/de/LC_MESSAGES/base.po +++ b/archinstall/locales/de/LC_MESSAGES/base.po @@ -850,6 +850,125 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Wählen sie eine Aktion aus für '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Bitte wählen sie eine oder mehrere Laufwerke aus die konfiguriert werden sollen" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Wenn sie die Laufwerkkonfiguration ändern, dann wird die Laufwer-layout zurückgesetzt. Sind sie sicher?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Partitionen werden hinzugefügt..." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Partition löschen" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Geben sie eine Ordner an wo die Konfigurationen gespeichert werden sollen: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Minimaler Speicherplatz für /home Partition: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Minimaler Speicherplatz für Arch Linux Partition: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Dies ist eine Liste von bereits programmierten Profilen, diese ermöglichen es einfacher Desktop Umgebungen einzustellen" + +#, fuzzy +msgid "Current profile selection" +msgstr "Momentanes Partitionslayout" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Neue Partition erstellen" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Mountort für Partition angeben" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Markieren welche Partition formattiert werden soll (alle Daten werden gelöscht)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Markieren/Unmarkieren Kompression von einer Partition (nur Btrfs) " + +#, fuzzy +msgid "Set subvolumes" +msgstr "Benutzerkonto löschen" + +#, fuzzy +msgid "Delete partition" +msgstr "Partition löschen" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Die Mountorte sind relativ zur Installation, zum Beispiel boot würde gemountet auf /boot" + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Ordner existiert nicht: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Bitte geben Sie den start Sektor ein (in Prozent oder Blocknummer, default: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Bitte geben Sie den end Sektor ein (in Prozent oder Blocknummer, default: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Verschlüsselungspasswort angeben" @@ -872,22 +991,138 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} enthält Partitionen in der Warteschlange, dies werden damit entfernt, sind sie sicher?" +msgid "Use a best-effort default partition layout" +msgstr "Alle Laufwerke löschen und ein vorgegebenes Partitionenlayout verwenden" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Manuelle konfiguration" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Keine Konfiguration" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Keine Konfiguration" + #, fuzzy msgid "Password" msgstr "Root Passwort" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} enthält Partitionen in der Warteschlange, dies werden damit entfernt, sind sie sicher?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" msgstr "" +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Zus. Packete" + +#, fuzzy +msgid "Add profile" +msgstr "Profile" + +#, fuzzy +msgid "Edit profile" +msgstr "Profile" + +#, fuzzy +msgid "Delete profile" +msgstr "Verbindung löschen" + +#, fuzzy +msgid "Profile name: " +msgstr "Profile" + +msgid "The profile name you entered is already in use. Try again" +msgstr "" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Schreiben Sie zusätzliche Packete die installiert werden sollen mit einem Leerzeichen getrennt (zum Überspringen leer lassen): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Schreiben Sie zusätzliche Packete die installiert werden sollen mit einem Leerzeichen getrennt (zum Überspringen leer lassen): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Bitte wählen sie einen Grafiktreiber aus oder leer lassen um alle open-source Treiber zu installieren" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" + +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Keine Konfiguration" + +#, fuzzy +msgid "Profiles" +msgstr "Profile" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Bitte wählen sie eine oder mehrere Laufwerke aus die konfiguriert werden sollen" + #~ msgid "Add :" #~ msgstr "Hinzufügen :" diff --git a/archinstall/locales/el/LC_MESSAGES/base.po b/archinstall/locales/el/LC_MESSAGES/base.po index efcd6b49..2d2d04c7 100644 --- a/archinstall/locales/el/LC_MESSAGES/base.po +++ b/archinstall/locales/el/LC_MESSAGES/base.po @@ -838,6 +838,125 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Επιλέξτε μία ενέργεια για '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Επιλέξτε έναν ή περισσότερους σκληρούς δίσκους προς χρήση και διαμόρφωση" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Αν επαναφέρετε την επιλογή σκληρού δίσκου αυτό επίσης θα επαναφέρει την τρέχουσα διάταξη δίσκου. Είστε σίγουρη/ος;" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Προσθέτωντας τη διαμέριση...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Διαγραφή διαμέρισης" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Εισάγετε έναν φάκελο για την αποθήκευση της/ων διαμόρφωση/ων: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Ελάχιστη χωρητικότητα για τη διαμέριση /home: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Ελάχιστη χωρητικότητα για τη διαμέριση Arch Linux: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Αυτή είναι μία λίστα με προ-προγραμματισμένα προφίλ, που μπορεί να κάνουν την εγκατάσταση πραγμάτων όπως περιβάλλοντα επιφάνειας εργασίας πιο εύκολη" + +#, fuzzy +msgid "Current profile selection" +msgstr "Τρέχουσα διάταξη διαμέρισης" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Δημιουργία καινούργιας διαμέρισης" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Εκχώρηση σημείου mount για μία διαμέριση" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Σημείωση/Ξεμαρκάρισμα διαμέρισης προς μορφοποίηση (διαγράφει τα δεδομένα)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Σημείωση/Ξεμαρκάρισμα μίας διαμέρισως ως συμπιεσμένη (μόνο για btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Διαγραφή υποόγκου" + +#, fuzzy +msgid "Delete partition" +msgstr "Διαγραφή διαμέρισης" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Τα σημεία mount της διαμέρισης είναι σχετικά ως προς το εσωτερικό της εγκατάστασης, για παράδειγμα το boot θα ήταν /boot." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Μη έγκυρος φάκελος: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Εισάγετε τον start sector (ποσοστό ή αριθμό block, προκαθορισμένο {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Εισάγετε τον end sector της διαμέρισης (ποσοστό ή αριθμό block, πχ: {}) " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Κωδικός κρυπτογράφησης" @@ -859,24 +978,135 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} περιέχει διαμερίσεις στην ουρά, αυτό θα τις διαγράψει, είστε σίγουρη/ος;" +msgid "Use a best-effort default partition layout" +msgstr "Διαγραφή όλων των επιλεγμένων δίσκων και χρήση μίας προκαθορισμένης διάταξης βέλτιστης προσπάθειας" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Χειροκίνητη διαμόρφωση" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Καμία διαμόρφωση" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Καμία διαμόρφωση" + #, fuzzy msgid "Password" msgstr "Κωδικός root" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} περιέχει διαμερίσεις στην ουρά, αυτό θα τις διαγράψει, είστε σίγουρη/ος;" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Περαιτέρω πακέτα" + +#, fuzzy +msgid "Add profile" +msgstr "Προφίλ" + +#, fuzzy +msgid "Edit profile" +msgstr "Προφίλ" + +#, fuzzy +msgid "Delete profile" +msgstr "Διαγραφή διεπαφής" + +#, fuzzy +msgid "Profile name: " +msgstr "Προφίλ" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "Το όνομα χρήστη που εισάγατε δεν είναι έγκυρο. Προσπαθήστε ξανά" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Γράψτε περαιτέρω πακέτα προς εγκατάσταση (χωρισμένα με κενό, αφήστε άδειο για να παραληφθεί): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Γράψτε περαιτέρω πακέτα προς εγκατάσταση (χωρισμένα με κενό, αφήστε άδειο για να παραληφθεί): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Επιλέξτε έναν οδηγώ γραφικών ή αφήστε άδειο για να εγκατασταθούν όλοι οι οδηγοί ανοιχτής πηγής" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Εισάγετε τον start sector (ποσοστό ή αριθμό block, προκαθορισμένο {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Καμία διαμόρφωση" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Εισάγετε τον end sector της διαμέρισης (ποσοστό ή αριθμό block, πχ: {}) " +#, fuzzy +msgid "Profiles" +msgstr "Προφίλ" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Επιλέξτε έναν ή περισσότερους σκληρούς δίσκους προς χρήση και διαμόρφωση" diff --git a/archinstall/locales/en/LC_MESSAGES/base.po b/archinstall/locales/en/LC_MESSAGES/base.po index f1722ef9..c95cc951 100644 --- a/archinstall/locales/en/LC_MESSAGES/base.po +++ b/archinstall/locales/en/LC_MESSAGES/base.po @@ -788,6 +788,105 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +msgid "Select an execution mode" +msgstr "" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +msgid "Select one or more devices to use and configure" +msgstr "" + +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "" + +msgid "Existing Partitions" +msgstr "" + +msgid "Select a partitioning option" +msgstr "" + +msgid "Enter the root directory of the mounted devices: " +msgstr "" + +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "" + +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "" + +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "" + +msgid "Current profile selection" +msgstr "" + +msgid "Remove all newly added partitions" +msgstr "" + +msgid "Assign mountpoint" +msgstr "" + +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +msgid "Mark/Unmark as compressed" +msgstr "" + +msgid "Set subvolumes" +msgstr "" + +msgid "Delete partition" +msgstr "" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr "" + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +msgid "Total sectors: {}" +msgstr "" + +msgid "Enter the start sector (default: {}): " +msgstr "" + +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "" + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + msgid "Encryption type" msgstr "" @@ -806,17 +905,115 @@ msgstr "" msgid "Select a FIDO2 device to use for HSM" msgstr "" -msgid "All settings will be reset, are you sure?" +msgid "Use a best-effort default partition layout" msgstr "" -msgid "Back" +msgid "Manual Partitioning" +msgstr "" + +msgid "Pre-mounted configuration" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +msgid "Configuration" +msgstr "" + msgid "Password" msgstr "" -msgid "Partition encryption" +msgid "All settings will be reset, are you sure?" +msgstr "" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +msgid "Installed packages" +msgstr "" + +msgid "Add profile" +msgstr "" + +msgid "Edit profile" +msgstr "" + +msgid "Delete profile" +msgstr "" + +msgid "Profile name: " +msgstr "" + +msgid "The profile name you entered is already in use. Try again" +msgstr "" + +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "" + +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "" + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" + +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +msgid "Disk configuration" +msgstr "" + +msgid "Profiles" +msgstr "" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +msgid "Select directory (or directories) for saving configuration files" msgstr "" diff --git a/archinstall/locales/es/LC_MESSAGES/base.po b/archinstall/locales/es/LC_MESSAGES/base.po index 20101de0..1599e7f4 100644 --- a/archinstall/locales/es/LC_MESSAGES/base.po +++ b/archinstall/locales/es/LC_MESSAGES/base.po @@ -836,6 +836,125 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Seleccione una acción para '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Seleccione uno o más discos duros para usar y configurar" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Si restablece la selección del disco duro, esto también restablecerá el diseño actual del disco. ¿Está seguro?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Añadiendo partición..." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Eliminar una partición" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Introduzca un directorio para guardar la(s) configuración(es): " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Capacidad mínima para la partición /home: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Capacidad mínima para la partición Arch Linux: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Esta es una lista de perfiles pre-programados, pueden facilitar la instalación de aplicaciones como entornos de escritorio" + +#, fuzzy +msgid "Current profile selection" +msgstr "Distribución actual de las particiones" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Crear una nueva partición" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Asignar punto de montaje para una partición" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Marcar/Desmarcar una partición para ser formateada (borra los datos)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Marcar/Desmarcar una partición como comprimida (solo btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Eliminar usuario" + +#, fuzzy +msgid "Delete partition" +msgstr "Eliminar una partición" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Los puntos de montaje de partición son relativos a la instalación, el arranque sería /boot como ejemplo." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "No es un directorio válido: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Introduzca el sector de inicio (porcentaje o número de bloque, predeterminado: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Ingrese el sector final de la partición (porcentaje o número de bloque, ej: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Contraseña de cifrado" @@ -858,27 +977,138 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} contiene particiones en cola, esto eliminará esas particiones, ¿estás seguro?" +msgid "Use a best-effort default partition layout" +msgstr "Borrar todas las unidades seleccionadas y use un diseño de partición predeterminado de mejor esfuerzo" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Configuración manual" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Sin configuración" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Sin configuración" + #, fuzzy msgid "Password" msgstr "Contraseña de root" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} contiene particiones en cola, esto eliminará esas particiones, ¿estás seguro?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Paquetes adicionales" + +#, fuzzy +msgid "Add profile" +msgstr "Perfil" + +#, fuzzy +msgid "Edit profile" +msgstr "Perfil" + +#, fuzzy +msgid "Delete profile" +msgstr "Eliminar intefaz" + +#, fuzzy +msgid "Profile name: " +msgstr "Perfil" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "El nombre de usuario que ingresó no es válido. Intente nuevamente" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Escriba paquetes adicionales para instalar (separados por espacios, deja en blanco para omitir): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Escriba paquetes adicionales para instalar (separados por espacios, deja en blanco para omitir): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Seleccione un controlador de gráficos o déjelo en blanco para instalar todos los controladores de código abierto" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Introduzca el sector de inicio (porcentaje o número de bloque, predeterminado: {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Sin configuración" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Ingrese el sector final de la partición (porcentaje o número de bloque, ej: {}): " +#, fuzzy +msgid "Profiles" +msgstr "Perfil" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Seleccione uno o más discos duros para usar y configurar" #~ msgid "Add :" #~ msgstr "Añadir :" diff --git a/archinstall/locales/fr/LC_MESSAGES/base.mo b/archinstall/locales/fr/LC_MESSAGES/base.mo index 16b8a6cf..22e004e0 100644 Binary files a/archinstall/locales/fr/LC_MESSAGES/base.mo and b/archinstall/locales/fr/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/fr/LC_MESSAGES/base.po b/archinstall/locales/fr/LC_MESSAGES/base.po index 76080e40..87b365e0 100644 --- a/archinstall/locales/fr/LC_MESSAGES/base.po +++ b/archinstall/locales/fr/LC_MESSAGES/base.po @@ -103,7 +103,7 @@ msgid "Enter the end location (in parted units: s, GB, %, etc. ; ex: {}): " msgstr "" msgid "{} contains queued partitions, this will remove those, are you sure?" -msgstr "{} contient des partitions en file d'attente, cela les supprimera, êtes-vous sûr ?" +msgstr "{} contient des partitions en file d'attente, cela les supprimera, êtes-vous sûr ?" msgid "" "{}\n" @@ -127,7 +127,7 @@ msgid " * Partition mount-points are relative to inside the installation, the bo msgstr " * Les points de montage de la partition sont relatifs à l'intérieur de l'installation, le démarrage serait /boot par exemple." msgid "Select where to mount partition (leave blank to remove mountpoint): " -msgstr "Sélectionner où monter la partition (laisser vide pour supprimer le point de montage) : " +msgstr "Sélectionner où monter la partition (laisser vide pour supprimer le point de montage) : " msgid "" "{}\n" @@ -166,7 +166,7 @@ msgstr "" "Sélectionner la partition sur laquelle définir un système de fichiers" msgid "Enter a desired filesystem type for the partition: " -msgstr "Entrer un type de système de fichiers souhaité pour la partition : " +msgstr "Entrer un type de système de fichiers souhaité pour la partition : " msgid "Archinstall language" msgstr "Langue d'Archinstall" @@ -223,10 +223,10 @@ msgid "Choose which locale encoding to use" msgstr "Choisir quel encodage de paramètres régionaux utiliser" msgid "Select one of the values shown below: " -msgstr "Sélectionner l'une des valeurs ci-dessous : " +msgstr "Sélectionner l'une des valeurs ci-dessous : " msgid "Select one or more of the options below: " -msgstr "Sélectionner une ou plusieurs des options ci-dessous : " +msgstr "Sélectionner une ou plusieurs des options ci-dessous : " msgid "Adding partition...." msgstr "Ajout de la partition...." @@ -235,10 +235,10 @@ msgid "You need to enter a valid fs-type in order to continue. See `man parted` msgstr "Vous devez entrer un type de fs valide pour continuer. Voir `man parted` pour les types de fs valides." msgid "Error: Listing profiles on URL \"{}\" resulted in:" -msgstr "Erreur : la liste des profils sur l'URL \"{}\" a entraîné :" +msgstr "Erreur : la liste des profils sur l'URL \"{}\" a entraîné :" msgid "Error: Could not decode \"{}\" result as JSON:" -msgstr "Erreur : Impossible de décoder le résultat \"{}\" en tant que JSON :" +msgstr "Erreur : Impossible de décoder le résultat \"{}\" en tant que JSON :" msgid "Keyboard layout" msgstr "Disposition du clavier" @@ -309,7 +309,7 @@ msgstr "" "Souhaitez-vous continuer ?" msgid "Re-using partition instance: {}" -msgstr "Réutilisation de l'instance de partition : {}" +msgstr "Réutilisation de l'instance de partition : {}" msgid "Create a new partition" msgstr "Créer une nouvelle partition" @@ -364,19 +364,19 @@ msgid "Suggest partition layout" msgstr "Suggérer la disposition des partitions" msgid "Enter a password: " -msgstr "Entrer un mot de passe : " +msgstr "Entrer un mot de passe : " msgid "Enter a encryption password for {}" msgstr "Entrer un mot de passe de chiffrement pour {}" msgid "Enter disk encryption password (leave blank for no encryption): " -msgstr "Entrer le mot de passe de chiffrement du disque (laisser vide pour aucun chiffrement) : " +msgstr "Entrer le mot de passe de chiffrement du disque (laisser vide pour aucun chiffrement) : " msgid "Create a required super-user with sudo privileges: " -msgstr "Créer un super-utilisateur requis avec les privilèges sudo : " +msgstr "Créer un super-utilisateur requis avec les privilèges sudo : " msgid "Enter root password (leave blank to disable root): " -msgstr "Entrer le mot de passe root (laisser vide pour désactiver root) : " +msgstr "Entrer le mot de passe root (laisser vide pour désactiver root) : " msgid "Password for user \"{}\": " msgstr "Mot de passe pour l'utilisateur \"{}\" : " @@ -385,7 +385,7 @@ msgid "Verifying that additional packages exist (this might take a few seconds)" msgstr "Vérifier que des packages supplémentaires existent (cela peut prendre quelques secondes)" msgid "Would you like to use automatic time synchronization (NTP) with the default time servers?\n" -msgstr "Souhaitez-vous utiliser la synchronisation automatique de l'heure (NTP) avec les serveurs de temps par défaut ?\n" +msgstr "Souhaitez-vous utiliser la synchronisation automatique de l'heure (NTP) avec les serveurs de temps par défaut ?\n" msgid "" "Hardware time and other post-configuration steps might be required in order for NTP to work.\n" @@ -395,7 +395,7 @@ msgstr "" "Pour plus d'informations, veuillez consulter le wiki Arch" msgid "Enter a username to create an additional user (leave blank to skip): " -msgstr "Entrer un nom d'utilisateur pour créer un utilisateur supplémentaire (laisser vide pour ignorer) : " +msgstr "Entrer un nom d'utilisateur pour créer un utilisateur supplémentaire (laisser vide pour ignorer) : " msgid "Use ESC to skip\n" msgstr "Utiliser ESC pour ignorer\n" @@ -588,22 +588,22 @@ msgid "[!] A log file has been created here: {}" msgstr "[!] Un fichier journal a été créé ici : {}" msgid "Would you like to use BTRFS subvolumes with a default structure?" -msgstr "Souhaitez-vous utiliser des sous-volumes BTRFS avec une structure par défaut ?" +msgstr "Souhaitez-vous utiliser des sous-volumes BTRFS avec une structure par défaut ?" msgid "Would you like to use BTRFS compression?" -msgstr "Souhaitez-vous utiliser la compression BTRFS ?" +msgstr "Souhaitez-vous utiliser la compression BTRFS ?" msgid "Would you like to create a separate partition for /home?" -msgstr "Souhaitez-vous créer une partition séparée pour /home ?" +msgstr "Souhaitez-vous créer une partition séparée pour /home ?" msgid "The selected drives do not have the minimum capacity required for an automatic suggestion\n" msgstr "Les disques sélectionnés n'ont pas la capacité minimale requise pour une suggestion automatique\n" msgid "Minimum capacity for /home partition: {}GB\n" -msgstr "Capacité minimale pour la partition /home : {} Go\n" +msgstr "Capacité minimale pour la partition /home : {} Go\n" msgid "Minimum capacity for Arch Linux partition: {}GB" -msgstr "Capacité minimale pour la partition Arch Linux : {} Go" +msgstr "Capacité minimale pour la partition Arch Linux : {} Go" msgid "Continue" msgstr "Poursuivre" @@ -645,7 +645,7 @@ msgid "Mark/Unmark a partition as compressed (btrfs only)" msgstr "Marquer/Démarquer une partition comme compressée (btrfs uniquement)" msgid "The password you are using seems to be weak, are you sure you want to use it?" -msgstr "Le mot de passe que vous utilisez semble faible, êtes-vous sûr de vouloir l'utiliser ?" +msgstr "Le mot de passe que vous utilisez semble faible, êtes-vous sûr de vouloir l'utiliser ?" msgid "Provides a selection of desktop environments and tiling window managers, e.g. gnome, kde, sway" msgstr "Fournit une sélection d'environnements de bureau et de gestionnaires de fenêtres en mosaïque, par ex. gnome, kde, sway" @@ -669,16 +669,16 @@ msgid "Press Enter to continue." msgstr "Appuyer sur Entrée pour continuer." msgid "Would you like to chroot into the newly created installation and perform post-installation configuration?" -msgstr "Souhaitez-vous chrooter dans l'installation nouvellement créée et effectuer la configuration post-installation ?" +msgstr "Souhaitez-vous chrooter dans l'installation nouvellement créée et effectuer la configuration post-installation ?" msgid "Are you sure you want to reset this setting?" -msgstr "Voulez-vous vraiment réinitialiser ce paramètre ?" +msgstr "Voulez-vous vraiment réinitialiser ce paramètre ?" msgid "Select one or more hard drives to use and configure\n" msgstr "Sélectionner un ou plusieurs disques durs à utiliser et à configurer\n" msgid "Any modifications to the existing setting will reset the disk layout!" -msgstr "Toute modification du paramètre existant réinitialisera la disposition du disque !" +msgstr "Toute modification du paramètre existant réinitialisera la disposition du disque !" msgid "If you reset the harddrive selection this will also reset the current disk layout. Are you sure?" msgstr "Si vous réinitialisez la sélection du disque dur, cela réinitialisera également la disposition actuelle du disque. Êtes-vous sûr ?" @@ -691,7 +691,7 @@ msgid "" "contains queued partitions, this will remove those, are you sure?" msgstr "" "{}\n" -"contient des partitions en file d'attente, cela les supprimera, êtes-vous sûr ?" +"contient des partitions en file d'attente, cela les supprimera, êtes-vous sûr ?" msgid "No audio server" msgstr "Pas de serveur audio" @@ -761,7 +761,7 @@ msgid "The username you entered is invalid. Try again" msgstr "Le nom d'utilisateur que vous avez saisi n'est pas valide. Réessayer" msgid "Should \"{}\" be a superuser (sudo)?" -msgstr "\"{}\" devrait-il être un superutilisateur (sudo) ?" +msgstr "\"{}\" devrait-il être un superutilisateur (sudo) ?" msgid "Select which partitions to encrypt" msgstr "Sélectionner les partitions à chiffrer" @@ -804,13 +804,13 @@ msgstr "" "Note :" msgid " - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Valeur maximale : {max_downloads} (Autorise {max_downloads} téléchargements parallèles, autorise {max_downloads+1} téléchargements à la fois)" +msgstr " - Valeur maximale : {max_downloads} (Autorise {max_downloads} téléchargements parallèles, autorise {max_downloads+1} téléchargements à la fois)" msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )" msgstr " - Valeur minimale : 1 (Autorise 1 téléchargement parallèle, autorise 2 téléchargements à la fois)" msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" -msgstr " - Désactiver/Défaut : 0 (Désactive le téléchargement parallèle, n'autorise qu'un seul téléchargement à la fois)" +msgstr " - Désactiver/Défaut : 0 (Désactive le téléchargement parallèle, n'autorise qu'un seul téléchargement à la fois)" #, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" @@ -837,6 +837,125 @@ msgstr "Pour pouvoir utiliser cette traduction, veuillez installer manuellement msgid "The font should be stored as {}" msgstr "La police doit être stockée sous {}" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Sélectionner une action pour '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Sélectionner un ou plusieurs disques durs à utiliser et à configurer" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Si vous réinitialisez la sélection du disque dur, cela réinitialisera également la disposition actuelle du disque. Êtes-vous sûr ?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Ajout de la partition...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Supprimer une partition" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Saisir un répertoire pour la ou les configuration(s) à enregistrer : " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Capacité minimale pour la partition /home : {} Go\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Capacité minimale pour la partition Arch Linux : {} Go" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Ceci est une liste de profils préprogrammés, ils pourraient faciliter l'installation d'outils comme les environnements de bureau" + +#, fuzzy +msgid "Current profile selection" +msgstr "Disposition actuelle des partitions" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Créer une nouvelle partition" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Attribuer un point de montage pour une partition" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Marquer/Démarquer une partition à formater (efface les données)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Marquer/Démarquer une partition comme compressée (btrfs uniquement)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Supprimer le sous-volume" + +#, fuzzy +msgid "Delete partition" +msgstr "Supprimer une partition" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Les points de montage de la partition sont relatifs à l'intérieur de l'installation, le démarrage serait /boot par exemple." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Répertoire non valide : {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Entrer le secteur de début (pourcentage ou numéro de bloc, par défaut : {}) : " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Entrer le secteur de fin de la partition (pourcentage ou numéro de bloc, ex : {}) : " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Mot de passe de chiffrement" @@ -859,27 +978,138 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} contient des partitions en file d'attente, cela les supprimera, êtes-vous sûr ?" +msgid "Use a best-effort default partition layout" +msgstr "Effacer tous les lecteurs sélectionnés et utiliser une disposition de partition par défaut optimale" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Configuration manuelle" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Aucune configuration" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Aucune configuration" + #, fuzzy msgid "Password" msgstr "Mot de passe root" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} contient des partitions en file d'attente, cela les supprimera, êtes-vous sûr ?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Packages supplémentaires" + +#, fuzzy +msgid "Add profile" +msgstr "Profil" + +#, fuzzy +msgid "Edit profile" +msgstr "Profil" + +#, fuzzy +msgid "Delete profile" +msgstr "Supprimer l'interface" + +#, fuzzy +msgid "Profile name: " +msgstr "Profil" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "Le nom d'utilisateur que vous avez saisi n'est pas valide. Réessayer" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Écrire des packages supplémentaires à installer (espaces séparés, laisser vide pour ignorer) : " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Écrire des packages supplémentaires à installer (espaces séparés, laisser vide pour ignorer) : " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" msgstr "" +"\n" +"\n" +"Sélectionner un pilote graphique ou laisser vide pour installer tous les pilotes open-source" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Entrer le secteur de début (pourcentage ou numéro de bloc, par défaut : {}) : " +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Entrer le secteur de fin de la partition (pourcentage ou numéro de bloc, ex : {}) : " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Aucune configuration" + +#, fuzzy +msgid "Profiles" +msgstr "Profil" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Sélectionner un ou plusieurs disques durs à utiliser et à configurer" #, python-brace-format #~ msgid "Edit {origkey} :" diff --git a/archinstall/locales/id/LC_MESSAGES/base.mo b/archinstall/locales/id/LC_MESSAGES/base.mo index 6e707237..b81fe108 100644 Binary files a/archinstall/locales/id/LC_MESSAGES/base.mo and b/archinstall/locales/id/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/id/LC_MESSAGES/base.po b/archinstall/locales/id/LC_MESSAGES/base.po index 6cc19cbf..5ed2f3c8 100644 --- a/archinstall/locales/id/LC_MESSAGES/base.po +++ b/archinstall/locales/id/LC_MESSAGES/base.po @@ -832,46 +832,280 @@ msgid "[Default value: 0] > " msgstr "[Nilai default: 0] > " msgid "To be able to use this translation, please install a font manually that supports the language." -msgstr "Untuk dapat menggunakan terjemahan ini, silakan instal font yang mendukung bahasa tersebut secara manual." +msgstr "" msgid "The font should be stored as {}" -msgstr "Font harus disimpan sebagai {}" +msgstr "" + +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Pilih tindakan untuk '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Pilih satu atau lebih hard drive untuk digunakan dan dikonfigurasi" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Jika Anda mengatur ulang pilihan harddrive, ini juga akan mengatur ulang tata letak disk saat ini. Apakah Anda yakin?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Menambahkan partisi...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Hapus partisi" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Masukkan direktori untuk konfigurasi yang akan disimpan: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Kapasitas minimum untuk partisi /home: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Kapasitas minimum untuk partisi Arch Linux: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Ini adalah daftar profil yang telah diprogram sebelumnya, mereka mungkin memudahkan untuk menginstal hal-hal seperti desktop environment" + +#, fuzzy +msgid "Current profile selection" +msgstr "Tata letak partisi saat ini" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Buat partisi baru" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Tetapkan titik-mount untuk sebuah partisi" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Tandai/Hapus tanda partisi yang akan diformat (menghapus data)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Tandai/Hapus tanda partisi sebagai terkompresi (hanya btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Hapus subvolume" + +#, fuzzy +msgid "Delete partition" +msgstr "Hapus partisi" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Mount point partisi relatif terhadap di dalam instalasi, boot akan menjadi /boot sebagai contoh." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Bukan direktori yang valid: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Masukkan sektor awal (persentase atau nomor blok, default: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Masukkan sektor akhir partisi (persentase atau nomor blok, mis: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + +#, fuzzy msgid "Encryption type" -msgstr "Tipe enkripsi" +msgstr "Kata sandi enkripsi" msgid "Partitions" -msgstr "Partisi" +msgstr "" msgid "No HSM devices available" -msgstr "Tidak ada perangkat HSM yang tersedia" +msgstr "" +#, fuzzy msgid "Partitions to be encrypted" -msgstr "Partisi yang akan dienkripsi" +msgstr "Pilih partisi mana yang akan dienkripsi" msgid "Select disk encryption option" -msgstr "Pilih opsi enkripsi disk" +msgstr "" msgid "Select a FIDO2 device to use for HSM" -msgstr "Pilih perangkat FID02 yang akan digunakan untuk HSM" +msgstr "" -msgid "All settings will be reset, are you sure?" -msgstr "Semua pengaturan akan direset, apakah Anda yakin?" +#, fuzzy +msgid "Use a best-effort default partition layout" +msgstr "Hapus semua drive yang dipilih dan gunakan upaya terbaik tata letak partisi default" -msgid "Back" -msgstr "Kembali" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Konfigurasi manual" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Tidak ada konfigurasi" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" +msgstr "" msgid "Disk encryption" -msgstr "Enkripsi disk" +msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Tidak ada konfigurasi" + +#, fuzzy msgid "Password" -msgstr "Kata sandi" +msgstr "Kata sandi root" -msgid "Partition encryption" -msgstr "Enkripsi partisi" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} berisi partisi yang mengantri, ini akan menghapusnya, apakah Anda yakin?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Masukkan sektor awal (persentase atau nomor blok, default: {}): " +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Paket tambahan" + +#, fuzzy +msgid "Add profile" +msgstr "Profil" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Masukkan sektor akhir partisi (persentase atau nomor blok, mis: {}): " +#, fuzzy +msgid "Edit profile" +msgstr "Profil" + +#, fuzzy +msgid "Delete profile" +msgstr "Hapus interface" + +#, fuzzy +msgid "Profile name: " +msgstr "Profil" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "Nama pengguna yang Anda masukkan tidak valid. Coba lagi" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Ketik paket tambahan untuk diinstal (dipisahkan dengan spasi, biarkan kosong untuk dilewati): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Ketik paket tambahan untuk diinstal (dipisahkan dengan spasi, biarkan kosong untuk dilewati): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Pilih driver grafis atau biarkan kosong untuk menginstal semua driver open-source" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" + +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Tidak ada konfigurasi" + +#, fuzzy +msgid "Profiles" +msgstr "Profil" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Pilih satu atau lebih hard drive untuk digunakan dan dikonfigurasi" diff --git a/archinstall/locales/it/LC_MESSAGES/base.po b/archinstall/locales/it/LC_MESSAGES/base.po index 00df1a3f..c4acc04d 100644 --- a/archinstall/locales/it/LC_MESSAGES/base.po +++ b/archinstall/locales/it/LC_MESSAGES/base.po @@ -837,6 +837,125 @@ msgstr "Per poter utilizzare questa traduzione, installa manualmente un font che msgid "The font should be stored as {}" msgstr "Il carattere dovrebbe essere memorizzato come {}" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Seleziona un'azione per '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Selezionare uno o più dischi rigidi da utilizzare e configurare" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Se si ripristina la selezione del disco rigido, verrà ripristinato anche il layout del disco corrente. Sei sicuro?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Aggiungendo la partizione...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Elimina una partizione" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Immettere una directory per le configurazioni da salvare: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Capacità minima per la partizione /home: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Capacità minima per la partizione Arch Linux: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Questo è un elenco di profili preprogrammati, che potrebbero semplificare l'installazione di elementi come gli ambienti desktop" + +#, fuzzy +msgid "Current profile selection" +msgstr "Layout della partizione corrente" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Crea una nuova partizione" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Assegna punto di montaggio per una partizione" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Seleziona/Deseleziona una partizione da formattare (cancella i dati)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Seleziona/Deseleziona una partizione come compressa (solo btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Elimina sottovolume" + +#, fuzzy +msgid "Delete partition" +msgstr "Elimina una partizione" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * I punti di montaggio della partizione sono relativi all'interno dell'installazione, l'avvio sarebbe per esempio /boot." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Directory non valida: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Inserisci il settore iniziale (percentuale o numero di blocco, predefinito: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Inserisci il settore finale (percentuale o numero di blocco, predefinito: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Password di crittografia" @@ -858,24 +977,135 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} contiene partizioni in coda, questo le rimuoverà, sei sicuro?" +msgid "Use a best-effort default partition layout" +msgstr "Cancella tutte le unità selezionate e utilizza un layout di partizione predefinito ottimale" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Configurazione manuale" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Nessuna configurazione" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Nessuna configurazione" + #, fuzzy msgid "Password" msgstr "Password di root" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} contiene partizioni in coda, questo le rimuoverà, sei sicuro?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Pacchetti aggiuntivi" + +#, fuzzy +msgid "Add profile" +msgstr "Profilo" + +#, fuzzy +msgid "Edit profile" +msgstr "Profilo" + +#, fuzzy +msgid "Delete profile" +msgstr "Elimina interfaccia" + +#, fuzzy +msgid "Profile name: " +msgstr "Profilo" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "Il nome utente inserito non è valido. Riprova" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Scrivi pacchetti aggiuntivi da installare (separati da spazi, lascia vuoto per saltare): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Scrivi pacchetti aggiuntivi da installare (separati da spazi, lascia vuoto per saltare): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Seleziona un driver grafico o lascia vuoto per installare tutti i driver open source" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Inserisci il settore iniziale (percentuale o numero di blocco, predefinito: {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Nessuna configurazione" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Inserisci il settore finale (percentuale o numero di blocco, predefinito: {}): " +#, fuzzy +msgid "Profiles" +msgstr "Profilo" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Selezionare uno o più dischi rigidi da utilizzare e configurare" diff --git a/archinstall/locales/ka/LC_MESSAGES/base.po b/archinstall/locales/ka/LC_MESSAGES/base.po index c89ec795..7a88f96c 100644 --- a/archinstall/locales/ka/LC_MESSAGES/base.po +++ b/archinstall/locales/ka/LC_MESSAGES/base.po @@ -838,6 +838,126 @@ msgstr "თარგმანის გამოსაყენებლად msgid "The font should be stored as {}" msgstr "ფონტი {}-ში უნდა იყოს შენახული" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "აირჩიეთ ქმედება '{}'-სთვის" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "აირჩიეთ ერთი ან მეტი მყარი დისკი და მოირგეთ" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "ეს მყარი დისკის არჩევანს და მიმდინარე დისკის განლაგებას საწყის მნიშვნელობებზე დააბრუნებს. დარწმუნებული ბრძანდებით?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "დანაყოფები" + +#, fuzzy +msgid "Select a partitioning option" +msgstr "აირჩიეთ დისკის დაშიფვრის პარამეტრი" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "შეიყვანეთ საქაღალდე, სადაც კონფიგურაცი(ებ)-ი იქნება შენახული: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "მინიმალური სივრცე დანაყოფისთვის /home: {}გბ\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "მინიმალური სივრცე ArchLinux-ის დანაყოფისთვის: {}გბ" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "ეს წინასწარ მითითებული პროფილების სიაა. მათი დახმარებით ისეთი რამების, როგორიცაა სამუშაო მაგიდის გარემოები, დაყენება უფრო ადვილია" + +#, fuzzy +msgid "Current profile selection" +msgstr "მიმდინარე დანაყოფების განლაგება" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "ახალი დანაყოფის შექმნა" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "დანაყოფის მიმაგრების წერტილის მინიჭება" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "დანაყოფის დასაფორმატებლობის ჭდის მოხსნა/დადება (მონაცემები წაიშლება)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "დანაყოფზე შეკუმშულობის ჭდის მოხსნა/დადება (მხოლოდ btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "ქვეტომის წაშლა" + +#, fuzzy +msgid "Delete partition" +msgstr "დანაყოფის წაშლა" + +#, fuzzy +msgid "Partition" +msgstr "დანაყოფები" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * დანაყოფის მიმაგრების წერტილები შედარებითია დაყენების შიგნით. ჩატვირთვა, მაგალითად, /boot შეიძლება, იყოს." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "არასწორი საქაღალდე: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "შეიყვანეთ საწყისი სექტორი (პროცენტებში ან ბლოკის ნომერი. ნაგულისხმები: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "შეიყვანეთ დანაყოფის ბოლო სექტორი (პროცენტულად ან ბლოკის ნომერი. მაგ: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + msgid "Encryption type" msgstr "დაშიფვრის ტიპი" @@ -856,23 +976,135 @@ msgstr "აირჩიეთ დისკის დაშიფვრის პ msgid "Select a FIDO2 device to use for HSM" msgstr "აირჩიეთ HSM-სთვის გამოსაყენებელი FIDO2 მოწყობილობა" -msgid "All settings will be reset, are you sure?" -msgstr "ყველა პარამეტრი დაბრუნდება. დარწმუნებული ბრძანდებით?" +#, fuzzy +msgid "Use a best-effort default partition layout" +msgstr "მონიშნულ დისკებზე ყველაფრის წაშლა და დანაყოფების განლაგების საუკეთესო განლაგების გამოყენება" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "დანაყოფები" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "მორგების გარეშე" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "დანაყოფის დაშიფვრა" + +msgid " ! Formatting {} in " +msgstr "" + +#, fuzzy +msgid "← Back" msgstr "უკან" msgid "Disk encryption" msgstr "დისკის დაშიფვრა" +#, fuzzy +msgid "Configuration" +msgstr "მორგების გარეშე" + msgid "Password" msgstr "პაროლი" -msgid "Partition encryption" -msgstr "დანაყოფის დაშიფვრა" +msgid "All settings will be reset, are you sure?" +msgstr "ყველა პარამეტრი დაბრუნდება. დარწმუნებული ბრძანდებით?" + +msgid "Back" +msgstr "უკან" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "შეიყვანეთ საწყისი სექტორი (პროცენტებში ან ბლოკის ნომერი. ნაგულისხმები: {}): " +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "დამატებითი პაკეტები" + +#, fuzzy +msgid "Add profile" +msgstr "პროფილი" + +#, fuzzy +msgid "Edit profile" +msgstr "პროფილი" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "შეიყვანეთ დანაყოფის ბოლო სექტორი (პროცენტულად ან ბლოკის ნომერი. მაგ: {}): " +#, fuzzy +msgid "Delete profile" +msgstr "ინტერფეისის წაშლა" + +#, fuzzy +msgid "Profile name: " +msgstr "პროფილი" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "შეყვანილი მომხმარებლის სახელი არასწორია. კიდევ სცადეთ" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "დამატებითი პაკეტები დასაყენებლად (გამოტოვებით გამოყოფილი, გამოსატოვებლად ცარიელი დატოვეთ): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "დამატებითი პაკეტები დასაყენებლად (გამოტოვებით გამოყოფილი, გამოსატოვებლად ცარიელი დატოვეთ): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"აირჩიეთ გრაფიკის დრაივერი ან, ღია კოდის მქონე დრაივერის დასაყენებლად, ცარიელი დატოვეთ" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" + +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "მორგების გარეშე" + +#, fuzzy +msgid "Profiles" +msgstr "პროფილი" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "აირჩიეთ ერთი ან მეტი მყარი დისკი და მოირგეთ" diff --git a/archinstall/locales/ko/LC_MESSAGES/base.po b/archinstall/locales/ko/LC_MESSAGES/base.po index 4363031e..3503a816 100644 --- a/archinstall/locales/ko/LC_MESSAGES/base.po +++ b/archinstall/locales/ko/LC_MESSAGES/base.po @@ -838,6 +838,125 @@ msgstr "이 번역을 사용하려면 해당 언어를 지원하는 글꼴을 msgid "The font should be stored as {}" msgstr "글꼴은 {} (으)로 저장해야 합니다" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "'{}' 에 대한 작업 선택" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "사용하고 구성할 하드 드라이브를 하나 이상 선택하세요" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "하드 드라이브 선택을 재설정하면 현재 디스크 레이아웃도 재설정됩니다. 정말 진행하시겠습니까?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "파티션 추가 중...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "파티션 제거" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "저장할 구성의 디렉토리를 입력하세요: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Arch Linux 파티션의 최대 용량: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Arch Linux 파티션의 최소 용량: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "이것은 사전 프로그래밍된 프로필 목록이며 데스크톱 환경과 같은 것을 더 쉽게 설치할 수 있습니다" + +#, fuzzy +msgid "Current profile selection" +msgstr "현재 파티션 레이아웃" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "새 파티션 생성" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "파티션에 대한 마운트 지점 할당" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "포맷할 파티션 표시/표시 해제 (데이터 삭제)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "파티션을 압축된 것으로 표시/표시 해제(btrfs만 해당)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "하위 볼륨 제거" + +#, fuzzy +msgid "Delete partition" +msgstr "파티션 제거" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * 파티션 마운트 포인트는 설치 내부를 기준으로 하며 예를 들어 부팅은 /boot 입니다." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "올바른 디렉터리가 아닙니다: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "시작 섹터를 입력하세요 (백분율 또는 블록 번호, 기본값: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "파티션의 끝 섹터를 입력하세요 (백분율 또는 블록 번호, 예시: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "비밀번호 암호화" @@ -859,24 +978,135 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} 에 대기 중인 파티션이 포함되어 있습니다. 그러면 이러한 파티션이 제거됩니다. 정말 진행하시겠습니까?" +msgid "Use a best-effort default partition layout" +msgstr "선택한 모든 드라이브를 지우고 최선의 기본 파티션 레이아웃 사용" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "수동 구성" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "구성 없음" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "구성 없음" + #, fuzzy msgid "Password" msgstr "루트 비밀번호" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} 에 대기 중인 파티션이 포함되어 있습니다. 그러면 이러한 파티션이 제거됩니다. 정말 진행하시겠습니까?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "추가 패키지" + +#, fuzzy +msgid "Add profile" +msgstr "프로필" + +#, fuzzy +msgid "Edit profile" +msgstr "프로필" + +#, fuzzy +msgid "Delete profile" +msgstr "인터페이스 제거" + +#, fuzzy +msgid "Profile name: " +msgstr "프로필" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "입력한 사용자 이름이 잘못되었습니다. 다시 시도하세요" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "설치할 추가 패키지 작성하세요 (띄어쓰기로 구분, 건너뛰려면 공백): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "설치할 추가 패키지 작성하세요 (띄어쓰기로 구분, 건너뛰려면 공백): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"모든 오픈 소스 드라이버를 설치하려면 그래픽 드라이버를 선택하거나 공백으로 두세요" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "시작 섹터를 입력하세요 (백분율 또는 블록 번호, 기본값: {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "구성 없음" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "파티션의 끝 섹터를 입력하세요 (백분율 또는 블록 번호, 예시: {}): " +#, fuzzy +msgid "Profiles" +msgstr "프로필" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "사용하고 구성할 하드 드라이브를 하나 이상 선택하세요" diff --git a/archinstall/locales/languages.json b/archinstall/locales/languages.json index 1e33dcde..58a55373 100644 --- a/archinstall/locales/languages.json +++ b/archinstall/locales/languages.json @@ -169,7 +169,7 @@ {"abbr": "tr", "lang": "Turkish", "translated_lang" : "Türkçe"}, {"abbr": "tw", "lang": "Twi"}, {"abbr": "ug", "lang": "Uighur"}, - {"abbr": "uk", "lang": "Ukrainian", "translated_lang": "Українська"}, + {"abbr": "uk", "lang": "Ukrainian"}, {"abbr": "ur", "lang": "Urdu", "translated_lang": "اردو"}, {"abbr": "uz", "lang": "Uzbek"}, {"abbr": "ve", "lang": "Venda"}, diff --git a/archinstall/locales/nl/LC_MESSAGES/base.po b/archinstall/locales/nl/LC_MESSAGES/base.po index 7f3de195..3f97513a 100644 --- a/archinstall/locales/nl/LC_MESSAGES/base.po +++ b/archinstall/locales/nl/LC_MESSAGES/base.po @@ -865,6 +865,122 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Kies een actie voor '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Selecteer één of meer in te stellen harde schijven" + +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Bezig met toevoegen van partitie…" + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Partitie verwijderen" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Voer de naam in van de map waarin de configuratie(s) moet(en) worden vastgelegd: " + +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "" + +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Dit is een vooraf opgestelde lijst met profielen, welke het installeren van zaken als werkomgevingen vereenvoudigt" + +#, fuzzy +msgid "Current profile selection" +msgstr "Huidige partitie-indeling" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Partitie aanmaken" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Aankoppelpunt toekennen aan partitie" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Partitie (de)markeren voor formatteren (alle gegevens worden gewist)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Partitie (de)markeren voor versleuteling" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Gebruiker verwijderen" + +#, fuzzy +msgid "Delete partition" +msgstr "Partitie verwijderen" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " *Partitie-aankoppelpunten zijn gekoppeld aan de fysieke installatie. Voorbeeld: ‘boot’ wordt ‘/boot’." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Ongeldige map: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Voer de beginsector in (percentage of bloknummer - standaard: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Voer de eindsector in (percentage of bloknummer - bijvoorbeeld: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Versleutelwachtwoord instellen" @@ -887,27 +1003,137 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "‘{}’ bevat in behandeling zijnde partities, welke hierdoor worden verwijderd. Weet u zeker dat u wilt doorgaan?" +msgid "Use a best-effort default partition layout" +msgstr "Alle geselecteerde schijven formatteren en best mogelijke partitie-indeling gebruiken" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Configuratie vastleggen" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Geen configuratie" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Geen configuratie" + #, fuzzy msgid "Password" msgstr "Rootwachtwoord" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "‘{}’ bevat in behandeling zijnde partities, welke hierdoor worden verwijderd. Weet u zeker dat u wilt doorgaan?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Aanvullende pakketten" + +#, fuzzy +msgid "Add profile" +msgstr "Profiel" + +#, fuzzy +msgid "Edit profile" +msgstr "Profiel" + +#, fuzzy +msgid "Delete profile" +msgstr "Gebruiker verwijderen" + +#, fuzzy +msgid "Profile name: " +msgstr "Profiel" + +msgid "The profile name you entered is already in use. Try again" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Voer de beginsector in (percentage of bloknummer - standaard: {}): " +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Typ de namen van te installeren pakketten (spatiegescheiden - laat leeg om over te slaan): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Typ de namen van te installeren pakketten (spatiegescheiden - laat leeg om over te slaan): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Kies een grafisch stuurprogramma of laat leeg om alle opensource-stuurprogramma's te installeren" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Voer de eindsector in (percentage of bloknummer - bijvoorbeeld: {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Geen configuratie" + +#, fuzzy +msgid "Profiles" +msgstr "Profiel" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Selecteer één of meer in te stellen harde schijven" #~ msgid "Add :" #~ msgstr "Toevoegen:" diff --git a/archinstall/locales/pl/LC_MESSAGES/base.po b/archinstall/locales/pl/LC_MESSAGES/base.po index ec4fcbd4..3f6c1dca 100644 --- a/archinstall/locales/pl/LC_MESSAGES/base.po +++ b/archinstall/locales/pl/LC_MESSAGES/base.po @@ -839,6 +839,125 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Wybierz akcję dla '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Wybierz jeden lub więcej dysków twardych do użycia i skonfiguruj je" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Jeżeli zresetujesz wybór dysków, zresetujesz także obecny układ dysków. Jesteś pewny, że chcesz to zrobić?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Dodawanie partycji..." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Usuń partycję" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Wprowadź katalog, w którym ma zostać zapisana konfiguracja: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Maksymalna pojemność dla partycji /home: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Minimalna pojemność dla partycji Arch Linux: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "To jest lista wstępnie zaprogramowanych profili, które mogą ułatwić instalację takich rzeczy jak środowiska graficzne" + +#, fuzzy +msgid "Current profile selection" +msgstr "Aktualny układ partycji" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Utwórz nową partycję" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Przydzielanie punktu montowania dla partycji" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Zaznacz/odznacz partycję, która ma zostać sformatowana (wymazuje dane)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Oznacz/odznacz partycje jako skompresowaną (tylko w btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Usuń podwolumen" + +#, fuzzy +msgid "Delete partition" +msgstr "Usuń partycję" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Punkty montowania partycji są względne w stosunku do wnętrza instalacji, np. partycja startowa to /boot." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Nie jest to prawidłowy katalog: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Wprowadź sektor początkowy (procent lub numer bloku, domyślnie: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Wprowadź sektor końcowy (procent lub numer bloku, domyślnie: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Hasło szyfrujące" @@ -861,27 +980,138 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} zawiera partycje oczekujące w kolejce, to spowoduje ich usunięcie, czy jesteś pewien?" +msgid "Use a best-effort default partition layout" +msgstr "Wymaż wszystkie wybrane dyski i użyj najlepszego domyślnego układu partycji" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Ręczna konfiguracja" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Brak konfiguracji" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Brak konfiguracji" + #, fuzzy msgid "Password" msgstr "Hasło użytkownika root" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} zawiera partycje oczekujące w kolejce, to spowoduje ich usunięcie, czy jesteś pewien?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Dodatkowe pakiety" + +#, fuzzy +msgid "Add profile" +msgstr "Profil" + +#, fuzzy +msgid "Edit profile" +msgstr "Profil" + +#, fuzzy +msgid "Delete profile" +msgstr "Usuń interfejs" + +#, fuzzy +msgid "Profile name: " +msgstr "Profil" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "Wprowadzona nazwa użytkownika jest nieprawidłowa. Spróbuj ponownie" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Wpisz dodatkowe pakiety do zainstalowania (oddzielone spacjami, pozostaw puste aby pominąć): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Wpisz dodatkowe pakiety do zainstalowania (oddzielone spacjami, pozostaw puste aby pominąć): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Wybierz sterownik graficzny lub pozostaw puste pole, aby zainstalować wszystkie sterowniki typu open source" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Wprowadź sektor początkowy (procent lub numer bloku, domyślnie: {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Brak konfiguracji" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Wprowadź sektor końcowy (procent lub numer bloku, domyślnie: {}): " +#, fuzzy +msgid "Profiles" +msgstr "Profil" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Wybierz jeden lub więcej dysków twardych do użycia i skonfiguruj je" #~ msgid "Add :" #~ msgstr "Dodaj :" diff --git a/archinstall/locales/pt/LC_MESSAGES/base.po b/archinstall/locales/pt/LC_MESSAGES/base.po index 4f772aae..9f44f02f 100644 --- a/archinstall/locales/pt/LC_MESSAGES/base.po +++ b/archinstall/locales/pt/LC_MESSAGES/base.po @@ -887,6 +887,122 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Selecione uma ação para '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Seleciona um ou mais discos rígidos para usar e configurar" + +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Adicionando partição...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Eliminar uma partição" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Introduz um diretório para as configurações a serem guardadas: " + +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "" + +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Esta é uma lista de perfis pré-programados, podem facilitar a instalação de ambientes de trabalho" + +#, fuzzy +msgid "Current profile selection" +msgstr "Esquema actual da partições" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Criar uma nova partição" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Atribuir um ponto de montagem para uma partição" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Marcar/Desmarcar uma partição para ser formatada (apaga os dados)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Marcar/Desmarcar uma partição como encriptada" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Eliminar Utilizador" + +#, fuzzy +msgid "Delete partition" +msgstr "Eliminar uma partição" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Os pontos de montagem das partições são relativos à dentro da instalação, o boot seria /boot por exemplo." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Não é uma diretoria válida: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Escreve o sector de início (percentagem ou número de bloco, padrão: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Escreve o sector final da partição (percentagem ou número de bloco, ex: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Define a palavra-passe de encriptação" @@ -909,27 +1025,137 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} contem partições em fila, isto irá remover essas, tens a certeza?" +msgid "Use a best-effort default partition layout" +msgstr "Limpar todos os discos selecionados e usar um esquema de partições padrão de melhor desempenho" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Guardar configuração" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Nenhuma configuração" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Nenhuma configuração" + #, fuzzy msgid "Password" msgstr "Palavra-passe de root" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} contem partições em fila, isto irá remover essas, tens a certeza?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Pacotes adicionais" + +#, fuzzy +msgid "Add profile" +msgstr "Perfil" + +#, fuzzy +msgid "Edit profile" +msgstr "Perfil" + +#, fuzzy +msgid "Delete profile" +msgstr "Eliminar Utilizador" + +#, fuzzy +msgid "Profile name: " +msgstr "Perfil" + +msgid "The profile name you entered is already in use. Try again" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Escreve o sector de início (percentagem ou número de bloco, padrão: {}): " +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Escreve pacotes adicionais para instalar (separados por espaço, deixa em branco para não instalar nada): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Escreve pacotes adicionais para instalar (separados por espaço, deixa em branco para não instalar nada): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Seleciona um driver de gráficos ou deixa em branco para instalar todos os drivers open-source" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Escreve o sector final da partição (percentagem ou número de bloco, ex: {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Nenhuma configuração" + +#, fuzzy +msgid "Profiles" +msgstr "Perfil" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Seleciona um ou mais discos rígidos para usar e configurar" #~ msgid "Add :" #~ msgstr "Adicionar :" diff --git a/archinstall/locales/pt_BR/LC_MESSAGES/base.mo b/archinstall/locales/pt_BR/LC_MESSAGES/base.mo index 8cba125a..4dd57dba 100644 Binary files a/archinstall/locales/pt_BR/LC_MESSAGES/base.mo and b/archinstall/locales/pt_BR/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/pt_BR/LC_MESSAGES/base.po b/archinstall/locales/pt_BR/LC_MESSAGES/base.po index 88506ce0..d0881905 100644 --- a/archinstall/locales/pt_BR/LC_MESSAGES/base.po +++ b/archinstall/locales/pt_BR/LC_MESSAGES/base.po @@ -841,6 +841,126 @@ msgstr "Para poder usar esta tradução, instale manualmente uma fonte que supor msgid "The font should be stored as {}" msgstr "A fonte deve ser armazenada como {}" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Selecione uma ação para '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Selecione um ou mais discos rígidos para usar e configurar" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Se você redefinir a seleção de unidades isso também redefinirá o layout da unidade atual. Tem certeza?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Adicionando partição...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Deletar uma partição" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Digite um diretório para as configurações serem salvas: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Capacidade mínima para partição /home : {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Capacidade mínima para a partição do Arch Linux: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Esta é uma lista de perfis pré-programados, que podem por exemplo facilitar a instalação de ambientes gráficos" + +#, fuzzy +msgid "Current profile selection" +msgstr "Layout de partições atual" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Criar uma nova partição" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Atribuir um ponto de montagem para uma partição" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Marcar/Desmarcar uma partição para ser formatada (apaga os dados)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Marcar/desmarcar a partição como comprimida (apenas btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Deletar subvolume" + +#, fuzzy +msgid "Delete partition" +msgstr "Deletar uma partição" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Os pontos de montagem das partições são relativos aos de dentro da instalação, boot por exemplo seria /boot." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Não é um diretório válido: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Digite o setor de início (porcentagem ou número do bloco, padrão: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Digite o setor final da partição (porcentagem ou número de bloco, ex.: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + +#, fuzzy msgid "Encryption type" msgstr "Tipo de encriptação" @@ -859,43 +979,135 @@ msgstr "Selecione a opção de encriptação de disco" msgid "Select a FIDO2 device to use for HSM" msgstr "Selecione um dispositivo FIDO2 para usar como HSM" -msgid "All settings will be reset, are you sure?" -msgstr "Todas as configurações serão redefinidas,você tem certeza ?" +#, fuzzy +msgid "Use a best-effort default partition layout" +msgstr "Apagar todos os discos selecionados e usar um esquema de partições padrão de melhor desempenho" -msgid "Back" -msgstr "Voltar" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Configuração manual" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Nenhuma configuração" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "Encriptação de partição" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" +msgstr "" msgid "Disk encryption" msgstr "Encriptação de disco" +#, fuzzy +msgid "Configuration" +msgstr "Nenhuma configuração" + msgid "Password" msgstr "Senha" -msgid "Partition encryption" -msgstr "Encriptação de partição" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} contém partições em fila, isto irá removê-las, tem certeza?" -msgid "When picking a directory to save configuration files to, by default we will ignore the following folders: " +msgid "Back" msgstr "" -msgid "Finding possible directories to save configuration files ..." +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" msgstr "" #, fuzzy -msgid "Select directory (or directories) for saving configuration files" -msgstr "Selecione um ou mais discos rígidos para usar e configurar" +msgid "Installed packages" +msgstr "Pacotes adicionais" + +#, fuzzy +msgid "Add profile" +msgstr "Perfil" + +#, fuzzy +msgid "Edit profile" +msgstr "Perfil" + +#, fuzzy +msgid "Delete profile" +msgstr "Deletar interface" + +#, fuzzy +msgid "Profile name: " +msgstr "Perfil" +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "O nome de usuário que você digitou é inválido. Tente novamente" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Digite pacotes adicionais para instalar (separados por espaço, deixe em branco para pular): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Digite pacotes adicionais para instalar (separados por espaço, deixe em branco para pular): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy msgid "" -"Do you want to save {} configuration file(s) in the following locations?\n" "\n" -"{}" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Selecione um driver de vídeo ou deixe em branco para instalar os drivers completamente open-source" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" + +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" msgstr "" #, fuzzy -msgid "Saving {} configuration files to {}" -msgstr "Salvar configuração" +msgid "Disk configuration" +msgstr "Nenhuma configuração" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Digite o setor de início (porcentagem ou número do bloco, padrão: {}): " +#, fuzzy +msgid "Profiles" +msgstr "Perfil" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Digite o setor final da partição (porcentagem ou número de bloco, ex.: {}): " +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Selecione um ou mais discos rígidos para usar e configurar" diff --git a/archinstall/locales/ru/LC_MESSAGES/base.mo b/archinstall/locales/ru/LC_MESSAGES/base.mo index 0fe08128..1da3a370 100644 Binary files a/archinstall/locales/ru/LC_MESSAGES/base.mo and b/archinstall/locales/ru/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/ru/LC_MESSAGES/base.po b/archinstall/locales/ru/LC_MESSAGES/base.po index 1a33881f..b99c6473 100644 --- a/archinstall/locales/ru/LC_MESSAGES/base.po +++ b/archinstall/locales/ru/LC_MESSAGES/base.po @@ -838,6 +838,125 @@ msgstr "Чтобы иметь возможность использовать э msgid "The font should be stored as {}" msgstr "Шрифт должен быть сохранен как {}" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "Для запуска Archinstall требуются привилегии root. Для получения дополнительной информации смотрите --help." + +#, fuzzy +msgid "Select an execution mode" +msgstr "Выберите действие для '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Выберите один или несколько жестких дисков для использования и настройте их" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Если вы сбросите выбор жесткого диска, это также сбросит текущую разметку диска. Вы уверены?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Добавление раздела...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Удалить раздел" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Введите каталог для сохранения конфигурации (-ций): " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Минимальный размер раздела /home: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Минимальный размер раздела Arch Linux: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Это список предварительно запрограммированных профилей, они могут облегчить установку таких вещей, как окружения рабочего стола" + +#, fuzzy +msgid "Current profile selection" +msgstr "Текущая разметка разделов" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Создать новый раздел" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Назначить точку монтирования для раздела" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Пометить/снять отметку с раздела, который будет отформатирован (стирание данных)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Пометить/снять отметку с раздела как сжатый (только для btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Удалить подтом" + +#, fuzzy +msgid "Delete partition" +msgstr "Удалить раздел" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Точки монтирования разделов являются относительными внутри установки, например, загрузочный будет /boot." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Недействительный каталог: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Введите начальный сектор (процент или номер блока, по умолчанию: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Введите конечный сектор раздела (процент или номер блока, например: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Пароль шифрования" @@ -859,34 +978,142 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} содержит разделы в очереди, это удалит их, вы уверены?" +msgid "Use a best-effort default partition layout" +msgstr "Стереть все выбранные диски и использовать оптимальную схему разделов по умолчанию" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Ручная конфигурация" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Отсутствует конфигурация" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Отсутствует конфигурация" + #, fuzzy msgid "Password" msgstr "Пароль root" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} содержит разделы в очереди, это удалит их, вы уверены?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Дополнительные пакеты" + +#, fuzzy +msgid "Add profile" +msgstr "Профиль" + +#, fuzzy +msgid "Edit profile" +msgstr "Профиль" + +#, fuzzy +msgid "Delete profile" +msgstr "Удалить интерфейс" + +#, fuzzy +msgid "Profile name: " +msgstr "Профиль" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "Введенное вами имя пользователя недействительно. Попробуйте еще раз" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Напишите дополнительные пакеты для установки (разделите пробелами, оставьте пустым, чтобы пропустить): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Напишите дополнительные пакеты для установки (разделите пробелами, оставьте пустым, чтобы пропустить): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Выберите графический драйвер или оставьте пустым, чтобы установить все драйверы с открытым исходным кодом" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" + +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Введите начальный сектор (процент или номер блока, по умолчанию: {}): " +#, fuzzy +msgid "Disk configuration" +msgstr "Отсутствует конфигурация" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Введите конечный сектор раздела (процент или номер блока, например: {}): " +#, fuzzy +msgid "Profiles" +msgstr "Профиль" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Выберите один или несколько жестких дисков для использования и настройте их" #, python-brace-format #~ msgid "Edit {origkey} :" #~ msgstr "Редактировать {origkey}:" -#~ msgid "Archinstall requires root privileges to run. See --help for more." -#~ msgstr "Для запуска Archinstall требуются привилегии root. Для получения дополнительной информации смотрите --help." - #~ msgid " ! Formatting {archinstall.arguments['harddrives']} in " #~ msgstr " ! Форматирование {archinstall.arguments['harddrives']} в " diff --git a/archinstall/locales/sv/LC_MESSAGES/base.po b/archinstall/locales/sv/LC_MESSAGES/base.po index 61755a2c..e114a266 100644 --- a/archinstall/locales/sv/LC_MESSAGES/base.po +++ b/archinstall/locales/sv/LC_MESSAGES/base.po @@ -846,6 +846,125 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Välj vad du vill göra med '{}'" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Välj en eller flera hårddiskar som skall användas och konfigureras" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Om du återställer hårddiskvalen kommer det också återställa diskuppsättningen. Är du säker?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Skapar en partition...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Ta bort en partition" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Välj vilken mapp du vill spara konfigurerationerna till: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Minsta kapaciteten för /home är: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Minsta kapaciteten för Arch Linux partitionen är: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Detta är en lista med förprogrammerade profiler, dom kan göra installation av exempelvis skrivbordsmiljöer lite enklare" + +#, fuzzy +msgid "Current profile selection" +msgstr "Nuvarande partioneringslayout" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Skapa en ny partition" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Välj monteringspunkt för en partition" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Markera/Avmarkera en partition för formatering (tar bort alla data)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Markera/Avmarkera en partition för komprimering (BTRFS enbart)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Ta bort användare" + +#, fuzzy +msgid "Delete partition" +msgstr "Ta bort en partition" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Partitionens monteringsplats är relativa till insidan av installationen, boot är exempelvis /boot." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Inte en giltig mapp: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Mata in startsektor (procent eller block-nummer, standard: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Mata in slutsektor för partitionen (procent eller block-nummer, ex: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Välj ett krypterings-lösenord" @@ -868,24 +987,134 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} innehåller uppköade partitionen och detta kommer ta bort dessa. Är du säker?" +msgid "Use a best-effort default partition layout" +msgstr "Töm alla partitioner och använd en generiskt rekommenderad partitionslayout" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Manuell konfiguration" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Ingen konfiguration" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Ingen konfiguration" + #, fuzzy msgid "Password" msgstr "root-lösenord" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} innehåller uppköade partitionen och detta kommer ta bort dessa. Är du säker?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Extra paket" + +#, fuzzy +msgid "Add profile" +msgstr "Profil" + +#, fuzzy +msgid "Edit profile" +msgstr "Profil" + +#, fuzzy +msgid "Delete profile" +msgstr "Ta bort interface" + +#, fuzzy +msgid "Profile name: " +msgstr "Profil" + +msgid "The profile name you entered is already in use. Try again" +msgstr "" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Skriv ytterligare paket som skall installeras (separerade med mellanslag, lämna tom för att skippa): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Skriv ytterligare paket som skall installeras (separerade med mellanslag, lämna tom för att skippa): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Välj en grafikdrivrutin eller lämna blank för att installera alla med publika drivrutiner" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Mata in startsektor (procent eller block-nummer, standard: {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Ingen konfiguration" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Mata in slutsektor för partitionen (procent eller block-nummer, ex: {}): " +#, fuzzy +msgid "Profiles" +msgstr "Profil" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Välj en eller flera hårddiskar som skall användas och konfigureras" diff --git a/archinstall/locales/ta/LC_MESSAGES/base.po b/archinstall/locales/ta/LC_MESSAGES/base.po index 4f1d6762..d430c8e5 100644 --- a/archinstall/locales/ta/LC_MESSAGES/base.po +++ b/archinstall/locales/ta/LC_MESSAGES/base.po @@ -837,6 +837,125 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "'{}'க்கான செயலைத் தேர்ந்தெடுக்கவும்" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "பயன்படுத்த மற்றும் கட்டமைக்க ஒன்று அல்லது அதற்கு மேற்பட்ட ஹார்டு டிரைவ்களைத் தேர்ந்தெடுக்கவும்" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "நீங்கள் ஹார்ட் டிரைவ் தேர்வை மீட்டமைத்தால், இது தற்போதைய வட்டு அமைப்பையும் மீட்டமைக்கும். நீங்கள் உறுதியாக இருக்கிறீர்களா?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "பகிர்வை சேர்க்கிறது...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "ஒரு பகிர்வை நீக்கவும்" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "உள்ளமைவு(களை) சேமிக்க ஒரு கோப்பகத்தை உள்ளிடவும்: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "/home பகிர்வுக்கான குறைந்த பட்ச கொள்ளளவு: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "ஆர்ச் லினக்ஸ் பகிர்வுக்கான குறைந்தபட்ச கொள்ளளவு: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "இது முன்-திட்டமிடப்பட்ட சுயவிவரங்களின் பட்டியல், அவை டெஸ்க்டாப் சூழல்கள் போன்றவற்றை நிறுவுவதை எளிதாக்கலாம்" + +#, fuzzy +msgid "Current profile selection" +msgstr "தற்போதைய பகிர்வு தளவமைப்பு" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "புதிய பகிர்வை உருவாக்கவும்" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "ஒரு பகிர்வுக்கு ஏற்ற-புள்ளியை ஒதுக்கவும்" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "குறி/குறிநீக்கு வடிவமைக்கப்பட வேண்டிய பகிர்வை (தரவை அழிக்கிறது)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "ஒரு பகிர்வை சுருக்கப்பட்டதாகக் குறிக்கவும்/குறி நீக்கவும் (btrfs மட்டும்)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "துணைத்தொகுதியை நீக்கவும்" + +#, fuzzy +msgid "Delete partition" +msgstr "ஒரு பகிர்வை நீக்கவும்" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * பகிர்வு மவுண்ட்-பாயிண்ட்கள் நிறுவலின் உள்ளே தொடர்புடையவை, துவக்கம் /boot எடுத்துக்காட்டாக இருக்கும்." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "சரியான கோப்பகம் இல்லை: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "தொடக்கப் பிரிவை உள்ளிடவும் (சதவீதம் அல்லது தொகுதி எண், இயல்புநிலை: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "பகிர்வின் இறுதிப் பகுதியை உள்ளிடவும் (சதவீதம் அல்லது தொகுதி எண், எ.கா: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "குறியாக்கம் கடவுச்சொல்" @@ -858,24 +977,135 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} வரிசைப்படுத்தப்பட்ட பகிர்வுகளைக் கொண்டுள்ளது, இது அவற்றை அகற்றும், நீங்கள் உறுதியாக இருக்கிறீர்களா?" +msgid "Use a best-effort default partition layout" +msgstr "தேர்ந்தெடுக்கப்பட்ட அனைத்து இயக்கிகளையும் துடைத்து, சிறந்த முயற்சி இயல்புநிலை பகிர்வு அமைப்பைப் பயன்படுத்தவும்" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "கைமுறை கட்டமைப்பு" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "கட்டமைப்பு இல்லை" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "கட்டமைப்பு இல்லை" + #, fuzzy msgid "Password" msgstr "ரூட் கடவுச்சொல்" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} வரிசைப்படுத்தப்பட்ட பகிர்வுகளைக் கொண்டுள்ளது, இது அவற்றை அகற்றும், நீங்கள் உறுதியாக இருக்கிறீர்களா?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "கூடுதல் தொகுப்புகள்" + +#, fuzzy +msgid "Add profile" +msgstr "சுயவிவரம்" + +#, fuzzy +msgid "Edit profile" +msgstr "சுயவிவரம்" + +#, fuzzy +msgid "Delete profile" +msgstr "இடைமுகத்தை நீக்கு" + +#, fuzzy +msgid "Profile name: " +msgstr "சுயவிவரம்" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "நீங்கள் உள்ளிட்ட பயனர்பெயர் தவறானது. மீண்டும் முயற்சிக்கவும்" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "நிறுவ கூடுதல் தொகுப்புகளை எழுதவும் (இடம் பிரிக்கப்பட்டது, தவிர்க்க காலியாக விடவும்): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "நிறுவ கூடுதல் தொகுப்புகளை எழுதவும் (இடம் பிரிக்கப்பட்டது, தவிர்க்க காலியாக விடவும்): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"கிராபிக்ஸ் இயக்கியைத் தேர்ந்தெடுக்கவும் அல்லது அனைத்து திறந்த மூல இயக்கிகளையும் நிறுவ காலியாக விடவும்" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "தொடக்கப் பிரிவை உள்ளிடவும் (சதவீதம் அல்லது தொகுதி எண், இயல்புநிலை: {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "கட்டமைப்பு இல்லை" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "பகிர்வின் இறுதிப் பகுதியை உள்ளிடவும் (சதவீதம் அல்லது தொகுதி எண், எ.கா: {}): " +#, fuzzy +msgid "Profiles" +msgstr "சுயவிவரம்" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "பயன்படுத்த மற்றும் கட்டமைக்க ஒன்று அல்லது அதற்கு மேற்பட்ட ஹார்டு டிரைவ்களைத் தேர்ந்தெடுக்கவும்" diff --git a/archinstall/locales/tr/LC_MESSAGES/base.po b/archinstall/locales/tr/LC_MESSAGES/base.po index f17efd3f..df978da8 100644 --- a/archinstall/locales/tr/LC_MESSAGES/base.po +++ b/archinstall/locales/tr/LC_MESSAGES/base.po @@ -846,6 +846,125 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "'{}' için bir eylem seçin" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Kullanmak ve yapılandırmak için bir veya daha fazla sabit disk seçin" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Eğer sabit disk seçimini sıfırlarsanız bu ayrıca mevcut disk şemasını da sıfırlayacaktır. Emin misiniz?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Disk bölümü ekleniyor…." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Disk bölümü sil" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Yapılandırma(lar)ın kaydedilmesi için bir dizin girin: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "/home disk bölümü için minimum kapasite: {}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Arch Linux disk bölümü için minimum kapasite: {}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Bu ön-programlanmış profillerin bir listesidir, bunlar masaüstü ortamları gibi şeyler kurmayı kolaylaştırabilir" + +#, fuzzy +msgid "Current profile selection" +msgstr "Mevcut disk bölümü düzeni" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Yeni disk bölümü oluştur" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Bir disk bölümü için mount (monte) noktası ata" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Bir disk bölümünü biçimlendirilmek üzere işaretle/işareti kaldır (veriyi temizler)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Bir disk bölümünü sıkıştırılmış olarak işaretle/işareti kaldır (sadece btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Kullanıcı Sil" + +#, fuzzy +msgid "Delete partition" +msgstr "Disk bölümü sil" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Disk bölümü mount (monte) noktaları iç kurulumla ilişkilidir, örnek olarak boot, /boot olacaktır." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Geçerli bir dizin değil: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Başlangıç kesimini girin (yüzde ya da blok numarası, varsayılan: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Disk bölümünün bitiş kesimini girin (yüzde ya da blok numarası, ör: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "Şifreleme şifresi" @@ -867,24 +986,134 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} işlem sırasında bekleyen disk bölümleri bulunduruyor, bu onları kaldıracak, emin misiniz?" +msgid "Use a best-effort default partition layout" +msgstr "Bütün seçilmiş diskleri temizle ve elden gelen en iyi varsayılan disk bölümü düzenini kullan" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Manuel yapılandırma" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Yapılandırma yok" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "Yapılandırma yok" + #, fuzzy msgid "Password" msgstr "Root (kök) şifresi" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} işlem sırasında bekleyen disk bölümleri bulunduruyor, bu onları kaldıracak, emin misiniz?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Ek paketler" + +#, fuzzy +msgid "Add profile" +msgstr "Profil" + +#, fuzzy +msgid "Edit profile" +msgstr "Profil" + +#, fuzzy +msgid "Delete profile" +msgstr "Arayüz sil" + +#, fuzzy +msgid "Profile name: " +msgstr "Profil" + +msgid "The profile name you entered is already in use. Try again" +msgstr "" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Kurulacak ek paketleri yazınız (boşlukla ayrılmış, geçmek için boş bırakın): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Kurulacak ek paketleri yazınız (boşlukla ayrılmış, geçmek için boş bırakın): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Bir grafik sürücüsü seçin ya da bütün açık-kaynak sürücüleri kurmak için boş bırakın" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Başlangıç kesimini girin (yüzde ya da blok numarası, varsayılan: {}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Yapılandırma yok" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Disk bölümünün bitiş kesimini girin (yüzde ya da blok numarası, ör: {}): " +#, fuzzy +msgid "Profiles" +msgstr "Profil" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Kullanmak ve yapılandırmak için bir veya daha fazla sabit disk seçin" diff --git a/archinstall/locales/uk/LC_MESSAGES/base.po b/archinstall/locales/uk/LC_MESSAGES/base.po index ee9740fc..ca019ff3 100644 --- a/archinstall/locales/uk/LC_MESSAGES/base.po +++ b/archinstall/locales/uk/LC_MESSAGES/base.po @@ -837,6 +837,126 @@ msgstr "Щоб мати можливість використовувати це msgid "The font should be stored as {}" msgstr "Шрифт слід зберігати як {}" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "Оберіть дію для \"{}\"" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "Оберіть один або кілька жорстких дисків для використання та налаштування" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "Якщо ви скинете вибір жорсткого диска, це також скине поточну схему розмітки диска. Ви впевнені?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "Розділи" + +#, fuzzy +msgid "Select a partitioning option" +msgstr "Оберіть параметр шифрування диска" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "Введіть каталог для конфігурацій, які потрібно зберегти: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "Мінімальна ємність для розділу /home: {} Гб\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Мінімальна ємність для розділу Arch Linux: {} Гб" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "Це список попередньо запрограмованих профілів, вони можуть спростити встановлення таких речей, як середовища робочого столу" + +#, fuzzy +msgid "Current profile selection" +msgstr "Поточна схема розмітки розділу" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "Створіть новий розділ" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "Призначити точку монтування для розділу" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "Позначити/зняти позначку з розділу для форматування (видалить дані)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "Позначити/зняти позначку розділу як стисненого (лише btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "Видалити підтом" + +#, fuzzy +msgid "Delete partition" +msgstr "Видалити розділ" + +#, fuzzy +msgid "Partition" +msgstr "Розділи" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * Точки монтування розділів відносяться до внутрішньої інсталяції, наприклад, завантажувач буде /boot." + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "Недійсний каталог: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "Введіть початковий сектор (відсоток або номер блоку, за замовчуванням: {}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "Введіть кінцевий сектор розділу (відсоток або номер блоку, наприклад: {}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + msgid "Encryption type" msgstr "Тип шифрування" @@ -855,23 +975,135 @@ msgstr "Оберіть параметр шифрування диска" msgid "Select a FIDO2 device to use for HSM" msgstr "Оберіть пристрій FIDO2 для використання для HSM" -msgid "All settings will be reset, are you sure?" -msgstr "Усі налаштування буде скинуто, ви впевнені?" +#, fuzzy +msgid "Use a best-effort default partition layout" +msgstr "Стерти усі вибрані диски та використовувати отпимальну схему розмітки розділів за замовчуванням" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "Розділи" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "Конфігурація відсутня" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "Шифрування розділу" + +msgid " ! Formatting {} in " +msgstr "" + +#, fuzzy +msgid "← Back" msgstr "Назад" msgid "Disk encryption" msgstr "Disk encryption" +#, fuzzy +msgid "Configuration" +msgstr "Конфігурація відсутня" + msgid "Password" msgstr "Пароль" -msgid "Partition encryption" -msgstr "Шифрування розділу" +msgid "All settings will be reset, are you sure?" +msgstr "Усі налаштування буде скинуто, ви впевнені?" + +msgid "Back" +msgstr "Назад" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "Введіть початковий сектор (відсоток або номер блоку, за замовчуванням: {}): " +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "Додаткові пакунки" + +#, fuzzy +msgid "Add profile" +msgstr "Профіль" + +#, fuzzy +msgid "Edit profile" +msgstr "Профіль" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "Введіть кінцевий сектор розділу (відсоток або номер блоку, наприклад: {}): " +#, fuzzy +msgid "Delete profile" +msgstr "Видалити інтерфейс" + +#, fuzzy +msgid "Profile name: " +msgstr "Профіль" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "Введене вами ім'я користувача недійсне. Спробуйте знову" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "Напишіть додаткові пакети для інсталяції (розділені пробілами, залиште порожнім, щоб пропустити): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "Напишіть додаткові пакети для інсталяції (розділені пробілами, залиште порожнім, щоб пропустити): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"Оберіть графічний драйвер або залиште поле пустим, щоб установити всі драйвери з відкритим вихідним кодом" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" + +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "Конфігурація відсутня" + +#, fuzzy +msgid "Profiles" +msgstr "Профіль" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "Оберіть один або кілька жорстких дисків для використання та налаштування" diff --git a/archinstall/locales/ur/LC_MESSAGES/base.po b/archinstall/locales/ur/LC_MESSAGES/base.po index 4e3d7a10..1a715d90 100644 --- a/archinstall/locales/ur/LC_MESSAGES/base.po +++ b/archinstall/locales/ur/LC_MESSAGES/base.po @@ -867,6 +867,122 @@ msgstr "" msgid "The font should be stored as {}" msgstr "" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "'{}' کے لیے ایک عمل منتخب کریں" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "استعمال کرنے اور کنفیگر کے لیے ایک یا زیادہ ہارڈ ڈرائیوز منتخب کریں" + +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "" + +#, fuzzy +msgid "Existing Partitions" +msgstr "پارٹیشن شامل ہو رہی ہے..." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "ایک پارٹیشن کو حذف کریں" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "محفوظ کیے جانے والے کنفیگریشن کے لیے ایک ڈائرکٹری درج کریں:" + +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "" + +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "یہ پہلے سے پروگرام شدہ پروفائلز کی فہرست ہے، وہ ڈیسک ٹاپ انسٹالیشن جیسی چیزوں کو آسان بناتے ہیں" + +#, fuzzy +msgid "Current profile selection" +msgstr "موجودہ پارٹیشن کی ترتیب" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "ایک نیا پارٹیشن بنائیں" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "پارٹیشن کے لیے ماؤنٹ پوائنٹ تفویض کریں" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "فارمیٹ کرنے کے لیے پارٹیشن کو مارک​​ /انمارک کریں (ڈیٹا صاف کرتا ہے)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "انکرپٹڈ کرنے کے لیے پارٹیشن کو مارک​​ /انمارک کریں" + +#, fuzzy +msgid "Set subvolumes" +msgstr "صارف کو حذف کریں" + +#, fuzzy +msgid "Delete partition" +msgstr "ایک پارٹیشن کو حذف کریں" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr "* پارٹیشن ماؤنٹ پوائنٹس انسٹالیشن کی نسبت سے ہیں، بوٹ مثال کے طور /boot پرہوگا۔" + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "درست ڈائریکٹری نہیں ہے: {}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "اسٹارٹ سیکٹر درج کریں (فیصد یا بلاک نمبر، ڈیفالٹ: {}):" + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "پارٹیشن کا آخری سیکٹر درج کریں (فیصد یا بلاک نمبر، مثال کے طور پر: {}):" + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "انکرپشن پاس ورڈ سیٹ کریں" @@ -889,27 +1005,138 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} پارٹیشنزکے گروپ پر مشتمل ہے، یہ ان کو مٹا دے گا، کیا آپ پر اعتماد ہیں؟" +msgid "Use a best-effort default partition layout" +msgstr "تمام منتخب ڈرائیوز کو صاف کریں اور ایک بہترین ڈیفالٹ پارٹیشن لے آؤٹ استعمال کریں" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "ترتیب کو محفوظ کریں" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "کوئی کنفیگریشن نہیں" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "کوئی کنفیگریشن نہیں" + #, fuzzy msgid "Password" msgstr "روٹ پاس ورڈ" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} پارٹیشنزکے گروپ پر مشتمل ہے، یہ ان کو مٹا دے گا، کیا آپ پر اعتماد ہیں؟" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "اسٹارٹ سیکٹر درج کریں (فیصد یا بلاک نمبر، ڈیفالٹ: {}):" +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "اضافی پیکجز" + +#, fuzzy +msgid "Add profile" +msgstr "پروفائل" + +#, fuzzy +msgid "Edit profile" +msgstr "پروفائل" + +#, fuzzy +msgid "Delete profile" +msgstr "صارف کو حذف کریں" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "پارٹیشن کا آخری سیکٹر درج کریں (فیصد یا بلاک نمبر، مثال کے طور پر: {}):" +#, fuzzy +msgid "Profile name: " +msgstr "پروفائل" + +msgid "The profile name you entered is already in use. Try again" +msgstr "" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "انسٹال کرنےکے لیے اضافی پیکجز لکھیں (الگ الگ لیکھیں، نہیں کی صورت میں خالی چھوڑیں):" + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "انسٹال کرنےکے لیے اضافی پیکجز لکھیں (الگ الگ لیکھیں، نہیں کی صورت میں خالی چھوڑیں):" + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"\n" +"ایک گرافکس ڈرائیور منتخب کریں یا تمام اوپن سورس ڈرائیورز کو انسٹال کرنے کے لیے خالی چھوڑ دیں" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" +msgstr "" + +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "کوئی کنفیگریشن نہیں" + +#, fuzzy +msgid "Profiles" +msgstr "پروفائل" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "استعمال کرنے اور کنفیگر کے لیے ایک یا زیادہ ہارڈ ڈرائیوز منتخب کریں" #~ msgid "Add :" #~ msgstr "شامل:" diff --git a/archinstall/locales/zh-CN/LC_MESSAGES/base.po b/archinstall/locales/zh-CN/LC_MESSAGES/base.po index d957204b..f1cee304 100644 --- a/archinstall/locales/zh-CN/LC_MESSAGES/base.po +++ b/archinstall/locales/zh-CN/LC_MESSAGES/base.po @@ -836,6 +836,125 @@ msgstr "为了能够使用此翻译,请手动安装支持该语言的字体。 msgid "The font should be stored as {}" msgstr "字体应存储为 {}" +msgid "Archinstall requires root privileges to run. See --help for more." +msgstr "" + +#, fuzzy +msgid "Select an execution mode" +msgstr "为“{}”选择一个操作" + +msgid "Unable to fetch profile from specified url: {}" +msgstr "" + +msgid "Profiles must have unique name, but profile definitions with duplicate name found: {}" +msgstr "" + +#, fuzzy +msgid "Select one or more devices to use and configure" +msgstr "选择一个或多个硬盘驱动器来使用和配置" + +#, fuzzy +msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" +msgstr "如果您重置硬盘驱动器选择,这也将重置当前磁盘布局。 你确定吗?" + +#, fuzzy +msgid "Existing Partitions" +msgstr "添加分区...." + +#, fuzzy +msgid "Select a partitioning option" +msgstr "删除一个分区" + +#, fuzzy +msgid "Enter the root directory of the mounted devices: " +msgstr "输入要保存配置的目录: " + +#, fuzzy +msgid "Minimum capacity for /home partition: {}GiB\n" +msgstr "/home 分区的最小容量:{}GB\n" + +#, fuzzy +msgid "Minimum capacity for Arch Linux partition: {}GiB" +msgstr "Arch Linux 分区的最小容量:{}GB" + +#, fuzzy +msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" +msgstr "这是预编程配置文件的列表,它们可能使安装桌面环境之类的东西变得更容易" + +#, fuzzy +msgid "Current profile selection" +msgstr "当前分区布局" + +#, fuzzy +msgid "Remove all newly added partitions" +msgstr "创建新分区" + +#, fuzzy +msgid "Assign mountpoint" +msgstr "为分区分配挂载点" + +#, fuzzy +msgid "Mark/Unmark to be formatted (wipes data)" +msgstr "标记/取消标记要格式化的分区(擦除数据)" + +msgid "Mark/Unmark as bootable" +msgstr "" + +msgid "Change filesystem" +msgstr "" + +#, fuzzy +msgid "Mark/Unmark as compressed" +msgstr "将分区标记/取消标记为压缩(仅限 btrfs)" + +#, fuzzy +msgid "Set subvolumes" +msgstr "删除子卷" + +#, fuzzy +msgid "Delete partition" +msgstr "删除一个分区" + +msgid "Partition" +msgstr "" + +msgid "This partition is currently encrypted, to format it a filesystem has to be specified" +msgstr "" + +#, fuzzy +msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." +msgstr " * 分区挂载点是相对于安装内部的,例如 boot 应该为 /boot。" + +msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." +msgstr "" + +msgid "Mountpoint: " +msgstr "" + +msgid "Current free sectors on device {}:" +msgstr "" + +#, fuzzy +msgid "Total sectors: {}" +msgstr "不是有效的目录:{}" + +#, fuzzy +msgid "Enter the start sector (default: {}): " +msgstr "输入起始扇区(百分比或块号,默认:{}): " + +#, fuzzy +msgid "Enter the end sector of the partition (percentage or block number, default: {}): " +msgstr "输入分区的结束扇区(百分比或块号,例如:{}): " + +msgid "This will remove all newly added partitions, continue?" +msgstr "" + +msgid "Partition management: {}" +msgstr "" + +msgid "Total length: {}" +msgstr "" + #, fuzzy msgid "Encryption type" msgstr "加密密码" @@ -857,24 +976,135 @@ msgid "Select a FIDO2 device to use for HSM" msgstr "" #, fuzzy -msgid "All settings will be reset, are you sure?" -msgstr "{} 包含排队分区,这将删除这些分区,您确定吗?" +msgid "Use a best-effort default partition layout" +msgstr "擦除所有选定的驱动器并使用最优的默认分区布局" -msgid "Back" +#, fuzzy +msgid "Manual Partitioning" +msgstr "手动配置" + +#, fuzzy +msgid "Pre-mounted configuration" +msgstr "无配置" + +msgid "Unknown" +msgstr "" + +msgid "Partition encryption" +msgstr "" + +msgid " ! Formatting {} in " +msgstr "" + +msgid "← Back" msgstr "" msgid "Disk encryption" msgstr "" +#, fuzzy +msgid "Configuration" +msgstr "无配置" + #, fuzzy msgid "Password" msgstr "Root 密码" -msgid "Partition encryption" +#, fuzzy +msgid "All settings will be reset, are you sure?" +msgstr "{} 包含排队分区,这将删除这些分区,您确定吗?" + +msgid "Back" +msgstr "" + +msgid "Please chose which greeter to install for the chosen profiles: {}" +msgstr "" + +msgid "Environment type: {}" +msgstr "" + +msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?" +msgstr "" + +#, fuzzy +msgid "Installed packages" +msgstr "附加包" + +#, fuzzy +msgid "Add profile" +msgstr "配置文件" + +#, fuzzy +msgid "Edit profile" +msgstr "配置文件" + +#, fuzzy +msgid "Delete profile" +msgstr "删除接口" + +#, fuzzy +msgid "Profile name: " +msgstr "配置文件" + +#, fuzzy +msgid "The profile name you entered is already in use. Try again" +msgstr "您输入的用户名无效。 再试一次" + +#, fuzzy +msgid "Packages to be install with this profile (space separated, leave blank to skip): " +msgstr "编写要安装的附加软件包(空格分隔,留空跳过): " + +#, fuzzy +msgid "Services to be enabled with this profile (space separated, leave blank to skip): " +msgstr "编写要安装的附加软件包(空格分隔,留空跳过): " + +msgid "Should this profile be enabled for installation?" +msgstr "" + +msgid "Create your own" +msgstr "" + +#, fuzzy +msgid "" +"\n" +"Select a graphics driver or leave blank to install all open-source drivers" +msgstr "" +"\n" +"\n" +"选择图形驱动程序或留空以安装所有开源驱动程序" + +msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" msgstr "" -#~ msgid "Enter the start sector (percentage or block number, default: {}): " -#~ msgstr "输入起始扇区(百分比或块号,默认:{}): " +msgid "" +"\n" +"\n" +"Choose an option to give Sway access to your hardware" +msgstr "" + +msgid "Graphics driver" +msgstr "" + +msgid "Greeter" +msgstr "" + +msgid "Please chose which greeter to install" +msgstr "" + +msgid "This is a list of pre-programmed default_profiles" +msgstr "" + +#, fuzzy +msgid "Disk configuration" +msgstr "无配置" -#~ msgid "Enter the end sector of the partition (percentage or block number, ex: {}): " -#~ msgstr "输入分区的结束扇区(百分比或块号,例如:{}): " +#, fuzzy +msgid "Profiles" +msgstr "配置文件" + +msgid "Finding possible directories to save configuration files ..." +msgstr "" + +#, fuzzy +msgid "Select directory (or directories) for saving configuration files" +msgstr "选择一个或多个硬盘驱动器来使用和配置" diff --git a/archinstall/profiles b/archinstall/profiles deleted file mode 120000 index c2968eea..00000000 --- a/archinstall/profiles +++ /dev/null @@ -1 +0,0 @@ -../profiles/ \ No newline at end of file diff --git a/archinstall/scripts/__init__.py b/archinstall/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py new file mode 100644 index 00000000..d9c5837c --- /dev/null +++ b/archinstall/scripts/guided.py @@ -0,0 +1,276 @@ +import logging +import os +from pathlib import Path +from typing import Any, TYPE_CHECKING + +import archinstall +from archinstall.lib import disk +from archinstall.lib.global_menu import GlobalMenu +from archinstall.default_profiles.applications.pipewire import PipewireProfile +from archinstall.lib.configuration import ConfigurationOutput +from archinstall.lib.installer import Installer +from archinstall.lib.menu import Menu +from archinstall.lib.mirrors import use_mirrors +from archinstall.lib.models.bootloader import Bootloader +from archinstall.lib.models.network_configuration import NetworkConfigurationHandler +from archinstall.lib.output import log +from archinstall.lib.profile.profiles_handler import profile_handler + +if TYPE_CHECKING: + _: Any + + +if archinstall.arguments.get('help'): + print("See `man archinstall` for help.") + exit(0) + +if os.getuid() != 0: + print(_("Archinstall requires root privileges to run. See --help for more.")) + exit(1) + +# Log various information about hardware before starting the installation. This might assist in troubleshooting +archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) +archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) +archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) +archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) +archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) + +# For support reasons, we'll log the disk layout pre installation to match against post-installation layout +archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +def ask_user_questions(): + """ + First, we'll ask the user for a bunch of user input. + Not until we're satisfied with what we want to install + will we continue with the actual installation steps. + """ + + # ref: https://github.com/archlinux/archinstall/pull/831 + # we'll set NTP to true by default since this is also + # the default value specified in the menu options; in + # case it will be changed by the user we'll also update + # the system immediately + global_menu = GlobalMenu(data_store=archinstall.arguments) + + global_menu.enable('archinstall-language') + + global_menu.enable('keyboard-layout') + + # Set which region to download packages from during the installation + global_menu.enable('mirror-region') + + global_menu.enable('sys-language') + + global_menu.enable('sys-encoding') + + global_menu.enable('disk_config', mandatory=True) + + # Specify disk encryption options + global_menu.enable('disk_encryption') + + # Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB) + global_menu.enable('bootloader') + + global_menu.enable('swap') + + # Get the hostname for the machine + global_menu.enable('hostname') + + # Ask for a root password (optional, but triggers requirement for super-user if skipped) + global_menu.enable('!root-password', mandatory=True) + + global_menu.enable('!users', mandatory=True) + + # Ask for archinstall-specific profiles_bck (such as desktop environments etc) + global_menu.enable('profile_config') + + # Ask about audio server selection if one is not already set + global_menu.enable('audio') + + # Ask for preferred kernel: + global_menu.enable('kernels') + + global_menu.enable('packages') + + if archinstall.arguments.get('advanced', False): + # Enable parallel downloads + global_menu.enable('parallel downloads') + + # Ask or Call the helper function that asks the user to optionally configure a network. + global_menu.enable('nic') + + global_menu.enable('timezone') + + global_menu.enable('ntp') + + global_menu.enable('additional-repositories') + + global_menu.enable('__separator__') + + global_menu.enable('save_config') + global_menu.enable('install') + global_menu.enable('abort') + + global_menu.run() + + +def perform_installation(mountpoint: Path): + """ + Performs the installation steps on a block device. + Only requirement is that the block devices are + formatted and setup prior to entering this function. + """ + log('Starting installation', level=logging.INFO) + disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + + # Retrieve list of additional repositories and set boolean values appropriately + enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) + enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) + + locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" + + disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) as installation: + # Mount all the drives to the desired mountpoint + if disk_config.config_type != disk.DiskLayoutType.Pre_mount: + installation.mount_ordered_layout() + + installation.sanity_check() + + if disk_config.config_type != disk.DiskLayoutType.Pre_mount: + if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption: + # generate encryption key files for the mounted luks devices + installation.generate_key_files() + + if archinstall.arguments.get('ntp', False): + installation.activate_ntp() + + # Set mirrors used by pacstrap (outside of installation) + if archinstall.arguments.get('mirror-region', None): + use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + + installation.minimal_installation( + testing=enable_testing, + multilib=enable_multilib, + hostname=archinstall.arguments.get('hostname', 'archlinux'), + locales=[locale] + ) + + if archinstall.arguments.get('mirror-region') is not None: + if archinstall.arguments.get("mirrors", None) is not None: + installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + + if archinstall.arguments.get('swap'): + installation.setup_swap('zram') + + if archinstall.arguments.get("bootloader") == Bootloader.Grub and archinstall.has_uefi(): + installation.add_additional_packages("grub") + + installation.add_bootloader(archinstall.arguments["bootloader"]) + + # If user selected to copy the current ISO network configuration + # Perform a copy of the config + network_config = archinstall.arguments.get('nic', None) + + if network_config: + handler = NetworkConfigurationHandler(network_config) + handler.config_installer(installation) + + if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': + installation.add_additional_packages(archinstall.arguments.get('packages', None)) + + if users := archinstall.arguments.get('!users', None): + installation.create_users(users) + + if audio := archinstall.arguments.get('audio', None): + log(f'Installing audio server: {audio}', level=logging.INFO) + if audio == 'pipewire': + PipewireProfile().install(installation) + elif audio == 'pulseaudio': + installation.add_additional_packages("pulseaudio") + else: + installation.log("No audio server will be installed.", level=logging.INFO) + + if profile_config := archinstall.arguments.get('profile_config', None): + profile_handler.install_profile_config(installation, profile_config) + + if timezone := archinstall.arguments.get('timezone', None): + installation.set_timezone(timezone) + + if archinstall.arguments.get('ntp', False): + installation.activate_time_syncronization() + + if archinstall.accessibility_tools_in_use(): + installation.enable_espeakup() + + if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): + installation.user_set_pw('root', root_pw) + + # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # After which, this step will set the language both for console and x11 if x11 was installed for instance. + installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + + if profile_config := archinstall.arguments.get('profile_config', None): + profile_config.profile.post_install(installation) + + # If the user provided a list of services to be enabled, pass the list to the enable_service function. + # Note that while it's called enable_service, it can actually take a list of services and iterate it. + if archinstall.arguments.get('services', None): + installation.enable_service(*archinstall.arguments['services']) + + # If the user provided custom commands to be run post-installation, execute them now. + if archinstall.arguments.get('custom-commands', None): + archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation) + + installation.genfstab() + + installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") + + if not archinstall.arguments.get('silent'): + prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run() + if choice.value == Menu.yes(): + try: + installation.drop_to_shell() + except: + pass + + archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +if archinstall.arguments.get('skip-mirror-check', False) is False and archinstall.check_mirror_reachable() is False: + log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) + archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + exit(1) + +if not archinstall.arguments.get('silent'): + ask_user_questions() + +config_output = ConfigurationOutput(archinstall.arguments) + +if not archinstall.arguments.get('silent'): + config_output.show() + +config_output.save() + +if archinstall.arguments.get('dry_run'): + exit(0) + +if not archinstall.arguments.get('silent'): + input(str(_('Press Enter to continue.'))) + +fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) +) + +fs_handler.perform_filesystem_operations() + +perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt'))) diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py new file mode 100644 index 00000000..0cdbdcef --- /dev/null +++ b/archinstall/scripts/minimal.py @@ -0,0 +1,104 @@ +from pathlib import Path +from typing import TYPE_CHECKING, Any, List + +import archinstall +from archinstall import ConfigurationOutput, Installer, ProfileConfiguration, profile_handler +from archinstall.default_profiles.minimal import MinimalProfile +from archinstall import disk +from archinstall import models +from archinstall.lib.user_interaction.disk_conf import select_devices, suggest_single_disk_layout + +if TYPE_CHECKING: + _: Any + + +archinstall.log("Minimal only supports:") +archinstall.log(" * Being installed to a single disk") + +if archinstall.arguments.get('help', None): + archinstall.log(" - Optional disk encryption via --!encryption-password=") + archinstall.log(" - Optional filesystem type via --filesystem=") + archinstall.log(" - Optional systemd network via --network") + + +def perform_installation(mountpoint: Path): + disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) as installation: + # Strap in the base system, add a boot loader and configure + # some other minor details as specified by this profile and user. + if installation.minimal_installation(): + installation.set_hostname('minimal-arch') + installation.add_bootloader(models.Bootloader.Systemd) + + # Optionally enable networking: + if archinstall.arguments.get('network', None): + installation.copy_iso_network_config(enable_services=True) + + installation.add_additional_packages(['nano', 'wget', 'git']) + + profile_config = ProfileConfiguration(MinimalProfile()) + profile_handler.install_profile_config(installation, profile_config) + + user = models.User('devel', 'devel', False) + installation.create_users(user) + + # Once this is done, we output some useful information to the user + # And the installation is complete. + archinstall.log("There are two new accounts in your installation after reboot:") + archinstall.log(" * root (password: airoot)") + archinstall.log(" * devel (password: devel)") + + +def prompt_disk_layout(): + fs_type = None + if filesystem := archinstall.arguments.get('filesystem', None): + fs_type = disk.FilesystemType(filesystem) + + devices = select_devices() + modifications = suggest_single_disk_layout(devices[0], filesystem_type=fs_type) + + archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Default, + device_modifications=[modifications] + ) + + +def parse_disk_encryption(): + if enc_password := archinstall.arguments.get('!encryption-password', None): + modification: List[disk.DeviceModification] = archinstall.arguments['disk_config'] + partitions: List[disk.PartitionModification] = [] + + # encrypt all partitions except the /boot + for mod in modification: + partitions += list(filter(lambda x: x.mountpoint != Path('/boot'), mod.partitions)) + + archinstall.arguments['disk_encryption'] = disk.DiskEncryption( + encryption_type=disk.EncryptionType.Partition, + encryption_password=enc_password, + partitions=partitions + ) + + +prompt_disk_layout() +parse_disk_encryption() + +config_output = ConfigurationOutput(archinstall.arguments) +config_output.show() + +input(str(_('Press Enter to continue.'))) + +fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) +) + +fs_handler.perform_filesystem_operations() + +perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt'))) diff --git a/archinstall/scripts/only_hd.py b/archinstall/scripts/only_hd.py new file mode 100644 index 00000000..a903c5fe --- /dev/null +++ b/archinstall/scripts/only_hd.py @@ -0,0 +1,104 @@ +import logging +import os +from pathlib import Path + +import archinstall +from archinstall import Installer +from archinstall.lib.configuration import ConfigurationOutput +from archinstall import disk + +if archinstall.arguments.get('help'): + print("See `man archinstall` for help.") + exit(0) + + +if os.getuid() != 0: + print("Archinstall requires root privileges to run. See --help for more.") + exit(1) + + +def ask_user_questions(): + global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments) + + global_menu.enable('archinstall-language') + + global_menu.enable('disk_config', mandatory=True) + global_menu.enable('disk_encryption') + global_menu.enable('swap') + + global_menu.enable('save_config') + global_menu.enable('install') + global_menu.enable('abort') + + global_menu.run() + + +def perform_installation(mountpoint: Path): + """ + Performs the installation steps on a block device. + Only requirement is that the block devices are + formatted and setup prior to entering this function. + """ + disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) as installation: + # Mount all the drives to the desired mountpoint + # This *can* be done outside of the installation, but the installer can deal with it. + if archinstall.arguments.get('disk_config'): + installation.mount_ordered_layout() + + # to generate a fstab directory holder. Avoids an error on exit and at the same time checks the procedure + target = Path(f"{mountpoint}/etc/fstab") + if not target.parent.exists(): + target.parent.mkdir(parents=True) + + # For support reasons, we'll log the disk layout post installation (crash or no crash) + archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +# Log various information about hardware before starting the installation. This might assist in troubleshooting +archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) +archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) +archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) +archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) +archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) + +# For support reasons, we'll log the disk layout pre installation to match against post-installation layout +archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +if archinstall.arguments.get('skip-mirror-check', False) is False and archinstall.check_mirror_reachable() is False: + log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) + archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + exit(1) + +if not archinstall.arguments.get('silent'): + ask_user_questions() + +config_output = ConfigurationOutput(archinstall.arguments) +if not archinstall.arguments.get('silent'): + config_output.show() + +config_output.save() + +if archinstall.arguments.get('dry_run'): + exit(0) + +if not archinstall.arguments.get('silent'): + input('Press Enter to continue.') + +fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) +) + +fs_handler.perform_filesystem_operations() + + +perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt'))) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py new file mode 100644 index 00000000..e2ee6fcb --- /dev/null +++ b/archinstall/scripts/swiss.py @@ -0,0 +1,353 @@ +import logging +import os +from enum import Enum +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict + +import archinstall +from archinstall.lib.mirrors import use_mirrors +from archinstall import models +from archinstall import disk +from archinstall.lib.profile.profiles_handler import profile_handler +from archinstall import menu +from archinstall.lib.global_menu import GlobalMenu +from archinstall.lib.output import log +from archinstall import Installer +from archinstall.lib.configuration import ConfigurationOutput +from archinstall.default_profiles.applications.pipewire import PipewireProfile + +if TYPE_CHECKING: + _: Any + + +if archinstall.arguments.get('help'): + print("See `man archinstall` for help.") + exit(0) + + +if os.getuid() != 0: + print("Archinstall requires root privileges to run. See --help for more.") + exit(1) + + +class ExecutionMode(Enum): + Full = 'full' + Lineal = 'lineal' + Only_HD = 'only-hd' + Only_OS = 'only-os' + Minimal = 'minimal' + + +def select_mode() -> ExecutionMode: + options = [str(e.value) for e in ExecutionMode] + choice = menu.Menu( + str(_('Select an execution mode')), + options, + default_option=ExecutionMode.Full.value, + skip=False + ).run() + + return ExecutionMode(choice.single_value) + + +class SetupMenu(GlobalMenu): + def __init__(self, storage_area: Dict[str, Any]): + super().__init__(data_store=storage_area) + + def setup_selection_menu_options(self): + super().setup_selection_menu_options() + + self._menu_options['mode'] = menu.Selector( + 'Excution mode', + lambda x : select_mode(), + display_func=lambda x: x.value if x else '', + default=ExecutionMode.Full) + + self._menu_options['continue'] = menu.Selector( + 'Continue', + exec_func=lambda n,v: True) + + self.enable('archinstall-language') + self.enable('ntp') + self.enable('mode') + self.enable('continue') + self.enable('abort') + + def exit_callback(self): + if self._data_store.get('ntp', False): + archinstall.SysCommand('timedatectl set-ntp true') + + if self._data_store.get('mode', None): + archinstall.arguments['mode'] = self._data_store['mode'] + log(f"Archinstall will execute under {archinstall.arguments['mode']} mode") + + +class SwissMainMenu(GlobalMenu): + def __init__( + self, + data_store: Dict[str, Any], + exec_mode: ExecutionMode = ExecutionMode.Full + ): + self._execution_mode = exec_mode + super().__init__(data_store) + + def setup_selection_menu_options(self): + super().setup_selection_menu_options() + + options_list = [] + mandatory_list = [] + + match self._execution_mode: + case ExecutionMode.Full | ExecutionMode.Lineal: + options_list = [ + 'keyboard-layout', 'mirror-region', 'disk_config', + 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', + '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', + 'timezone', 'ntp' + ] + + if archinstall.arguments.get('advanced', False): + options_list.extend(['sys-language', 'sys-encoding']) + + mandatory_list = ['disk_config', 'bootloader', 'hostname'] + case ExecutionMode.Only_HD: + options_list = ['disk_config', 'disk_encryption','swap'] + mandatory_list = ['disk_config'] + case ExecutionMode.Only_OS: + options_list = [ + 'keyboard-layout', 'mirror-region','bootloader', 'hostname', + '!root-password', '!users', 'profile_config', 'audio', 'kernels', + 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' + ] + + mandatory_list = ['hostname'] + + if archinstall.arguments.get('advanced', False): + options_list += ['sys-language','sys-encoding'] + case ExecutionMode.Minimal: + pass + case _: + archinstall.log(f' Execution mode {self._execution_mode} not supported') + exit(1) + + if self._execution_mode != ExecutionMode.Lineal: + options_list += ['save_config', 'install'] + + if not archinstall.arguments.get('advanced', False): + options_list.append('archinstall-language') + + options_list += ['abort'] + + for entry in mandatory_list: + self.enable(entry, mandatory=True) + + for entry in options_list: + self.enable(entry) + + +def ask_user_questions(exec_mode: ExecutionMode = ExecutionMode.Full): + """ + First, we'll ask the user for a bunch of user input. + Not until we're satisfied with what we want to install + will we continue with the actual installation steps. + """ + if archinstall.arguments.get('advanced', None): + setup_area: Dict[str, Any] = {} + setup = SetupMenu(setup_area) + + if exec_mode == ExecutionMode.Lineal: + for entry in setup.list_enabled_options(): + if entry in ('continue', 'abort'): + continue + if not setup.option(entry).enabled: + continue + setup.exec_option(entry) + else: + setup.run() + + archinstall.arguments['archinstall-language'] = setup_area.get('archinstall-language') + + with SwissMainMenu(data_store=archinstall.arguments, exec_mode=exec_mode) as menu: + if mode == ExecutionMode.Lineal: + for entry in menu.list_enabled_options(): + if entry in ('install', 'abort'): + continue + menu.exec_option(entry) + archinstall.arguments[entry] = menu.option(entry).get_selection() + else: + menu.run() + + +def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): + disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + + enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) + enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) + + locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) as installation: + if exec_mode in [ExecutionMode.Full, ExecutionMode.Only_HD]: + installation.mount_ordered_layout() + + installation.sanity_check() + + if disk_config.config_type != disk.DiskLayoutType.Pre_mount: + if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption: + # generate encryption key files for the mounted luks devices + installation.generate_key_files() + + if archinstall.arguments.get('ntp', False): + installation.activate_ntp() + + # Set mirrors used by pacstrap (outside of installation) + if archinstall.arguments.get('mirror-region', None): + use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + + installation.minimal_installation( + testing=enable_testing, + multilib=enable_multilib, + hostname=archinstall.arguments.get('hostname', 'archlinux'), + locales=[locale] + ) + + if archinstall.arguments.get('mirror-region') is not None: + if archinstall.arguments.get("mirrors", None) is not None: + installation.set_mirrors( + archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + + if archinstall.arguments.get('swap'): + installation.setup_swap('zram') + + if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and archinstall.has_uefi(): + installation.add_additional_packages("grub") + + installation.add_bootloader(archinstall.arguments["bootloader"]) + + # If user selected to copy the current ISO network configuration + # Perform a copy of the config + network_config = archinstall.arguments.get('nic', None) + + if network_config: + handler = models.NetworkConfigurationHandler(network_config) + handler.config_installer(installation) + + if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': + installation.add_additional_packages(archinstall.arguments.get('packages', None)) + + if users := archinstall.arguments.get('!users', None): + installation.create_users(users) + + if audio := archinstall.arguments.get('audio', None): + log(f'Installing audio server: {audio}', level=logging.INFO) + if audio == 'pipewire': + PipewireProfile().install(installation) + elif audio == 'pulseaudio': + installation.add_additional_packages("pulseaudio") + else: + installation.log("No audio server will be installed.", level=logging.INFO) + + if profile_config := archinstall.arguments.get('profile_config', None): + profile_handler.install_profile_config(installation, profile_config) + + if timezone := archinstall.arguments.get('timezone', None): + installation.set_timezone(timezone) + + if archinstall.arguments.get('ntp', False): + installation.activate_time_syncronization() + + if archinstall.accessibility_tools_in_use(): + installation.enable_espeakup() + + if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): + installation.user_set_pw('root', root_pw) + + # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # After which, this step will set the language both for console and x11 if x11 was installed for instance. + installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + + if profile_config := archinstall.arguments.get('profile_config', None): + profile_config.profile.post_install(installation) + + # If the user provided a list of services to be enabled, pass the list to the enable_service function. + # Note that while it's called enable_service, it can actually take a list of services and iterate it. + if archinstall.arguments.get('services', None): + installation.enable_service(*archinstall.arguments['services']) + + # If the user provided custom commands to be run post-installation, execute them now. + if archinstall.arguments.get('custom-commands', None): + archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation) + + installation.genfstab() + + installation.log( + "For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", + fg="yellow") + + if not archinstall.arguments.get('silent'): + prompt = str( + _('Would you like to chroot into the newly created installation and perform post-installation configuration?')) + choice = menu.Menu(prompt, menu.Menu.yes_no(), default_option=menu.Menu.yes()).run() + if choice.value == menu.Menu.yes(): + try: + installation.drop_to_shell() + except: + pass + + archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +# Log various information about hardware before starting the installation. This might assist in troubleshooting +archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) +archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) +archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) +archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) +archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) + +# For support reasons, we'll log the disk layout pre installation to match against post-installation layout +archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +if not archinstall.check_mirror_reachable(): + log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) + archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + exit(1) + +param_mode = archinstall.arguments.get('mode', ExecutionMode.Full.value).lower() + +try: + mode = ExecutionMode(param_mode) +except KeyError: + log(f'Mode "{param_mode}" is not supported') + exit(1) + +if not archinstall.arguments.get('silent'): + ask_user_questions(mode) + +config_output = ConfigurationOutput(archinstall.arguments) +if not archinstall.arguments.get('silent'): + config_output.show() + +config_output.save() + +if archinstall.arguments.get('dry_run'): + exit(0) + +if not archinstall.arguments.get('silent'): + input('Press Enter to continue.') + +if mode in (ExecutionMode.Full, ExecutionMode.Only_HD): + fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) + ) + + fs_handler.perform_filesystem_operations() + +perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt')), mode) diff --git a/archinstall/scripts/unattended.py b/archinstall/scripts/unattended.py new file mode 100644 index 00000000..0a1c5160 --- /dev/null +++ b/archinstall/scripts/unattended.py @@ -0,0 +1,18 @@ +import time + +import archinstall +from archinstall.lib.profile.profiles_handler import profile_handler + +for profile in profile_handler.get_mac_addr_profiles(): + # Tailored means it's a match for this machine + # based on it's MAC address (or some other criteria + # that fits the requirements for this machine specifically). + archinstall.log(f'Found a tailored profile for this machine called: "{profile.name}"') + + print('Starting install in:') + for i in range(10, 0, -1): + print(f'{i}...') + time.sleep(1) + + install_session = archinstall.storage['installation_session'] + profile.install(install_session) diff --git a/build_iso.sh b/build_iso.sh new file mode 100755 index 00000000..74e815ee --- /dev/null +++ b/build_iso.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +zprofile="/tmp/archlive/airootfs/root/.zprofile" + +mkdir -p /tmp/archlive/airootfs/root/archinstall-git +cp -r . /tmp/archlive/airootfs/root/archinstall-git + +echo "pip uninstall archinstall -y" > $zprofile +echo "cd archinstall-git" >> $zprofile +echo "rm -rf dist" >> $zprofile + +echo "python -m build --wheel --no-isolation" >> $zprofile +echo "pip install dist/archinstall*.whl" >> $zprofile + +echo "echo \"This is an unofficial ISO for development and testing of archinstall. No support will be provided.\"" >> $zprofile +echo "echo \"This ISO was built from Git SHA $GITHUB_SHA\"" >> $zprofile +echo "echo \"Type archinstall to launch the installer.\"" >> $zprofile + +cat $zprofile + +pacman -Sy +pacman --noconfirm -S git archiso + +cp -r /usr/share/archiso/configs/releng/* /tmp/archlive + +echo -e "git\npython\npython-pip\npython-build\npython-flit\npython-setuptools\npython-wheel\npython-pyparted" >> /tmp/archlive/packages.x86_64 + +find /tmp/archlive +cd /tmp/archlive + +mkarchiso -v -w work/ -o out/ ./ diff --git a/docs/examples/python.rst b/docs/examples/python.rst index dfc0abb3..b5478fe2 100644 --- a/docs/examples/python.rst +++ b/docs/examples/python.rst @@ -34,11 +34,11 @@ To do this, we'll begin by importing `archinstall` in our `./archinstall/example .. code-block:: python import archinstall - + all_drives = archinstall.all_blockdevices(partitions=False) print(list(all_drives.keys())) -This should print out a list of drives. +This should print out a list of drives and some meta-information about them. As an example, this will do just fine. Now, go ahead and install the library either as a user-module or system-wide. diff --git a/examples/__init__.py b/examples/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/auto_discovery_mounted.py b/examples/auto_discovery_mounted.py new file mode 100644 index 00000000..0bd30cd1 --- /dev/null +++ b/examples/auto_discovery_mounted.py @@ -0,0 +1,13 @@ +from pathlib import Path + +from archinstall import disk + +root_mount_dir = Path('/mnt/archinstall') + +mods = disk.device_handler.detect_pre_mounted_mods(root_mount_dir) + +disk_config = disk.DiskLayoutConfiguration( + disk.DiskLayoutType.Pre_mount, + device_modifications=mods, + relative_mountpoint=Path('/mnt/archinstall') +) diff --git a/examples/config-sample.json b/examples/config-sample.json index dc8693a7..a7c5d537 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -1,28 +1,132 @@ { - "audio": null, - "bootloader": "systemd-bootctl", - "harddrives": [ - "/dev/loop0" - ], - "hostname": "", + "config_version": "2.5.2", + "additional-repositories": [], + "archinstall-language": "English", + "audio": "pipewire", + "bootloader": "Systemd-boot", + "debug": false, + "disk_config": { + "config_type": "default_layout", + "device_modifications": [ + { + "device": "/dev/sda", + "partitions": [ + { + "btrfs": [], + "flags": [ + "Boot" + ], + "fs_type": "fat32", + "length": { + "sector_size": null, + "total_size": null, + "unit": "MiB", + "value": 512 + }, + "mount_options": [], + "mountpoint": "/boot", + "obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0", + "start": { + "sector_size": null, + "total_size": null, + "unit": "MiB", + "value": 1 + }, + "status": "create", + "type": "primary" + }, + { + "btrfs": [], + "flags": [], + "fs_type": "ext4", + "length": { + "sector_size": null, + "total_size": null, + "unit": "GiB", + "value": 20 + }, + "mount_options": [], + "mountpoint": "/", + "obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208", + "start": { + "sector_size": null, + "total_size": null, + "unit": "MiB", + "value": 513 + }, + "status": "create", + "type": "primary" + }, + { + "btrfs": [], + "flags": [], + "fs_type": "ext4", + "length": { + "sector_size": null, + "total_size": { + "sector_size": null, + "total_size": null, + "unit": "B", + "value": 250148290560 + }, + "unit": "Percent", + "value": 100 + }, + "mount_options": [], + "mountpoint": "/home", + "obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f", + "start": { + "sector_size": null, + "total_size": null, + "unit": "GiB", + "value": 20 + }, + "status": "create", + "type": "primary" + } + ], + "wipe": true + } + ] + }, + "hostname": "archlinux", "kernels": [ "linux" ], "keyboard-layout": "us", "mirror-region": { - "Worldwide": { - "https://mirror.rackspace.com/archlinux/$repo/os/$arch": true + "Australia": { + "http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch": true, } }, "nic": { - "type": "NM" + "dhcp": true, + "dns": null, + "gateway": null, + "iface": null, + "ip": null, + "type": "nm" }, + "no_pkg_lookups": false, "ntp": true, + "offline": false, "packages": [], - "profile": null, + "parallel downloads": 0, + "profile_config": { + "gfx_driver": "All open-source (default)", + "greeter": "sddm", + "profile": { + "details": [ + "Kde" + ], + "main": "Desktop" + } + }, "script": "guided", + "silent": false, "swap": true, "sys-encoding": "utf-8", "sys-language": "en_US", - "timezone": "UTC" + "timezone": "UTC", + "version": "2.5.2" } diff --git a/examples/creds-sample.json b/examples/creds-sample.json index 0681e16f..530a2465 100644 --- a/examples/creds-sample.json +++ b/examples/creds-sample.json @@ -1,15 +1,8 @@ { - "!root-password": "", - "!users": [ - { - "username": "", - "!password": "", - "sudo": false - }, - { - "username": "", - "!password": "", - "sudo": true - } - ] + "!users": [ + { + "sudo": true, + "username": "archinstall" + } + ] } diff --git a/examples/full_automated_installation.py b/examples/full_automated_installation.py new file mode 100644 index 00000000..a169dd50 --- /dev/null +++ b/examples/full_automated_installation.py @@ -0,0 +1,95 @@ +from pathlib import Path + +from archinstall import Installer, ProfileConfiguration, profile_handler +from archinstall.default_profiles.minimal import MinimalProfile +from archinstall import disk +from archinstall.lib.models import User + +# we're creating a new ext4 filesystem installation +fs_type = disk.FilesystemType('ext4') +device_path = Path('/dev/sda') + +# get the physical disk device +device = disk.device_handler.get_device(device_path) + +if not device: + raise ValueError('No device found for given path') + +# create a new modification for the specific device +device_modification = disk.DeviceModification(device, wipe=True) + +# create a new boot partition +boot_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=disk.Size(1, disk.Unit.MiB), + length=disk.Size(512, disk.Unit.MiB), + mountpoint=Path('/boot'), + fs_type=disk.FilesystemType.Fat32, + flags=[disk.PartitionFlag.Boot] +) +device_modification.add_partition(boot_partition) + +# create a root partition +root_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=disk.Size(513, disk.Unit.MiB), + length=disk.Size(20, disk.Unit.GiB), + mountpoint=None, + fs_type=fs_type, + mount_options=[], +) +device_modification.add_partition(root_partition) + +# create a new home partition +home_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=root_partition.length, + length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + mountpoint=Path('/home'), + fs_type=fs_type, + mount_options=[] +) +device_modification.add_partition(home_partition) + +disk_config = disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Default, + device_modifications=[device_modification] +) + +# disk encryption configuration (Optional) +disk_encryption = disk.DiskEncryption( + encryption_password="enc_password", + encryption_type=disk.EncryptionType.Partition, + partitions=[home_partition], + hsm_device=None +) + +# initiate file handler with the disk config and the optional disk encryption config +fs_handler = disk.FilesystemHandler(disk_config, disk_encryption) + +# perform all file operations +# WARNING: this will potentially format the filesystem and delete all data +fs_handler.perform_filesystem_operations(show_countdown=False) + +mountpoint = Path('/tmp') + +with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=['linux'] +) as installation: + installation.mount_ordered_layout() + installation.minimal_installation(hostname='minimal-arch') + installation.add_additional_packages(['nano', 'wget', 'git']) + +# 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) + +user = User('archinstall', 'password', True) +installation.create_users(user) diff --git a/examples/guided.py b/examples/guided.py deleted file mode 100644 index e9240c03..00000000 --- a/examples/guided.py +++ /dev/null @@ -1,306 +0,0 @@ -import logging -import os -import time - -import archinstall -from archinstall import ConfigurationOutput, Menu -from archinstall.lib.models.network_configuration import NetworkConfigurationHandler - -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: {archinstall.disk_layouts()}", level=logging.DEBUG) - - -def ask_user_questions(): - """ - First, we'll ask the user for a bunch of user input. - Not until we're satisfied with what we want to install - will we continue with the actual installation steps. - """ - - # ref: https://github.com/archlinux/archinstall/pull/831 - # we'll set NTP to true by default since this is also - # the default value specified in the menu options; in - # case it will be changed by the user we'll also update - # the system immediately - global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments) - - global_menu.enable('archinstall-language') - - global_menu.enable('keyboard-layout') - - # Set which region to download packages from during the installation - global_menu.enable('mirror-region') - - global_menu.enable('sys-language') - global_menu.enable('sys-encoding') - - # Ask which harddrives/block-devices we will install to - # and convert them into archinstall.BlockDevice() objects. - global_menu.enable('harddrives') - - global_menu.enable('disk_layouts') - - # Specify disk encryption options - global_menu.enable('disk_encryption') - - # Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB) - global_menu.enable('bootloader') - - global_menu.enable('swap') - - # Get the hostname for the machine - global_menu.enable('hostname') - - # Ask for a root password (optional, but triggers requirement for super-user if skipped) - global_menu.enable('!root-password') - - global_menu.enable('!users') - - # Ask for archinstall-specific profiles (such as desktop environments etc) - global_menu.enable('profile') - - # Ask about audio server selection if one is not already set - global_menu.enable('audio') - - # Ask for preferred kernel: - global_menu.enable('kernels') - - global_menu.enable('packages') - - if archinstall.arguments.get('advanced', False): - # Enable parallel downloads - global_menu.enable('parallel downloads') - - # Ask or Call the helper function that asks the user to optionally configure a network. - global_menu.enable('nic') - - global_menu.enable('timezone') - - global_menu.enable('ntp') - - global_menu.enable('additional-repositories') - - global_menu.enable('__separator__') - - global_menu.enable('save_config') - global_menu.enable('install') - global_menu.enable('abort') - - global_menu.run() - - -def perform_filesystem_operations(): - """ - Issue a final warning before we continue with something un-revertable. - We mention the drive one last time, and count from 5 to 0. - """ - - if archinstall.arguments.get('harddrives', None): - print(_(f" ! Formatting {archinstall.arguments['harddrives']} in "), end='') - archinstall.do_countdown() - - """ - Setup the blockdevice, filesystem (and optionally encryption). - Once that's done, we'll hand over to perform_installation() - """ - mode = archinstall.GPT - if archinstall.has_uefi() is False: - mode = archinstall.MBR - - for drive in archinstall.arguments.get('harddrives', []): - if archinstall.arguments.get('disk_layouts', {}).get(drive.path): - with archinstall.Filesystem(drive, mode) as fs: - fs.load_layout(archinstall.arguments['disk_layouts'][drive.path]) - - -def perform_installation(mountpoint): - """ - Performs the installation steps on a block device. - Only requirement is that the block devices are - formatted and setup prior to entering this function. - """ - - with archinstall.Installer(mountpoint, kernels=archinstall.arguments.get('kernels', ['linux'])) as installation: - # Mount all the drives to the desired mountpoint - # This *can* be done outside of the installation, but the installer can deal with it. - if archinstall.arguments.get('disk_layouts'): - installation.mount_ordered_layout(archinstall.arguments['disk_layouts']) - - # Placing /boot check during installation because this will catch both re-use and wipe scenarios. - for partition in installation.partitions: - if partition.mountpoint == installation.target + '/boot': - if partition.size < 0.19: # ~200 MiB in GiB - raise archinstall.DiskError(f"The selected /boot partition in use is not large enough to properly install a boot loader. Please resize it to at least 200MiB and re-run the installation.") - - # If we've activated NTP, make sure it's active in the ISO too and - # make sure at least one time-sync finishes before we continue with the installation - if archinstall.arguments.get('ntp', False): - # Activate NTP in the ISO - archinstall.SysCommand('timedatectl set-ntp true') - - # TODO: This block might be redundant, but this service is not activated unless - # `timedatectl set-ntp true` is executed. - logged = False - while archinstall.service_state('dbus-org.freedesktop.timesync1.service') not in ('running'): - if not logged: - installation.log(f"Waiting for dbus-org.freedesktop.timesync1.service to enter running state", level=logging.INFO) - logged = True - time.sleep(1) - - logged = False - while 'Server: n/a' in archinstall.SysCommand('timedatectl timesync-status --no-pager --property=Server --value'): - if not logged: - installation.log(f"Waiting for timedatectl timesync-status to report a timesync against a server", level=logging.INFO) - logged = True - time.sleep(1) - - # if len(mirrors): - # Certain services might be running that affects the system during installation. - # Currently, only one such service is "reflector.service" which updates /etc/pacman.d/mirrorlist - # We need to wait for it before we continue since we opted in to use a custom mirror/region. - installation.log('Waiting for automatic mirror selection (reflector) to complete.', level=logging.INFO) - while archinstall.service_state('reflector') not in ('dead', 'failed', 'exited'): - time.sleep(1) - - installation.log('Waiting pacman-init.service to complete.', level=logging.INFO) - while archinstall.service_state('pacman-init') not in ('dead', 'failed', 'exited'): - time.sleep(1) - - installation.log('Waiting Arch Linux keyring sync (archlinux-keyring-wkd-sync) to complete.', level=logging.INFO) - while archinstall.service_state('archlinux-keyring-wkd-sync') not in ('dead', 'failed', 'exited'): - time.sleep(1) - - # Set mirrors used by pacstrap (outside of installation) - if archinstall.arguments.get('mirror-region', None): - archinstall.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium - - # Retrieve list of additional repositories and set boolean values appropriately - if archinstall.arguments.get('additional-repositories', None) is not None: - enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', None) - enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', None) - else: - enable_testing = False - enable_multilib = False - - if installation.minimal_installation( - testing=enable_testing, multilib=enable_multilib, hostname=archinstall.arguments['hostname'], - locales=[f"{archinstall.arguments['sys-language']} {archinstall.arguments['sys-encoding'].upper()}"]): - if archinstall.arguments.get('mirror-region') is not None: - if archinstall.arguments.get("mirrors", None) is not None: - installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium - if archinstall.arguments.get('swap'): - installation.setup_swap('zram') - if archinstall.arguments.get("bootloader") == "grub-install" and archinstall.has_uefi(): - installation.add_additional_packages("grub") - installation.add_bootloader(archinstall.arguments["bootloader"]) - - # If user selected to copy the current ISO network configuration - # Perform a copy of the config - network_config = archinstall.arguments.get('nic', None) - - if network_config: - handler = NetworkConfigurationHandler(network_config) - handler.config_installer(installation) - - if archinstall.arguments.get('audio', None) is not None: - installation.log(f"This audio server will be used: {archinstall.arguments.get('audio', None)}", level=logging.INFO) - if archinstall.arguments.get('audio', None) == 'pipewire': - archinstall.Application(installation, 'pipewire').install() - elif archinstall.arguments.get('audio', None) == 'pulseaudio': - print('Installing pulseaudio ...') - installation.add_additional_packages("pulseaudio") - else: - installation.log("No audio server will be installed.", level=logging.INFO) - - if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', None)) - - if archinstall.arguments.get('profile', None): - installation.install_profile(archinstall.arguments.get('profile', None)) - - if users := archinstall.arguments.get('!users', None): - installation.create_users(users) - - if timezone := archinstall.arguments.get('timezone', None): - installation.set_timezone(timezone) - - if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() - - if archinstall.accessibility_tools_in_use(): - installation.enable_espeakup() - - if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): - installation.user_set_pw('root', root_pw) - - # This step must be after profile installs to allow profiles to install language pre-requisits. - # After which, this step will set the language both for console and x11 if x11 was installed for instance. - installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) - - if archinstall.arguments['profile'] and archinstall.arguments['profile'].has_post_install(): - with archinstall.arguments['profile'].load_instructions(namespace=f"{archinstall.arguments['profile'].namespace}.py") as imported: - if not imported._post_install(): - archinstall.log(' * Profile\'s post configuration requirements was not fulfilled.', fg='red') - exit(1) - - # If the user provided a list of services to be enabled, pass the list to the enable_service function. - # Note that while it's called enable_service, it can actually take a list of services and iterate it. - if archinstall.arguments.get('services', None): - installation.enable_service(*archinstall.arguments['services']) - - # If the user provided custom commands to be run post-installation, execute them now. - if archinstall.arguments.get('custom-commands', None): - archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation) - - installation.genfstab() - - installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") - if not archinstall.arguments.get('silent'): - prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run() - if choice.value == Menu.yes(): - try: - installation.drop_to_shell() - except: - pass - - # For support reasons, we'll log the disk layout post installation (crash or no crash) - archinstall.log(f"Disk states after installing: {archinstall.disk_layouts()}", level=logging.DEBUG) - - -if archinstall.arguments.get('skip-mirror-check', False) is False and archinstall.check_mirror_reachable() is False: - log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") - exit(1) - -if not archinstall.arguments.get('silent'): - ask_user_questions() - -config_output = ConfigurationOutput(archinstall.arguments) -if not archinstall.arguments.get('silent'): - config_output.show() -config_output.save() - -if archinstall.arguments.get('dry_run'): - exit(0) - -if not archinstall.arguments.get('silent'): - input(str(_('Press Enter to continue.'))) - -archinstall.configuration_sanity_check() -perform_filesystem_operations() -perform_installation(archinstall.storage.get('MOUNT_POINT', '/mnt')) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py new file mode 100644 index 00000000..a78b1712 --- /dev/null +++ b/examples/interactive_installation.py @@ -0,0 +1,220 @@ +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.default_profiles.applications.pipewire import PipewireProfile +from archinstall import disk +from archinstall import menu +from archinstall.lib.models import Bootloader, NetworkConfigurationHandler + +if TYPE_CHECKING: + _: Any + + +def ask_user_questions(): + global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments) + + global_menu.enable('archinstall-language') + + global_menu.enable('keyboard-layout') + + # Set which region to download packages from during the installation + global_menu.enable('mirror-region') + + global_menu.enable('sys-language') + + global_menu.enable('sys-encoding') + + global_menu.enable('disk_config', mandatory=True) + + # Specify disk encryption options + global_menu.enable('disk_encryption') + + # Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB) + global_menu.enable('bootloader') + + global_menu.enable('swap') + + # Get the hostname for the machine + global_menu.enable('hostname') + + # Ask for a root password (optional, but triggers requirement for super-user if skipped) + global_menu.enable('!root-password', mandatory=True) + + global_menu.enable('!users', mandatory=True) + + # Ask for archinstall-specific profiles_bck (such as desktop environments etc) + global_menu.enable('profile_config') + + # Ask about audio server selection if one is not already set + global_menu.enable('audio') + + # Ask for preferred kernel: + global_menu.enable('kernels') + + global_menu.enable('packages') + + if archinstall.arguments.get('advanced', False): + # Enable parallel downloads + global_menu.enable('parallel downloads') + + # Ask or Call the helper function that asks the user to optionally configure a network. + global_menu.enable('nic') + + global_menu.enable('timezone') + + global_menu.enable('ntp') + + global_menu.enable('additional-repositories') + + global_menu.enable('__separator__') + + global_menu.enable('save_config') + global_menu.enable('install') + global_menu.enable('abort') + + global_menu.run() + + +def perform_installation(mountpoint: Path): + """ + Performs the installation steps on a block device. + Only requirement is that the block devices are + formatted and setup prior to entering this function. + """ + log('Starting installation', level=logging.INFO) + disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + + # Retrieve list of additional repositories and set boolean values appropriately + enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) + enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) + + locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}" + + disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) as installation: + # Mount all the drives to the desired mountpoint + if disk_config.config_type != disk.DiskLayoutType.Pre_mount: + installation.mount_ordered_layout() + + installation.sanity_check() + + if disk_config.config_type != disk.DiskLayoutType.Pre_mount: + if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption: + # generate encryption key files for the mounted luks devices + installation.generate_key_files() + + if archinstall.arguments.get('ntp', False): + installation.activate_ntp() + + # Set mirrors used by pacstrap (outside of installation) + if archinstall.arguments.get('mirror-region', None): + use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + + installation.minimal_installation( + testing=enable_testing, + multilib=enable_multilib, + hostname=archinstall.arguments.get('hostname', 'archlinux'), + locales=[locale] + ) + + if archinstall.arguments.get('mirror-region') is not None: + if archinstall.arguments.get("mirrors", None) is not None: + installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + + if archinstall.arguments.get('swap'): + installation.setup_swap('zram') + + if archinstall.arguments.get("bootloader") == Bootloader.Grub and archinstall.has_uefi(): + installation.add_additional_packages("grub") + + installation.add_bootloader(archinstall.arguments["bootloader"]) + + # If user selected to copy the current ISO network configuration + # Perform a copy of the config + network_config = archinstall.arguments.get('nic', None) + + if network_config: + handler = NetworkConfigurationHandler(network_config) + handler.config_installer(installation) + + if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': + installation.add_additional_packages(archinstall.arguments.get('packages', None)) + + if users := archinstall.arguments.get('!users', None): + installation.create_users(users) + + if audio := archinstall.arguments.get('audio', None): + log(f'Installing audio server: {audio}', level=logging.INFO) + if audio == 'pipewire': + PipewireProfile().install(installation) + elif audio == 'pulseaudio': + installation.add_additional_packages("pulseaudio") + else: + installation.log("No audio server will be installed.", level=logging.INFO) + + if profile_config := archinstall.arguments.get('profile_config', None): + profile_handler.install_profile_config(installation, profile_config) + + if timezone := archinstall.arguments.get('timezone', None): + installation.set_timezone(timezone) + + if archinstall.arguments.get('ntp', False): + installation.activate_time_syncronization() + + if archinstall.accessibility_tools_in_use(): + installation.enable_espeakup() + + if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): + installation.user_set_pw('root', root_pw) + + # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # After which, this step will set the language both for console and x11 if x11 was installed for instance. + installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) + + if profile_config := archinstall.arguments.get('profile_config', None): + profile_config.profile.post_install(installation) + + # If the user provided a list of services to be enabled, pass the list to the enable_service function. + # Note that while it's called enable_service, it can actually take a list of services and iterate it. + if archinstall.arguments.get('services', None): + installation.enable_service(*archinstall.arguments['services']) + + # If the user provided custom commands to be run post-installation, execute them now. + if archinstall.arguments.get('custom-commands', None): + archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation) + + installation.genfstab() + + installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") + + if not archinstall.arguments.get('silent'): + prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) + choice = menu.Menu(prompt, menu.Menu.yes_no(), default_option=menu.Menu.yes()).run() + if choice.value == menu.Menu.yes(): + try: + installation.drop_to_shell() + except: + pass + + archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +ask_user_questions() + +fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) +) + +fs_handler.perform_filesystem_operations() + +perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt'))) diff --git a/examples/mac_address_installation.py b/examples/mac_address_installation.py new file mode 100644 index 00000000..0a1c5160 --- /dev/null +++ b/examples/mac_address_installation.py @@ -0,0 +1,18 @@ +import time + +import archinstall +from archinstall.lib.profile.profiles_handler import profile_handler + +for profile in profile_handler.get_mac_addr_profiles(): + # Tailored means it's a match for this machine + # based on it's MAC address (or some other criteria + # that fits the requirements for this machine specifically). + archinstall.log(f'Found a tailored profile for this machine called: "{profile.name}"') + + print('Starting install in:') + for i in range(10, 0, -1): + print(f'{i}...') + time.sleep(1) + + install_session = archinstall.storage['installation_session'] + profile.install(install_session) diff --git a/examples/minimal.py b/examples/minimal.py deleted file mode 100644 index 8b4c847f..00000000 --- a/examples/minimal.py +++ /dev/null @@ -1,75 +0,0 @@ -import archinstall - -# Select a harddrive and a disk password -from archinstall import User - -archinstall.log("Minimal only supports:") -archinstall.log(" * Being installed to a single disk") - -if archinstall.arguments.get('help', None): - archinstall.log(" - Optional disk encryption via --!encryption-password=") - archinstall.log(" - Optional filesystem type via --filesystem=") - archinstall.log(" - Optional systemd network via --network") - -archinstall.arguments['harddrive'] = archinstall.select_disk(archinstall.all_blockdevices()) - - -def install_on(mountpoint): - # We kick off the installer by telling it where the - with archinstall.Installer(mountpoint) as installation: - # Strap in the base system, add a boot loader and configure - # some other minor details as specified by this profile and user. - if installation.minimal_installation(): - installation.set_hostname('minimal-arch') - installation.add_bootloader() - - # Optionally enable networking: - if archinstall.arguments.get('network', None): - installation.copy_iso_network_config(enable_services=True) - - installation.add_additional_packages(['nano', 'wget', 'git']) - installation.install_profile('minimal') - - 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)") - - -if archinstall.arguments['harddrive']: - archinstall.arguments['harddrive'].keep_partitions = False - - print(f" ! Formatting {archinstall.arguments['harddrive']} in ", end='') - archinstall.do_countdown() - - # First, we configure the basic filesystem layout - with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs: - # We use the entire disk instead of setting up partitions on your own - if archinstall.arguments['harddrive'].keep_partitions is False: - fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs')) - - boot = fs.find_partition('/boot') - root = fs.find_partition('/') - - boot.format('fat32') - - # We encrypt the root partition if we got a password to do so with, - # Otherwise we just skip straight to formatting and installation - if archinstall.arguments.get('!encryption-password', None): - root.encrypted = True - root.encrypt(password=archinstall.arguments.get('!encryption-password', None)) - - with archinstall.luks2(root, 'luksloop', archinstall.arguments.get('!encryption-password', None)) as unlocked_root: - unlocked_root.format(root.filesystem) - unlocked_root.mount('/mnt') - else: - root.format(root.filesystem) - root.mount('/mnt') - - boot.mount('/mnt/boot') - -install_on('/mnt') diff --git a/examples/minimal_installation.py b/examples/minimal_installation.py new file mode 100644 index 00000000..8bd6fd55 --- /dev/null +++ b/examples/minimal_installation.py @@ -0,0 +1,85 @@ +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.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 + + +def perform_installation(mountpoint: Path): + disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) as installation: + # Strap in the base system, add a boot loader and configure + # some other minor details as specified by this profile and user. + if installation.minimal_installation(): + installation.set_hostname('minimal-arch') + installation.add_bootloader(Bootloader.Systemd) + + # Optionally enable networking: + if archinstall.arguments.get('network', None): + installation.copy_iso_network_config(enable_services=True) + + installation.add_additional_packages(['nano', 'wget', 'git']) + + profile_config = ProfileConfiguration(MinimalProfile()) + profile_handler.install_profile_config(installation, profile_config) + + user = User('devel', 'devel', False) + installation.create_users(user) + + +def prompt_disk_layout(): + fs_type = None + if filesystem := archinstall.arguments.get('filesystem', None): + fs_type = disk.FilesystemType(filesystem) + + devices = select_devices() + modifications = suggest_single_disk_layout(devices[0], filesystem_type=fs_type) + + archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Default, + device_modifications=[modifications] + ) + + +def parse_disk_encryption(): + if enc_password := archinstall.arguments.get('!encryption-password', None): + modification: List[disk.DeviceModification] = archinstall.arguments['disk_config'] + partitions: List[disk.PartitionModification] = [] + + # encrypt all partitions except the /boot + for mod in modification: + partitions += list(filter(lambda x: x.mountpoint != Path('/boot'), mod.partitions)) + + archinstall.arguments['disk_encryption'] = disk.DiskEncryption( + encryption_type=disk.EncryptionType.Partition, + encryption_password=enc_password, + partitions=partitions + ) + + +prompt_disk_layout() +parse_disk_encryption() + +fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) +) + +fs_handler.perform_filesystem_operations() + +mount_point = Path('/mnt') +perform_installation(mount_point) diff --git a/examples/only_hd.py b/examples/only_hd.py deleted file mode 100644 index e3d18f0a..00000000 --- a/examples/only_hd.py +++ /dev/null @@ -1,151 +0,0 @@ - -import logging -import os -import pathlib - -import archinstall -from archinstall import ConfigurationOutput - - -class OnlyHDMenu(archinstall.GlobalMenu): - def _setup_selection_menu_options(self): - super()._setup_selection_menu_options() - options_list = [] - mandatory_list = [] - options_list = ['harddrives', 'disk_layouts', 'disk_encryption','swap'] - mandatory_list = ['harddrives'] - options_list.extend(['save_config','install','abort']) - - for entry in self._menu_options: - if entry in options_list: - # for not lineal executions, only self.option(entry).set_enabled and set_mandatory are necessary - if entry in mandatory_list: - self.enable(entry,mandatory=True) - else: - self.enable(entry) - else: - self.option(entry).set_enabled(False) - self._update_install_text() - - def mandatory_lacking(self) -> [int, list]: - mandatory_fields = [] - mandatory_waiting = 0 - for field in self._menu_options: - option = self._menu_options[field] - if option.is_mandatory(): - if not option.has_selection(): - mandatory_waiting += 1 - mandatory_fields += [field,] - return mandatory_fields, mandatory_waiting - - def _missing_configs(self): - """ overloaded method """ - def check(s): - return self.option(s).has_selection() - - missing, missing_cnt = self.mandatory_lacking() - if check('harddrives'): - if not self.option('harddrives').is_empty() and not check('disk_layouts'): - missing_cnt += 1 - missing += ['disk_layout'] - return missing - -def ask_user_questions(): - """ - First, we'll ask the user for a bunch of user input. - Not until we're satisfied with what we want to install - will we continue with the actual installation steps. - """ - with OnlyHDMenu(data_store=archinstall.arguments) as menu: - # We select the execution language separated - menu.exec_option('archinstall-language') - menu.option('archinstall-language').set_enabled(False) - menu.run() - -def perform_disk_operations(): - """ - Issue a final warning before we continue with something un-revertable. - We mention the drive one last time, and count from 5 to 0. - """ - if archinstall.arguments.get('harddrives', None): - print(f" ! Formatting {archinstall.arguments['harddrives']} in ", end='') - archinstall.do_countdown() - """ - Setup the blockdevice, filesystem (and optionally encryption). - Once that's done, we'll hand over to perform_installation() - """ - mode = archinstall.GPT - if archinstall.has_uefi() is False: - mode = archinstall.MBR - - for drive in archinstall.arguments.get('harddrives', []): - if archinstall.arguments.get('disk_layouts', {}).get(drive.path): - with archinstall.Filesystem(drive, mode) as fs: - fs.load_layout(archinstall.arguments['disk_layouts'][drive.path]) - -def perform_installation(mountpoint): - """ - Performs the installation steps on a block device. - Only requirement is that the block devices are - formatted and setup prior to entering this function. - """ - with archinstall.Installer(mountpoint, kernels=None) as installation: - # Mount all the drives to the desired mountpoint - # This *can* be done outside of the installation, but the installer can deal with it. - if archinstall.arguments.get('disk_layouts'): - installation.mount_ordered_layout(archinstall.arguments['disk_layouts']) - - # Placing /boot check during installation because this will catch both re-use and wipe scenarios. - for partition in installation.partitions: - if partition.mountpoint == installation.target + '/boot': - if partition.size <= 0.25: # in GB - raise archinstall.DiskError(f"The selected /boot partition in use is not large enough to properly install a boot loader. Please resize it to at least 256MB and re-run the installation.") - # to generate a fstab directory holder. Avoids an error on exit and at the same time checks the procedure - target = pathlib.Path(f"{mountpoint}/etc/fstab") - if not target.parent.exists(): - target.parent.mkdir(parents=True) - - # For support reasons, we'll log the disk layout post installation (crash or no crash) - archinstall.log(f"Disk states after installing: {archinstall.disk_layouts()}", level=logging.DEBUG) - -def log_execution_environment(): - # 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: {archinstall.disk_layouts()}", level=logging.DEBUG) - - -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_execution_environment() - -if not archinstall.check_mirror_reachable(): - log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") - exit(1) - -if not archinstall.arguments.get('silent'): - ask_user_questions() - -config_output = ConfigurationOutput(archinstall.arguments) -if not archinstall.arguments.get('silent'): - config_output.show() -config_output.save() - -if archinstall.arguments.get('dry_run'): - exit(0) -if not archinstall.arguments.get('silent'): - input('Press Enter to continue.') - -perform_disk_operations() -perform_installation(archinstall.storage.get('MOUNT_POINT', '/mnt')) diff --git a/examples/only_hd_installation.py b/examples/only_hd_installation.py new file mode 100644 index 00000000..2fc74bf0 --- /dev/null +++ b/examples/only_hd_installation.py @@ -0,0 +1,63 @@ +import logging +from pathlib import Path + +import archinstall +from archinstall import Installer +from archinstall.lib import disk + + +def ask_user_questions(): + global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments) + + global_menu.enable('archinstall-language') + + global_menu.enable('disk_config', mandatory=True) + global_menu.enable('disk_encryption') + global_menu.enable('swap') + + global_menu.enable('save_config') + global_menu.enable('install') + global_menu.enable('abort') + + global_menu.run() + + +def perform_installation(mountpoint: Path): + """ + Performs the installation steps on a block device. + Only requirement is that the block devices are + formatted and setup prior to entering this function. + """ + disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] + disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) + + with Installer( + mountpoint, + disk_config, + disk_encryption=disk_encryption, + kernels=archinstall.arguments.get('kernels', ['linux']) + ) as installation: + # Mount all the drives to the desired mountpoint + # This *can* be done outside of the installation, but the installer can deal with it. + if archinstall.arguments.get('disk_config'): + installation.mount_ordered_layout() + + # to generate a fstab directory holder. Avoids an error on exit and at the same time checks the procedure + target = Path(f"{mountpoint}/etc/fstab") + if not target.parent.exists(): + target.parent.mkdir(parents=True) + + # For support reasons, we'll log the disk layout post installation (crash or no crash) + archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + + +ask_user_questions() + +fs_handler = disk.FilesystemHandler( + archinstall.arguments['disk_config'], + archinstall.arguments.get('disk_encryption', None) +) + +fs_handler.perform_filesystem_operations() + +perform_installation(archinstall.storage.get('MOUNT_POINT', Path('/mnt'))) diff --git a/examples/swiss.py b/examples/swiss.py deleted file mode 100644 index 442281de..00000000 --- a/examples/swiss.py +++ /dev/null @@ -1,526 +0,0 @@ -""" - -Script swiss (army knife) -Designed to make different workflows for the installation process. Which is controlled by the argument --mode -mode full guides the full process of installation -mode only_hd only proceeds to the creation of the disk infraestructure (partition, mount points, encryption) -mode only_os processes only the installation of Archlinux and software at --mountpoint (or /mnt/archinstall) -mode minimal (still not implemented) -mode lineal. Instead of a menu, shows a sequence of selection screens (eq. to the old mode for guided.py) - -When using the argument --advanced. an additional menu for several special parameters needed during installation appears - -This script respects the --dry_run argument - -""" -import logging -import os -import time -import pathlib -from typing import TYPE_CHECKING, Any - -import archinstall -from archinstall import ConfigurationOutput, NetworkConfigurationHandler, Menu - -if TYPE_CHECKING: - _: Any - -if archinstall.arguments.get('help'): - print("See `man archinstall` for help.") - exit(0) -if os.getuid() != 0: - print("Archinstall requires root privileges to run. See --help for more.") - exit(1) - -""" -particular routines to SetupMenu -TODO exec con return parameter -""" -def select_activate_NTP(): - prompt = "Would you like to use automatic time synchronization (NTP) with the default time servers? [Y/n]: " - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run() - if choice == Menu.yes(): - return True - else: - return False - - -def select_mode(): - return archinstall.generic_select(['full','only_hd','only_os','minimal','lineal'], - 'Select one execution mode', - default=archinstall.arguments.get('mode','full')) - - -""" -following functions will be at locale_helpers, so they will have to be called prefixed by archinstall -""" -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 """ - archinstall.storage['CMD_LOCALE'] = archinstall.storage.get('CMD_LOCALE_DEFAULT',{}) - -def unset_cmd_locale(): - """ archinstall will use the execution environment default """ - archinstall.storage['CMD_LOCALE'] = {} - -def set_cmd_locale(general :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: - archinstall.storage['CMD_LOCALE'] = {'LC_ALL':general} - else: - archinstall.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: - archinstall.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: - archinstall.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: - archinstall.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: - archinstall.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: - archinstall.log(f"{get_locale_mode_text('LC_MESSAGES')} {messages} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - archinstall.storage['CMD_LOCALE'] = result - -def list_installed_locales() -> list[str]: - lista = [] - for line in archinstall.SysCommand('locale -a'): - lista.append(line.decode('UTF-8').strip()) - return lista - - -""" -end of locale helpers -""" - -def select_installed_locale(mode): - mode_text = get_locale_mode_text(mode) - if mode == 'LC_ALL': - texto = "Select the default execution locale \nIf none, you will be prompted for specific settings" - else: - texto = f"Select the {mode_text} ({mode}) execution locale \nIf none, you will get the installation default" - return archinstall.generic_select([None] + list_installed_locales(), - texto, - allow_empty_input=True, - default=archinstall.storage.get('CMD_LOCALE',{}).get(mode,'C')) - - -""" - _menus -""" - -class SetupMenu(archinstall.AbstractMenu): - def __init__(self,storage_area): - super().__init__(data_store=storage_area) - - def _setup_selection_menu_options(self): - self.set_option( - 'archinstall-language', - archinstall.Selector( - _('Archinstall language'), - lambda x: self._select_archinstall_language(x), - display_func=lambda x: x.display_name, - default=self.translation_handler.get_language_by_abbr('en'), - enabled=True - ) - ) - - self.set_option( - 'ntp', - archinstall.Selector( - 'Activate NTP', - lambda x: select_activate_NTP(), - default='Y', - enabled=True - ) - ) - - self.set_option( - 'mode', - archinstall.Selector( - 'Excution mode', - lambda x : select_mode(), - default='full', - enabled=True) - ) - - for item in ['LC_ALL','LC_CTYPE','LC_NUMERIC','LC_TIME','LC_MESSAGES','LC_COLLATE']: - self.set_option(item, - archinstall.Selector( - f'{get_locale_mode_text(item)} locale', - lambda x,item=item: select_installed_locale(item), # the parameter is needed for the lambda in the loop - enabled=True, - dependencies_not=['LC_ALL'] if item != 'LC_ALL' else [])) - self.option('LC_ALL').set_enabled(True) - self.set_option('continue', - archinstall.Selector( - 'Continue', - exec_func=lambda n,v: True, - enabled=True)) - - def exit_callback(self): - if self._data_store.get('ntp',False): - archinstall.log("Hardware time and other post-configuration steps might be required in order for NTP to work. For more information, please check the Arch wiki.", fg="yellow") - archinstall.SysCommand('timedatectl set-ntp true') - if self._data_store.get('mode',None): - archinstall.arguments['mode'] = self._data_store['mode'] - archinstall.log(f"Archinstall will execute under {archinstall.arguments['mode']} mode") - if self._data_store.get('LC_ALL',None): - archinstall.storage['CMD_LOCALE'] = {'LC_ALL':self._data_store['LC_ALL']} - else: - exec_locale = {} - for item in ['LC_COLLATE','LC_CTYPE','LC_MESSAGES','LC_NUMERIC','LC_TIME']: - if self._data_store.get(item,None): - exec_locale[item] = self._data_store[item] - archinstall.storage['CMD_LOCALE'] = exec_locale - archinstall.log(f"Archinstall will execute with {archinstall.storage.get('CMD_LOCALE',None)} locale") - -class MyMenu(archinstall.GlobalMenu): - def __init__(self,data_store=archinstall.arguments,mode='full'): - self._execution_mode = mode - super().__init__(data_store) - - def _setup_selection_menu_options(self): - super()._setup_selection_menu_options() - options_list = [] - mandatory_list = [] - if self._execution_mode in ('full','lineal'): - options_list = ['keyboard-layout', 'mirror-region', 'harddrives', 'disk_layouts', - 'disk_encryption','swap', 'bootloader', 'hostname', '!root-password', - '!users', 'profile', 'audio', 'kernels', 'packages','additional-repositories','nic', - 'timezone', 'ntp'] - if archinstall.arguments.get('advanced',False): - options_list.extend(['sys-language','sys-encoding']) - mandatory_list = ['harddrives','bootloader','hostname'] - elif self._execution_mode == 'only_hd': - options_list = ['harddrives', 'disk_layouts', 'disk_encryption','swap'] - mandatory_list = ['harddrives'] - elif self._execution_mode == 'only_os': - options_list = ['keyboard-layout', 'mirror-region','bootloader', 'hostname', - '!root-password', '!users', 'profile', 'audio', 'kernels', - 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp'] - mandatory_list = ['hostname'] - if archinstall.arguments.get('advanced',False): - options_list.expand(['sys-language','sys-encoding']) - elif self._execution_mode == 'minimal': - pass - else: - archinstall.log(f"self._execution_mode {self._execution_mode} not supported") - exit(1) - if self._execution_mode != 'lineal': - options_list.extend(['save_config','install','abort']) - if not archinstall.arguments.get('advanced'): - options_list.append('archinstall-language') - - for entry in self._menu_options: - if entry in options_list: - # for not lineal executions, only self.option(entry).set_enabled and set_mandatory are necessary - if entry in mandatory_list: - self.enable(entry,mandatory=True) - else: - self.enable(entry) - else: - self.option(entry).set_enabled(False) - self._update_install_text() - - def post_callback(self,option=None,value=None): - self._update_install_text(self._execution_mode) - - def _missing_configs(self,mode='full'): - def check(s): - return self.option(s).has_selection() - - def has_superuser() -> bool: - users = self._menu_options['!users'].current_selection - return any([u.sudo for u in users]) - - _, missing = self.mandatory_overview() - if mode in ('full','only_os') and (not check('!root-password') and not has_superuser()): - missing += 1 - if mode in ('full', 'only_hd') and check('harddrives'): - if not self.option('harddrives').is_empty() and not check('disk_layouts'): - missing += 1 - return missing - - def _install_text(self,mode='full'): - missing = self._missing_configs(mode) - if missing > 0: - return f'Instalation ({missing} config(s) missing)' - return 'Install' - - def _update_install_text(self, mode='full'): - text = self._install_text(mode) - self.option('install').update_description(text) - - -""" -Installation general subroutines -""" - -def get_current_status(): - # 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: {archinstall.disk_layouts()}", level=logging.DEBUG) - -def ask_user_questions(mode): - """ - First, we'll ask the user for a bunch of user input. - Not until we're satisfied with what we want to install - will we continue with the actual installation steps. - """ - if archinstall.arguments.get('advanced',None): - # 3.9 syntax. former x = {**y,**z} or x.update(y) - set_cmd_locale(charset='es_ES.utf8',collate='es_ES.utf8') - setup_area = archinstall.storage.get('CMD_LOCALE',{}) | {} - with SetupMenu(setup_area) as setup: - if mode == 'lineal': - for entry in setup.list_enabled_options(): - if entry in ('continue','abort'): - continue - if not setup.option(entry).enabled: - continue - setup.exec_option(entry) - else: - setup.run() - archinstall.arguments['archinstall-language'] = setup_area.get('archinstall-language') - else: - archinstall.log("Hardware time and other post-configuration steps might be required in order for NTP to work. For more information, please check the Arch wiki.", fg="yellow") - archinstall.SysCommand('timedatectl set-ntp true') - - with MyMenu(data_store=archinstall.arguments,mode=mode) as global_menu: - - if mode == 'lineal': - for entry in global_menu.list_enabled_options(): - if entry in ('install','abort'): - continue - global_menu.exec_option(entry) - archinstall.arguments[entry] = global_menu.option(entry).get_selection() - else: - global_menu.set_option('install', - archinstall.Selector( - global_menu._install_text(mode), - exec_func=lambda n,v: True if global_menu._missing_configs(mode) == 0 else False, - enabled=True)) - - global_menu.run() - -def perform_filesystem_operations(): - """ - Issue a final warning before we continue with something un-revertable. - We mention the drive one last time, and count from 5 to 0. - """ - - if archinstall.arguments.get('harddrives', None): - print(f" ! Formatting {archinstall.arguments['harddrives']} in ", end='') - archinstall.do_countdown() - - """ - Setup the blockdevice, filesystem (and optionally encryption). - Once that's done, we'll hand over to perform_installation() - """ - - mode = archinstall.GPT - if archinstall.has_uefi() is False: - mode = archinstall.MBR - - for drive in archinstall.arguments.get('harddrives', []): - if archinstall.arguments.get('disk_layouts', {}).get(drive.path): - with archinstall.Filesystem(drive, mode) as fs: - fs.load_layout(archinstall.arguments['disk_layouts'][drive.path]) - -def disk_setup(installation): - # Mount all the drives to the desired mountpoint - # This *can* be done outside of the installation, but the installer can deal with it. - if archinstall.arguments.get('disk_layouts'): - installation.mount_ordered_layout(archinstall.arguments['disk_layouts']) - - # Placing /boot check during installation because this will catch both re-use and wipe scenarios. - for partition in installation.partitions: - if partition.mountpoint == installation.target + '/boot': - if partition.size < 0.19: # ~200 MiB in GiB - raise archinstall.DiskError( - f"The selected /boot partition in use is not large enough to properly install a boot loader. Please resize it to at least 200MiB and re-run the installation.") - -def os_setup(installation): - # if len(mirrors): - # Certain services might be running that affects the system during installation. - # Currently, only one such service is "reflector.service" which updates /etc/pacman.d/mirrorlist - # We need to wait for it before we continue since we opted in to use a custom mirror/region. - installation.log('Waiting for automatic mirror selection (reflector) to complete.', level=logging.INFO) - while archinstall.service_state('reflector') not in ('dead', 'failed'): - time.sleep(1) - # Set mirrors used by pacstrap (outside of installation) - if archinstall.arguments.get('mirror-region', None): - archinstall.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium - if installation.minimal_installation( - hostname=archinstall.arguments['hostname'], - locales=[f"{archinstall.arguments['sys-language']} {archinstall.arguments['sys-encoding'].upper()}"]): - if archinstall.arguments['mirror-region'].get("mirrors", None) is not None: - installation.set_mirrors( - archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium - if archinstall.arguments["bootloader"] == "grub-install" and archinstall.has_uefi(): - installation.add_additional_packages("grub") - installation.add_bootloader(archinstall.arguments["bootloader"]) - if archinstall.arguments['swap']: - installation.setup_swap('zram') - - network_config = archinstall.arguments.get('nic', None) - - if network_config: - handler = NetworkConfigurationHandler(network_config) - handler.config_installer(installation) - - if archinstall.arguments.get('audio', None) is not None: - installation.log(f"This audio server will be used: {archinstall.arguments.get('audio', None)}",level=logging.INFO) - if archinstall.arguments.get('audio', None) == 'pipewire': - archinstall.Application(installation, 'pipewire').install() - elif archinstall.arguments.get('audio', None) == 'pulseaudio': - print('Installing pulseaudio ...') - installation.add_additional_packages("pulseaudio") - else: - installation.log("No audio server will be installed.", level=logging.INFO) - - if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', None)) - - if archinstall.arguments.get('profile', None): - installation.install_profile(archinstall.arguments.get('profile', None)) - - if users := archinstall.arguments.get('!users', None): - installation.create_users(users) - - if timezone := archinstall.arguments.get('timezone', None): - installation.set_timezone(timezone) - - if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() - - if archinstall.accessibility_tools_in_use(): - installation.enable_espeakup() - - if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): - installation.user_set_pw('root', root_pw) - - # This step must be after profile installs to allow profiles to install language pre-requisits. - # After which, this step will set the language both for console and x11 if x11 was installed for instance. - installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) - - if archinstall.arguments['profile'] and archinstall.arguments['profile'].has_post_install(): - with archinstall.arguments['profile'].load_instructions( - namespace=f"{archinstall.arguments['profile'].namespace}.py") as imported: - if not imported._post_install(): - archinstall.log(' * Profile\'s post configuration requirements was not fulfilled.', fg='red') - exit(1) - - # If the user provided a list of services to be enabled, pass the list to the enable_service function. - # Note that while it's called enable_service, it can actually take a list of services and iterate it. - if archinstall.arguments.get('services', None): - installation.enable_service(*archinstall.arguments['services']) - - # If the user provided custom commands to be run post-installation, execute them now. - if archinstall.arguments.get('custom-commands', None): - archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation) - - -def perform_installation(mountpoint, mode): - """ - Performs the installation steps on a block device. - Only requirement is that the block devices are - formatted and setup prior to entering this function. - """ - with archinstall.Installer(mountpoint, kernels=archinstall.arguments.get('kernels', ['linux'])) as installation: - if mode in ('full','only_hd'): - disk_setup(installation) - if mode == 'only_hd': - target = pathlib.Path(f"{mountpoint}/etc/fstab") - if not target.parent.exists(): - target.parent.mkdir(parents=True) - - if mode in ('full','only_os'): - os_setup(installation) - installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") - if not archinstall.arguments.get('silent'): - prompt = 'Would you like to chroot into the newly created installation and perform post-installation configuration?' - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run() - if choice == Menu.yes(): - try: - installation.drop_to_shell() - except: - pass - - # For support reasons, we'll log the disk layout post installation (crash or no crash) - archinstall.log(f"Disk states after installing: {archinstall.disk_layouts()}", level=logging.DEBUG) - - -if not archinstall.check_mirror_reachable(): - log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") - exit(1) - -mode = archinstall.arguments.get('mode', 'full').lower() -if not archinstall.arguments.get('silent'): - ask_user_questions(mode) - -config_output = ConfigurationOutput(archinstall.arguments) -if not archinstall.arguments.get('silent'): - config_output.show() -config_output.save() - -if archinstall.arguments.get('dry_run'): - exit(0) -if not archinstall.arguments.get('silent'): - input('Press Enter to continue.') - -if mode in ('full','only_hd'): - perform_filesystem_operations() -perform_installation(archinstall.storage.get('MOUNT_POINT', '/mnt'), mode) diff --git a/examples/unattended.py b/examples/unattended.py deleted file mode 100644 index f1ed4c94..00000000 --- a/examples/unattended.py +++ /dev/null @@ -1,21 +0,0 @@ -import time - -import archinstall - -archinstall.storage['UPSTREAM_URL'] = 'https://archlinux.life/profiles' -archinstall.storage['PROFILE_DB'] = 'index.json' - -for name, info in archinstall.list_profiles().items(): - # 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). - if info['tailored']: - print(f'Found a tailored profile for this machine called: "{name}".') - print('Starting install in:') - for i in range(10, 0, -1): - print(f'{i}...') - time.sleep(1) - - profile = archinstall.Profile(None, info['path']) - profile.install() - break diff --git a/mypy-strict.ini b/mypy-strict.ini new file mode 100644 index 00000000..be7ddf57 --- /dev/null +++ b/mypy-strict.ini @@ -0,0 +1,102 @@ +[mypy] +python_version = 3.10 +follow_imports = silent +exclude = (?x)( + | ^archinstall/lib/configuration\.py$ + | ^archinstall/lib/disk/btrfs/btrfssubvolumeinfo\.py$ + | ^archinstall/lib/disk/helpers\.py$ + | ^archinstall/lib/hsm/fido\.py$ + | ^archinstall/lib/menu/list_manager\.py$ + | ^archinstall/lib/menu/menu\.py$ + | ^archinstall/lib/menu/simple_menu\.py$ + | ^archinstall/lib/menu/text_input\.py$ + | ^archinstall/lib/models/dataclasses\.py$ + | ^archinstall/lib/models/network_configuration\.py$ + | ^archinstall/lib/models/password_strength\.py$ + | ^archinstall/lib/models/pydantic\.py$ + | ^archinstall/lib/models/subvolume\.py$ + | ^archinstall/lib/models/users\.py$ + | ^archinstall/lib/output\.py$ + | ^archinstall/lib/plugins\.py$ + | ^archinstall/examples/guided\.py$ + | ^archinstall/examples/minimal\.py$ + | ^archinstall/examples/only_hd\.py$ + | ^archinstall/examples/swiss\.py$ + | ^archinstall/__init__\.py$ + | ^archinstall/lib/disk/blockdevice\.py$ + | ^archinstall/lib/disk/btrfs/btrfs_helpers\.py$ + | ^archinstall/lib/disk/btrfs/btrfspartition\.py$ + | ^archinstall/lib/disk/dmcryptdev\.py$ + | ^archinstall/lib/disk/filesystem\.py$ + | ^archinstall/lib/disk/mapperdev\.py$ + | ^archinstall/lib/disk/partition\.py$ + | ^archinstall/lib/disk/user_guides\.py$ + | ^archinstall/lib/general\.py$ + | ^archinstall/lib/hardware\.py$ + | ^archinstall/lib/installer\.py$ + | ^archinstall/lib/locale_helpers\.py$ + | ^archinstall/lib/luks\.py$ + | ^archinstall/lib/menu/global_menu\.py$ + | ^archinstall/lib/menu/selection_menu\.py$ + | ^archinstall/lib/mirrors\.py$ + | ^archinstall/lib/networking\.py$ + | ^archinstall/lib/packages/packages\.py$ + | ^archinstall/lib/pacman\.py$ + | ^archinstall/lib/profiles\.py$ + | ^archinstall/lib/systemd\.py$ + | ^archinstall/lib/translation\.py$ + | ^archinstall/lib/user_interaction/backwards_compatible_conf\.py$ + | ^archinstall/lib/user_interaction/disk_conf\.py$ + | ^archinstall/lib/user_interaction/general_conf\.py$ + | ^archinstall/lib/user_interaction/locale_conf\.py$ + | ^archinstall/lib/user_interaction/manage_users_conf\.py$ + | ^archinstall/lib/user_interaction/network_conf\.py$ + | ^archinstall/lib/user_interaction/partitioning_conf\.py$ + | ^archinstall/lib/user_interaction/save_conf\.py$ + | ^archinstall/lib/user_interaction/system_conf\.py$ + | ^archinstall/lib/user_interaction/utils\.py$ + | ^archinstall/profiles/applications/pipewire\.py$ + | ^archinstall/profiles/awesome\.py$ + | ^archinstall/profiles/bspwm\.py$ + | ^archinstall/profiles/budgie\.py$ + | ^archinstall/profiles/cinnamon\.py$ + | ^archinstall/profiles/cutefish\.py$ + | ^archinstall/profiles/deepin\.py$ + | ^archinstall/profiles/desktop\.py$ + | ^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/qtile\.py$ + | ^archinstall/profiles/server\.py$ + | ^archinstall/profiles/sway\.py$ + | ^archinstall/profiles/xfce4\.py$ + | ^archinstall/profiles/xorg\.py$ + | ^profiles/applications/pipewire\.py$ + | ^profiles/awesome\.py$ + | ^profiles/bspwm\.py$ + | ^profiles/budgie\.py$ + | ^profiles/cinnamon\.py$ + | ^profiles/cutefish\.py$ + | ^profiles/deepin\.py$ + | ^profiles/desktop\.py$ + | ^profiles/enlightenment\.py$ + | ^profiles/gnome\.py$ + | ^profiles/i3\.py$ + | ^profiles/kde\.py$ + | ^profiles/lxqt\.py$ + | ^profiles/mate\.py$ + | ^profiles/minimal\.py$ + | ^profiles/qtile\.py$ + | ^profiles/server\.py$ + | ^profiles/sway\.py$ + | ^profiles/xfce4\.py$ + | ^profiles/xorg\.py$ + | ^examples/guided\.py$ + | ^examples/only_hd\.py$ + | ^examples/minimal\.py$ + | ^examples/swiss\.py$) +files = archinstall/, profiles/, examples/ diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..0add1eb1 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,15 @@ +[mypy] +python_version = 3.10 +follow_imports = silent +exclude = (?x)(^archinstall/lib/disk/btrfs/btrfssubvolumeinfo\.py$ + | ^archinstall/lib/general\.py$ + | ^archinstall/lib/hardware\.py$ + | ^archinstall/lib/menu/menu\.py$ + | ^archinstall/lib/mirrors\.py$ + | ^archinstall/lib/plugins\.py$ + | ^archinstall/lib/installer\.py$ + | ^archinstall/lib/systemd\.py$ + | ^archinstall/lib/user_interaction/general_conf\.py$ + | ^archinstall/lib/user_interaction/locale_conf\.py$ + | ^archinstall/default_profiles/custom\.py$) +files = archinstall/ diff --git a/profiles/52-54-00-12-34-56.py b/profiles/52-54-00-12-34-56.py deleted file mode 100644 index 3b074629..00000000 --- a/profiles/52-54-00-12-34-56.py +++ /dev/null @@ -1,62 +0,0 @@ -import archinstall - -# import json -# import urllib.request - -__packages__ = ['nano', 'wget', 'git'] - -if __name__ == '52-54-00-12-34-56': - awesome = archinstall.Application(archinstall.storage['installation_session'], 'postgresql') - awesome.install() - -""" -# Unmount and close previous runs (Mainly only used for re-runs, but won't hurt.) -archinstall.sys_command(f'umount -R /mnt', suppress_errors=True) -archinstall.sys_command(f'cryptsetup close /dev/mapper/luksloop', suppress_errors=True) - -# Select a harddrive and a disk password -harddrive = archinstall.all_blockdevices()['/dev/sda'] -disk_password = '1234' - -with archinstall.Filesystem(harddrive) as fs: - # Use the entire disk instead of setting up partitions on your own - fs.use_entire_disk('luks2') - - if harddrive.partition[1].size == '512M': - raise OSError('Trying to encrypt the boot partition for Pete's sake..') - harddrive.partition[0].format('fat32') - - with archinstall.luks2(harddrive.partition[1], 'luksloop', disk_password) as unlocked_device: - unlocked_device.format('btrfs') - - with archinstall.Installer( - unlocked_device, - boot_partition=harddrive.partition[0], - hostname="testmachine" - ) as installation: - if installation.minimal_installation(): - installation.add_bootloader() - - installation.add_additional_packages(__packages__) - installation.install_profile('awesome') - - user = User('devel', 'devel', False) - installation.create_users(user) - installation.user_set_pw('root', 'toor') - - print(f'Submitting {archinstall.__version__}: success') - - conditions = { - "project": "archinstall", - "profile": "52-54-00-12-34-56", - "status": "success", - "version": archinstall.__version__ - } - req = urllib.request.Request("https://api.archlinux.life/build/success", - data=json.dumps(conditions).encode('utf8'), - headers={'content-type': 'application/json'}) - try: - urllib.request.urlopen(req, timeout=5) - except: - pass -""" diff --git a/profiles/__init__.py b/profiles/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/profiles/applications/__init__.py b/profiles/applications/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/profiles/applications/awesome.py b/profiles/applications/awesome.py deleted file mode 100644 index 33526fd7..00000000 --- a/profiles/applications/awesome.py +++ /dev/null @@ -1,34 +0,0 @@ -import archinstall - -__packages__ = [ - "awesome", - "xorg-xrandr", - "xterm", - "feh", - "slock", - "terminus-font", - "gnu-free-fonts", - "ttf-liberation", - "xsel", -] - -archinstall.storage['installation_session'].install_profile('xorg') - -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -with open(f"{archinstall.storage['installation_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"{archinstall.storage['installation_session'].target}/etc/X11/xinit/xinitrc", 'w') as xinitrc: - xinitrc.write(xinitrc_data) diff --git a/profiles/applications/cockpit.py b/profiles/applications/cockpit.py deleted file mode 100644 index d8aa0fd1..00000000 --- a/profiles/applications/cockpit.py +++ /dev/null @@ -1,13 +0,0 @@ -import archinstall - -# Define the package list in order for lib to source -# which packages will be installed by this profile -__packages__ = [ - "cockpit", - "udisks2", - "packagekit", -] - -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -archinstall.storage['installation_session'].enable_service('cockpit.socket') diff --git a/profiles/applications/docker.py b/profiles/applications/docker.py deleted file mode 100644 index afbde1a5..00000000 --- a/profiles/applications/docker.py +++ /dev/null @@ -1,9 +0,0 @@ -import archinstall - -# Define the package list in order for lib to source -# which packages will be installed by this profile -__packages__ = ["docker"] - -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -archinstall.storage['installation_session'].enable_service('docker') diff --git a/profiles/applications/httpd.py b/profiles/applications/httpd.py deleted file mode 100644 index 23b3fefa..00000000 --- a/profiles/applications/httpd.py +++ /dev/null @@ -1,9 +0,0 @@ -import archinstall - -# Define the package list in order for lib to source -# which packages will be installed by this profile -__packages__ = ["apache"] - -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -archinstall.storage['installation_session'].enable_service('httpd') diff --git a/profiles/applications/lighttpd.py b/profiles/applications/lighttpd.py deleted file mode 100644 index 71158861..00000000 --- a/profiles/applications/lighttpd.py +++ /dev/null @@ -1,9 +0,0 @@ -import archinstall - -# Define the package list in order for lib to source -# which packages will be installed by this profile -__packages__ = ["lighttpd"] - -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -archinstall.storage['installation_session'].enable_service('lighttpd') diff --git a/profiles/applications/mariadb.py b/profiles/applications/mariadb.py deleted file mode 100644 index bdde18b5..00000000 --- a/profiles/applications/mariadb.py +++ /dev/null @@ -1,11 +0,0 @@ -import archinstall - -# Define the package list in order for lib to source -# which packages will be installed by this profile -__packages__ = ["mariadb"] - -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -archinstall.storage['installation_session'].arch_chroot("mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql") - -archinstall.storage['installation_session'].enable_service('mariadb') diff --git a/profiles/applications/nginx.py b/profiles/applications/nginx.py deleted file mode 100644 index 6f63b15c..00000000 --- a/profiles/applications/nginx.py +++ /dev/null @@ -1,9 +0,0 @@ -import archinstall - -# Define the package list in order for lib to source -# which packages will be installed by this profile -__packages__ = ["nginx"] - -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -archinstall.storage['installation_session'].enable_service('nginx') diff --git a/profiles/applications/pipewire.py b/profiles/applications/pipewire.py deleted file mode 100644 index b6e79347..00000000 --- a/profiles/applications/pipewire.py +++ /dev/null @@ -1,14 +0,0 @@ -import archinstall -import logging - -# Define the package list in order for lib to source -# which packages will be installed by this profile -__packages__ = ["pipewire", "pipewire-alsa", "pipewire-jack", "pipewire-pulse", "gst-plugin-pipewire", "libpulse", "wireplumber"] - -archinstall.log('Installing pipewire', level=logging.INFO) -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -@archinstall.plugin -def on_user_created(installation :archinstall.Installer, user :str): - archinstall.log(f"Enabling pipewire-pulse for {user}", level=logging.INFO) - installation.chroot('systemctl enable --user pipewire-pulse.service', run_as=user) diff --git a/profiles/applications/postgresql.py b/profiles/applications/postgresql.py deleted file mode 100644 index 80ad4b66..00000000 --- a/profiles/applications/postgresql.py +++ /dev/null @@ -1,11 +0,0 @@ -import archinstall - -# Define the package list in order for lib to source -# which packages will be installed by this profile -__packages__ = ["postgresql"] - -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -archinstall.storage['installation_session'].arch_chroot("initdb -D /var/lib/postgres/data", run_as='postgres') - -archinstall.storage['installation_session'].enable_service('postgresql') diff --git a/profiles/applications/sshd.py b/profiles/applications/sshd.py deleted file mode 100644 index 4199ecb0..00000000 --- a/profiles/applications/sshd.py +++ /dev/null @@ -1,9 +0,0 @@ -import archinstall - -# Define the package list in order for lib to source -# which packages will be installed by this profile -__packages__ = ["openssh"] - -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -archinstall.storage['installation_session'].enable_service('sshd') diff --git a/profiles/applications/tomcat.py b/profiles/applications/tomcat.py deleted file mode 100644 index ae6d1c2a..00000000 --- a/profiles/applications/tomcat.py +++ /dev/null @@ -1,12 +0,0 @@ -import archinstall - -# This is using Tomcat 10 as that is the latest release at the time of implementation. -# This should probably be updated to use newer releases as they come out. - -# Define the package list in order for lib to source -# which packages will be installed by this profile -__packages__ = ["tomcat10"] - -archinstall.storage['installation_session'].add_additional_packages(__packages__) - -archinstall.storage['installation_session'].enable_service('tomcat10') diff --git a/profiles/awesome.py b/profiles/awesome.py deleted file mode 100644 index 11c8de3b..00000000 --- a/profiles/awesome.py +++ /dev/null @@ -1,51 +0,0 @@ -# A desktop environment using "Awesome" window manager. - -import archinstall - -is_top_level_profile = False - -# New way of defining packages for a profile, which is iterable and can be used out side -# of the profile to get a list of "what packages will be installed". -__packages__ = [ - "alacritty", -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # Awesome WM requires that xorg is installed - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("awesome", "/somewhere/awesome.py") -# or through conventional import awesome -if __name__ == 'awesome': - # Install the application awesome from the template under /applications/ - awesome = archinstall.Application(archinstall.storage['installation_session'], 'awesome') - awesome.install() - - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - # TODO: Copy a full configuration to ~/.config/awesome/rc.lua instead. - with open(f"{archinstall.storage['installation_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"{archinstall.storage['installation_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) \ No newline at end of file diff --git a/profiles/bspwm.py b/profiles/bspwm.py deleted file mode 100644 index 0fb67ad6..00000000 --- a/profiles/bspwm.py +++ /dev/null @@ -1,43 +0,0 @@ -# A desktop environment using the bspwm window manager. - -import archinstall - -is_top_level_profile = False - -__packages__ = [ - 'bspwm', - 'sxhkd', - 'dmenu', - 'xdo', - 'rxvt-unicode', - 'lightdm', - 'lightdm-gtk-greeter', -] - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # bspwm requires a functioning Xorg installation. - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("bspwm", "/somewhere/bspwm.py") -# or through conventional import bspwm -if __name__ == 'bspwm': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - # Install bspwm packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - # Set up LightDM for login - archinstall.storage['installation_session'].enable_service('lightdm') diff --git a/profiles/budgie.py b/profiles/budgie.py deleted file mode 100644 index 33484680..00000000 --- a/profiles/budgie.py +++ /dev/null @@ -1,45 +0,0 @@ -# A desktop environment using "budgie" - -import archinstall - -is_top_level_profile = False - -__packages__ = [ - "arc-gtk-theme", - "budgie", - "lightdm", - "lightdm-gtk-greeter", - "mate-terminal", - "nemo", - "papirus-icon-theme", -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # budgie requires a functioning Xorg installation. - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("budgie", "/somewhere/budgie.py") -# or through conventional import budgie -if __name__ == 'budgie': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install the Budgie packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - archinstall.storage['installation_session'].enable_service('lightdm') # Light Display Manager diff --git a/profiles/cinnamon.py b/profiles/cinnamon.py deleted file mode 100644 index 0122677a..00000000 --- a/profiles/cinnamon.py +++ /dev/null @@ -1,46 +0,0 @@ -# A desktop environment using "Cinnamon" - -import archinstall - -is_top_level_profile = False - -__packages__ = [ - "cinnamon", - "system-config-printer", - "gnome-keyring", - "gnome-terminal", - "blueberry", - "metacity", - "lightdm", - "lightdm-gtk-greeter", -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # Cinnamon requires a functioning Xorg installation. - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("cinnamon", "/somewhere/cinnamon.py") -# or through conventional import cinnamon -if __name__ == 'cinnamon': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install the Cinnamon packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - archinstall.storage['installation_session'].enable_service('lightdm') # Light Display Manager diff --git a/profiles/cutefish.py b/profiles/cutefish.py deleted file mode 100644 index 486fa389..00000000 --- a/profiles/cutefish.py +++ /dev/null @@ -1,41 +0,0 @@ -# A desktop environment using "Cutefish" - -import archinstall - -is_top_level_profile = False - -__packages__ = [ - "cutefish", - "noto-fonts", - "sddm" -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # Cutefish requires a functional xorg installation. - profile = archinstall.Profile(None, "xorg") - with profile.load_instructions(namespace="xorg.py") as imported: - if hasattr(imported, "_prep_function"): - return imported._prep_function() - else: - print("Deprecated (??): xorg profile has no _prep_function() anymore") - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("cutefish", "/somewhere/cutefish.py") -# or through conventional import cutefish -if __name__ == "cutefish": - # Install dependency profiles - archinstall.storage["installation_session"].install_profile("xorg") - - # Install the Cutefish packages - archinstall.storage["installation_session"].add_additional_packages(__packages__) - - archinstall.storage["installation_session"].enable_service("sddm") diff --git a/profiles/deepin.py b/profiles/deepin.py deleted file mode 100644 index 8196bc4b..00000000 --- a/profiles/deepin.py +++ /dev/null @@ -1,44 +0,0 @@ -# A desktop environment using "Deepin". - -import archinstall - -is_top_level_profile = False - -__packages__ = [ - "deepin", - "deepin-terminal", - "deepin-editor", - "lightdm", - "lightdm-deepin-greeter", -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # Deepin requires a functioning Xorg installation. - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("deepin", "/somewhere/deepin.py") -# or through conventional import deepin -if __name__ == 'deepin': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install the Deepin packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - # Enable autostart of Deepin for all users - archinstall.storage['installation_session'].enable_service('lightdm') diff --git a/profiles/desktop.py b/profiles/desktop.py deleted file mode 100644 index e94d3505..00000000 --- a/profiles/desktop.py +++ /dev/null @@ -1,97 +0,0 @@ -# A desktop environment selector. -from typing import Any, TYPE_CHECKING - -import archinstall -from archinstall import log, Menu -from archinstall.lib.menu.menu import MenuSelectionType - -if TYPE_CHECKING: - _: Any - -is_top_level_profile = True - -__description__ = str(_('Provides a selection of desktop environments and tiling window managers, e.g. gnome, kde, sway')) - -# New way of defining packages for a profile, which is iterable and can be used out side -# of the profile to get a list of "what packages will be installed". -__packages__ = [ - 'nano', - 'vim', - 'openssh', - 'htop', - 'wget', - 'iwd', - 'wireless_tools', - 'wpa_supplicant', - 'smartmontools', - 'xdg-utils', -] - -__supported__ = [ - 'gnome', - 'kde', - 'awesome', - 'sway', - 'cinnamon', - 'xfce4', - 'lxqt', - 'i3', - 'bspwm', - 'budgie', - 'mate', - 'deepin', - 'enlightenment', - 'qtile' -] - - -def _prep_function(*args, **kwargs) -> bool: - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - choice = Menu(str(_('Select your desired desktop environment')), __supported__).run() - - if choice.type_ != MenuSelectionType.Selection: - return False - - if choice.value: - # Temporarily store the selected desktop profile - # in a session-safe location, since this module will get reloaded - # the next time it gets executed. - if not archinstall.storage.get('_desktop_profile', None): - archinstall.storage['_desktop_profile'] = choice.value - if not archinstall.arguments.get('desktop-environment', None): - archinstall.arguments['desktop-environment'] = choice.value - profile = archinstall.Profile(None, choice.value) - # Loading the instructions with a custom namespace, ensures that a __name__ comparison is never triggered. - with profile.load_instructions(namespace=f"{choice.value}.py") as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - log(f"Deprecated (??): {choice.value} profile has no _prep_function() anymore") - exit(1) - - return False - - -if __name__ == 'desktop': - """ - This "profile" is a meta-profile. - There are no desktop-specific steps, it simply routes - the installer to whichever desktop environment/window manager was chosen. - - Maybe in the future, a network manager or similar things *could* be added here. - We should honor that Arch Linux does not officially endorse a desktop-setup, nor is - it trying to be a turn-key desktop distribution. - - There are plenty of desktop-turn-key-solutions based on Arch Linux, - this is therefore just a helper to get started - """ - - # Install common packages for all desktop environments - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - archinstall.storage['installation_session'].install_profile(archinstall.storage['_desktop_profile']) diff --git a/profiles/enlightenment.py b/profiles/enlightenment.py deleted file mode 100644 index 3850fed0..00000000 --- a/profiles/enlightenment.py +++ /dev/null @@ -1,43 +0,0 @@ -# A desktop environment using "Enlightenment". - -import archinstall - -is_top_level_profile = False - -__packages__ = [ - "enlightenment", - "terminology", - "lightdm", - "lightdm-gtk-greeter", -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # Enlightenment requires a functioning Xorg installation. - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("enlightenment", "/somewhere/enlightenment.py") -# or through conventional import enlightenment -if __name__ == 'enlightenment': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install the enlightenment packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - # Enable autostart of enlightenment for all users - archinstall.storage['installation_session'].enable_service('lightdm') diff --git a/profiles/gnome.py b/profiles/gnome.py deleted file mode 100644 index 5e3a8da6..00000000 --- a/profiles/gnome.py +++ /dev/null @@ -1,45 +0,0 @@ -# A desktop environment using "Gnome" - -import archinstall - -is_top_level_profile = False - -# Note: GDM should be part of the gnome group, but adding it here for clarity -__packages__ = [ - "gnome", - "gnome-tweaks", - "gdm" -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # Gnome optionally supports xorg, we'll install it since it also - # includes graphic driver setups (this might change in the future) - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("gnome", "/somewhere/gnome.py") -# or through conventional import gnome -if __name__ == 'gnome': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install the GNOME packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - archinstall.storage['installation_session'].enable_service('gdm') # Gnome Display Manager -# We could also start it via xinitrc since we do have Xorg, -# but for gnome that's deprecated and wayland is preferred. diff --git a/profiles/i3.py b/profiles/i3.py deleted file mode 100644 index d9b98b77..00000000 --- a/profiles/i3.py +++ /dev/null @@ -1,59 +0,0 @@ -# Common package for i3. - -import archinstall - -is_top_level_profile = False - -# New way of defining packages for a profile, which is iterable and can be used out side -# of the profile to get a list of "what packages will be installed". -__packages__ = [ - 'i3-wm', - 'i3lock', - 'i3status', - 'i3blocks', - 'xterm', - 'lightdm-gtk-greeter', - 'lightdm', - 'dmenu', -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # i3 requires a functioning Xorg installation. - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -if __name__ == 'i3': - """ - This "profile" is a meta-profile. - There are no desktop-specific steps, it simply routes - the installer to whichever desktop environment/window manager was chosen. - - Maybe in the future, a network manager or similar things *could* be added here. - We should honor that Arch Linux does not officially endorse a desktop-setup, nor is - it trying to be a turn-key desktop distribution. - - There are plenty of desktop-turn-key-solutions based on Arch Linux, - this is therefore just a helper to get started - """ - - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install the i3 packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - # Enable autostart of lightdm for all users - archinstall.storage['installation_session'].enable_service('lightdm') diff --git a/profiles/kde.py b/profiles/kde.py deleted file mode 100644 index d32bf31b..00000000 --- a/profiles/kde.py +++ /dev/null @@ -1,58 +0,0 @@ -# A desktop environment using "KDE". - -import archinstall - -is_top_level_profile = False - -__packages__ = [ - "plasma-meta", - "konsole", - "kwrite", - "dolphin", - "ark", - "sddm", - "plasma-wayland-session", - "egl-wayland" -] - - -# TODO: Remove hard dependency of bash (due to .bash_profile) - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # KDE requires a functioning Xorg installation. - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -""" -def _post_install(*args, **kwargs): - if "nvidia" in _gfx_driver_packages: - print("Plasma Wayland has known compatibility issues with the proprietary Nvidia driver") - print("After booting, you can choose between Wayland and Xorg using the drop-down menu") - return True -""" - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("kde", "/somewhere/kde.py") -# or through conventional import kde -if __name__ == 'kde': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install the KDE packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - # Enable autostart of KDE for all users - archinstall.storage['installation_session'].enable_service('sddm') diff --git a/profiles/lxqt.py b/profiles/lxqt.py deleted file mode 100644 index 2419b4fa..00000000 --- a/profiles/lxqt.py +++ /dev/null @@ -1,50 +0,0 @@ -# A desktop environment using "LXQt" - -import archinstall - -is_top_level_profile = False - -# 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 -__packages__ = [ - "lxqt", - "breeze-icons", - "oxygen-icons", - "xdg-utils", - "ttf-freefont", - "leafpad", - "slock", - "sddm", -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # LXQt requires a functional xorg installation. - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("lxqt", "/somewhere/lxqt.py") -# or through conventional import lxqt -if __name__ == 'lxqt': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install the LXQt packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - # Enable autostart of LXQt for all users - archinstall.storage['installation_session'].enable_service('sddm') diff --git a/profiles/mate.py b/profiles/mate.py deleted file mode 100644 index 94b91f81..00000000 --- a/profiles/mate.py +++ /dev/null @@ -1,42 +0,0 @@ -# A desktop environment using "MATE" - -import archinstall - -is_top_level_profile = False - -__packages__ = [ - "mate", - "mate-extra", - "lightdm", - "lightdm-gtk-greeter", -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # MATE requires a functional xorg installation. - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("mate", "/somewhere/mate.py") -# or through conventional import mate -if __name__ == 'mate': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install the MATE packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - archinstall.storage['installation_session'].enable_service('lightdm') # Light Display Manager diff --git a/profiles/minimal.py b/profiles/minimal.py deleted file mode 100644 index a412aa81..00000000 --- a/profiles/minimal.py +++ /dev/null @@ -1,24 +0,0 @@ -# Used to do a minimal install -import archinstall - -is_top_level_profile = True - -__description__ = str(_('A very basic installation that allows you to customize Arch Linux as you see fit.')) - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. For minimal install, - we don't need to do anything special here, but it - needs to exist and return True. - """ - archinstall.storage['profile_minimal'] = True - return True # Do nothing and just return True - - -if __name__ == 'minimal': - """ - This "profile" is a meta-profile. - It is used for a custom minimal installation, without any desktop-specific packages. - """ diff --git a/profiles/qtile.py b/profiles/qtile.py deleted file mode 100644 index ace13dcc..00000000 --- a/profiles/qtile.py +++ /dev/null @@ -1,42 +0,0 @@ -# A desktop environment using "qtile" window manager with common packages. - -import archinstall - -is_top_level_profile = False - -# New way of defining packages for a profile, which is iterable and can be used out side -# of the profile to get a list of "what packages will be installed". -__packages__ = [ - 'qtile', - 'alacritty', - 'lightdm-gtk-greeter', - 'lightdm', -] - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # qtile optionally supports xorg, we'll install it since it also - # includes graphic driver setups (this might change in the future) - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -if __name__ == 'qtile': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install packages for qtile - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - # Auto start lightdm for all users - archinstall.storage['installation_session'].enable_service('lightdm') # Light Display Manager diff --git a/profiles/server.py b/profiles/server.py deleted file mode 100644 index f3e32d26..00000000 --- a/profiles/server.py +++ /dev/null @@ -1,63 +0,0 @@ -# Used to select various server application profiles on top of a minimal installation. - -import logging -from typing import Any, TYPE_CHECKING - -import archinstall -from archinstall import Menu -from archinstall.lib.menu.menu import MenuSelectionType - -if TYPE_CHECKING: - _: Any - -is_top_level_profile = True - -__description__ = str(_('Provides a selection of various server packages to install and enable, e.g. httpd, nginx, mariadb')) - -available_servers = [ - "cockpit", - "docker", - "httpd", - "lighttpd", - "mariadb", - "nginx", - "postgresql", - "sshd", - "tomcat", -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. - """ - choice = Menu(str(_( - 'Choose which servers to install, if none then a minimal installation will be done')), - available_servers, - preset_values=kwargs['servers'], - multi=True - ).run() - - if choice.type_ != MenuSelectionType.Selection: - return False - - if choice.value: - archinstall.storage['_selected_servers'] = choice.value - return True - - return False - - -if __name__ == 'server': - """ - This "profile" is a meta-profile. - """ - archinstall.log('Now installing the selected servers.', level=logging.INFO) - archinstall.log(archinstall.storage['_selected_servers'], level=logging.DEBUG) - for server in archinstall.storage['_selected_servers']: - archinstall.log(f'Installing {server} ...', level=logging.INFO) - app = archinstall.Application(archinstall.storage['installation_session'], server) - app.install() - - archinstall.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/profiles/sway.py b/profiles/sway.py deleted file mode 100644 index f69f73ce..00000000 --- a/profiles/sway.py +++ /dev/null @@ -1,100 +0,0 @@ -# A desktop environment using "Sway" -from typing import Any, TYPE_CHECKING - -import archinstall -from archinstall import Menu -from archinstall.lib.menu.menu import MenuSelectionType - -if TYPE_CHECKING: - _: Any - -is_top_level_profile = False - -__packages__ = [ - "sway", - "swaybg", - "swaylock", - "swayidle", - "waybar", - "dmenu", - "brightnessctl", - "grim", - "slurp", - "pavucontrol", - "foot", - "xorg-xwayland", -] - - -def _check_driver() -> bool: - packages = archinstall.storage.get("gfx_driver_packages", []) - - if packages and "nvidia" in packages: - prompt = _('The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?') - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run() - - if choice.value == Menu.no(): - return False - - return True - -def _get_system_privelege_control_preference(): - # 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"]).run() - - if choice.type_ != MenuSelectionType.Selection: - return False - - archinstall.storage['sway_sys_priv_ctrl'] = [choice.value] - archinstall.arguments['sway_sys_priv_ctrl'] = [choice.value] - return True - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - if not _get_system_privelege_control_preference(): - return False - - driver = archinstall.select_driver() - - if driver: - archinstall.storage["gfx_driver_packages"] = driver - if not _check_driver(): - return _prep_function(args, kwargs) - return True - - return False - - -""" -def _post_install(*args, **kwargs): - if "seatd" in sway_sys_priv_ctrl: - print(_('After booting, add user(s) to the `seat` user group and re-login to use Sway')) - return True -""" - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("sway", "/somewhere/sway.py") -# or through conventional import sway -if __name__ == "sway": - if not _check_driver(): - raise archinstall.lib.exceptions.HardwareIncompatibilityError(_('Sway does not support the proprietary nvidia drivers.')) - - # Install the Sway packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - if "seatd" in archinstall.storage['sway_sys_priv_ctrl']: - archinstall.storage['installation_session'].add_additional_packages(['seatd']) - archinstall.storage['installation_session'].enable_service('seatd') - elif "polkit" in archinstall.storage['sway_sys_priv_ctrl']: - archinstall.storage['installation_session'].add_additional_packages(['polkit']) - else: - raise archinstall.lib.exceptions.ProfileError(_('Sway requires either seatd or polkit to run')) - - # Install the graphics driver packages - archinstall.storage['installation_session'].add_additional_packages(f"xorg-server xorg-xinit {' '.join(archinstall.storage.get('gfx_driver_packages', None))}") diff --git a/profiles/xfce4.py b/profiles/xfce4.py deleted file mode 100644 index fbc68c10..00000000 --- a/profiles/xfce4.py +++ /dev/null @@ -1,45 +0,0 @@ -# A desktop environment using "Xfce4" - -import archinstall - -is_top_level_profile = False - -__packages__ = [ - "xfce4", - "xfce4-goodies", - "pavucontrol", - "lightdm", - "lightdm-gtk-greeter", - "gvfs", - "xarchiver" -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - # XFCE requires a functional xorg installation. - profile = archinstall.Profile(None, 'xorg') - with profile.load_instructions(namespace='xorg.py') as imported: - if hasattr(imported, '_prep_function'): - return imported._prep_function() - else: - print('Deprecated (??): xorg profile has no _prep_function() anymore') - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("xfce4", "/somewhere/xfce4.py") -# or through conventional import xfce4 -if __name__ == 'xfce4': - # Install dependency profiles - archinstall.storage['installation_session'].install_profile('xorg') - - # Install the XFCE4 packages - archinstall.storage['installation_session'].add_additional_packages(__packages__) - - archinstall.storage['installation_session'].enable_service('lightdm') # Light Display Manager diff --git a/profiles/xorg.py b/profiles/xorg.py deleted file mode 100644 index de45acd3..00000000 --- a/profiles/xorg.py +++ /dev/null @@ -1,68 +0,0 @@ -# A system with "xorg" installed - -import archinstall -import logging -from archinstall.lib.hardware import __packages__ as __hwd__packages__ - -is_top_level_profile = True - -__description__ = str(_('Installs a minimal system as well as xorg and graphics drivers.')) - -__packages__ = [ - 'dkms', - 'xorg-server', - 'xorg-xinit', - 'nvidia-dkms', - *__hwd__packages__, -] - - -def _prep_function(*args, **kwargs): - """ - Magic function called by the importing installer - before continuing any further. It also avoids executing any - other code in this stage. So it's a safe way to ask the user - for more input before any other installer steps start. - """ - - driver = archinstall.select_driver() - - if driver: - archinstall.storage["gfx_driver_packages"] = driver - return True - - # TODO: Add language section and/or merge it with the locale selected - # earlier in for instance guided.py installer. - - return False - - -# Ensures that this code only gets executed if executed -# through importlib.util.spec_from_file_location("xorg", "/somewhere/xorg.py") -# or through conventional import xorg -if __name__ == 'xorg': - try: - if "nvidia" in archinstall.storage.get("gfx_driver_packages", []): - if "linux-zen" in archinstall.storage['installation_session'].base_packages or "linux-lts" in archinstall.storage['installation_session'].base_packages: - for kernel in archinstall.storage['installation_session'].kernels: - archinstall.storage['installation_session'].add_additional_packages(f"{kernel}-headers") # Fixes https://github.com/archlinux/archinstall/issues/585 - archinstall.storage['installation_session'].add_additional_packages("dkms") # I've had kernel regen fail if it wasn't installed before nvidia-dkms - archinstall.storage['installation_session'].add_additional_packages("xorg-server", "xorg-xinit", "nvidia-dkms") - else: - archinstall.storage['installation_session'].add_additional_packages(f"xorg-server", "xorg-xinit", *archinstall.storage.get('gfx_driver_packages', [])) - elif 'amdgpu' in archinstall.storage.get("gfx_driver_packages", []): - # The order of these two are important if amdgpu is installed #808 - if 'amdgpu' in archinstall.storage['installation_session'].MODULES: - archinstall.storage['installation_session'].MODULES.remove('amdgpu') - archinstall.storage['installation_session'].MODULES.append('amdgpu') - - if 'radeon' in archinstall.storage['installation_session'].MODULES: - archinstall.storage['installation_session'].MODULES.remove('radeon') - archinstall.storage['installation_session'].MODULES.append('radeon') - - archinstall.storage['installation_session'].add_additional_packages(f"xorg-server", "xorg-xinit", *archinstall.storage.get('gfx_driver_packages', [])) - else: - archinstall.storage['installation_session'].add_additional_packages(f"xorg-server", "xorg-xinit", *archinstall.storage.get('gfx_driver_packages', [])) - except Exception as err: - archinstall.log(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}", level=logging.WARNING, fg="yellow") - archinstall.storage['installation_session'].add_additional_packages("xorg-server", "xorg-xinit") # Prep didn't run, so there's no driver to install diff --git a/pyproject.toml b/pyproject.toml index 6e0fcb99..557418cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "archinstall" -dynamic = ["version", "entry-points"] +dynamic = ["version"] description = "Arch Linux installer - guided, templates etc." authors = [ {name = "Anton Hvornum", email = "anton@hvornum.se"}, @@ -12,15 +12,17 @@ authors = [ license = {text = "GPL-3.0-only"} readme = "README.md" requires-python = ">=3.10" - keywords = ["linux", "arch", "archinstall", "installer"] - classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Operating System :: POSIX :: Linux", ] +dependencies = [ + "pyparted==3.12.0", + "simple-term-menu==1.6.1", +] [project.urls] Home = "https://archlinux.org" @@ -28,23 +30,33 @@ Documentation = "https://archinstall.readthedocs.io/" Source = "https://github.com/archlinux/archinstall" [project.optional-dependencies] +dev = [ + "mypy==1.1.1", +] doc = ["sphinx"] [project.scripts] archinstall = "archinstall:run_as_a_module" +[tool.setuptools.dynamic] +version = {attr = "archinstall.__version__"} +readme = {file = ["README.rst", "USAGE.rst"]} + [tool.setuptools] -packages = ["archinstall", "profiles", "examples"] +packages = ["archinstall"] [tool.setuptools.package-data] -archinstall = [ - "examples/*.py", - "profiles/*.py", - "profiles/applications/*.py" +# We could specify locales/lancuages.json etc instead, but catchall works too. +"archinstall" = [ + "**/*.py", + "**/*.mo", + "**/*.po", + "**/*.pot", + "**/*.json", ] -[tool.setuptools.dynamic] -version = {attr = "archinstall.__version__"} +# [tool.setuptools.packages.find] +# where = ["archinstall"] [tool.mypy] python_version = "3.10" diff --git a/schema.json b/schema.json index 9269e8e8..0a41ebf0 100644 --- a/schema.json +++ b/schema.json @@ -37,17 +37,6 @@ "type": "string" } }, - "gfx_driver": { - "description": "Graphics Drivers to install if a desktop profile is used, ignored otherwise.", - "type": "string", - "enum": [ - "VMware / VirtualBox (open-source)", - "Nvidia", - "Intel (open-source)", - "AMD / ATI (open-source)", - "All open-source (default)" - ] - }, "harddrives": { "description": "Path of device to be used", "type": "array", @@ -110,29 +99,83 @@ } }, "profile": { - "description": "Profiles are present in profiles/, use the name of a profile to install it", - "type": "string", - "enum": [ - "awesome", - "bspwm", - "budgie", - "cinnamon", - "cutefish", - "deepin", - "desktop", - "enlightenment", - "gnome", - "i3", - "kde", - "lxqt", - "mate", - "minimal", - "server", - "sway", - "xfce4", - "xorg", - "qtile" - ] + "path": { + "description": "Local path or Url that points to a python file containing profile definitions", + "type": "string" + }, + "main": { + "description": "Main top level profile selection", + "type": "string", + "enum": [ + "desktop", + "minimal", + "server", + "xorg", + "custom" + ] + }, + "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)", + "type": "string", + "enum": [ + "awesome", + "bspwm", + "budgie", + "cinnamon", + "cutefish", + "deepin", + "desktop", + "enlightenment", + "gnome", + "i3-wm", + "i3-gasp", + "kde", + "lxqt", + "mate", + "sway", + "xfce4", + "qtile", + "cockpit", + "docker", + "httpd", + "lighttpd", + "mariadb", + "nginx", + "postgresql", + "sshd", + "tomcat" + ] + }, + "custom": { + "description": "Specific profile definitions for custom setup profiles)", + "type": "array", + "items": { + "type": "object", + "properties": { + "packages": "string", + "services": "string", + "enabled": "boolean" + } + } + }, + "gfx_driver": { + "description": "Graphics Drivers to install if a desktop profile is used, ignored otherwise.", + "type": "string", + "enum": [ + "VMware / VirtualBox (open-source)", + "Intel (open-source)", + "AMD / ATI (open-source)", + "All open-source (default)", + "Nvidia (open kernel module for newer GPUs, Turing+)", + "Nvidia (open-source nouveau driver)", + "Nvidia (proprietary)" + ] + }, + "greeter_type": { + "description": "Greeter type to install if a desktop profile is used, ignored otherwise.", + "type": "string", + "enum": ["lightdm", "sddm", "gdm"] + } }, "services": { "description": "Services to enable post-installation", -- cgit v1.2.3-70-g09d2