├── examples ├── __init__.py ├── creds-sample.json ├── config-sample.json ├── unattended.py ├── custom-command-sample.json ├── disk_layouts-sample.json └── minimal.py ├── profiles ├── __init__.py ├── applications │ ├── __init__.py │ ├── httpd.py │ ├── nginx.py │ ├── sshd.py │ ├── docker.py │ ├── lighttpd.py │ ├── cockpit.py │ ├── postgresql.py │ ├── mariadb.py │ ├── tomcat.py │ ├── pipewire.py │ └── awesome.py ├── minimal.py ├── cutefish.py ├── mate.py ├── bspwm.py ├── xfce4.py ├── deepin.py ├── enlightenment.py ├── cinnamon.py ├── qtile.py ├── budgie.py ├── gnome.py ├── lxqt.py ├── kde.py ├── server.py ├── sway.py ├── awesome.py ├── 52-54-00-12-34-56.py ├── i3.py ├── desktop.py └── xorg.py ├── archinstall ├── lib │ ├── __init__.py │ ├── packages │ │ ├── __init__.py │ │ └── packages.py │ ├── udev │ │ ├── __init__.py │ │ └── udevadm.py │ ├── hsm │ │ ├── __init__.py │ │ └── fido.py │ ├── models │ │ ├── __init__.py │ │ ├── subvolume.py │ │ ├── users.py │ │ ├── password_strength.py │ │ ├── pydantic.py │ │ ├── dataclasses.py │ │ └── network_configuration.py │ ├── menu │ │ ├── __init__.py │ │ ├── text_input.py │ │ └── list_manager.py │ ├── disk │ │ ├── __init__.py │ │ ├── diskinfo.py │ │ ├── validators.py │ │ ├── dmcryptdev.py │ │ ├── btrfs │ │ │ ├── __init__.py │ │ │ └── btrfspartition.py │ │ └── mapperdev.py │ ├── services.py │ ├── user_interaction │ │ ├── __init__.py │ │ ├── locale_conf.py │ │ ├── utils.py │ │ ├── save_conf.py │ │ ├── disk_conf.py │ │ ├── subvolume_config.py │ │ ├── manage_users_conf.py │ │ ├── backwards_compatible_conf.py │ │ └── system_conf.py │ ├── pacman.py │ ├── exceptions.py │ ├── storage.py │ ├── networking.py │ ├── plugins.py │ ├── systemd.py │ ├── hardware.py │ ├── configuration.py │ └── locale_helpers.py ├── examples ├── profiles ├── locales │ ├── ar │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── cs │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── de │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── el │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── en │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── es │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── fr │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── id │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── it │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── nl │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── pl │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── pt │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── ru │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── sv │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── ta │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── tr │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── ur │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ └── base.mo │ ├── locales_generator.sh │ └── README.md └── __main__.py ├── docs ├── logo.png ├── _static │ ├── logo.png │ ├── logo.psd │ ├── logo.pride.png │ └── logo.pride.psd ├── archinstall │ ├── Application.rst │ ├── Installer.rst │ ├── Profile.rst │ └── general.rst ├── pull_request_template.md ├── README.md ├── Makefile ├── help │ ├── discord.rst │ └── issues.rst ├── examples │ ├── binary.rst │ └── python.rst ├── installing │ ├── python.rst │ └── binary.rst ├── index.rst ├── flowcharts │ └── DiskSelectionProcess.drawio └── conf.py ├── .pypirc ├── setup.py ├── .github ├── CODEOWNERS └── workflows │ ├── bandit.yaml │ ├── flake8.yaml │ ├── pytest.yaml │ ├── python-build.yml │ ├── python-publish.yml │ ├── mypy.yaml │ └── iso-build.yaml ├── .editorconfig ├── .flake8 ├── .readthedocs.yml ├── .gitignore ├── setup.cfg ├── pyproject.toml ├── PKGBUILD ├── .gitlab-ci.yml └── CONTRIBUTING.md /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /profiles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /archinstall/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /archinstall/examples: -------------------------------------------------------------------------------- 1 | ../examples/ -------------------------------------------------------------------------------- /archinstall/profiles: -------------------------------------------------------------------------------- 1 | ../profiles/ -------------------------------------------------------------------------------- /profiles/applications/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /archinstall/lib/packages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /archinstall/lib/udev/__init__.py: -------------------------------------------------------------------------------- 1 | from .udevadm import udevadm_info -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/docs/logo.png -------------------------------------------------------------------------------- /archinstall/lib/hsm/__init__.py: -------------------------------------------------------------------------------- 1 | from .fido import ( 2 | get_fido2_devices, 3 | fido2_enroll 4 | ) -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/_static/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/docs/_static/logo.psd -------------------------------------------------------------------------------- /archinstall/lib/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .network_configuration import NetworkConfiguration as NetworkConfiguration -------------------------------------------------------------------------------- /docs/_static/logo.pride.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/docs/_static/logo.pride.png -------------------------------------------------------------------------------- /docs/_static/logo.pride.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/docs/_static/logo.pride.psd -------------------------------------------------------------------------------- /.pypirc: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers = 3 | pypi 4 | 5 | [pypi] 6 | repository = https://upload.pypi.org/legacy/ -------------------------------------------------------------------------------- /archinstall/lib/menu/__init__.py: -------------------------------------------------------------------------------- 1 | from .menu import Menu as Menu 2 | from .global_menu import GlobalMenu as GlobalMenu -------------------------------------------------------------------------------- /archinstall/locales/ar/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/ar/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/cs/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/cs/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/de/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/de/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/el/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/el/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/en/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/en/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/es/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/es/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/fr/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/fr/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/id/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/id/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/it/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/it/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/nl/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/nl/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/pl/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/pl/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/pt/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/pt/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/ru/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/ru/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/sv/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/sv/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/ta/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/ta/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/tr/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/tr/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/ur/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/ur/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /archinstall/locales/pt_BR/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0-zero/archinstall/master/archinstall/locales/pt_BR/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools # type: ignore 2 | 3 | setuptools.setup(package_data={'archinstall': ['locales/*','locales/*/*','locales/*/*/*']}, include_package_data=True) 4 | -------------------------------------------------------------------------------- /archinstall/lib/disk/__init__.py: -------------------------------------------------------------------------------- 1 | from .btrfs import * 2 | from .helpers import * 3 | from .blockdevice import BlockDevice 4 | from .filesystem import Filesystem, MBR, GPT 5 | from .partition import * 6 | from .user_guides import * 7 | from .validators import * -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # As per https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#example-of-a-codeowners-file 2 | 3 | * @Torxed 4 | 5 | # Any PKGBUILD changes should tag grazzolini 6 | /PKGBUILDs/ @grazzolini 7 | /PKGBUILD @grazzolini 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | # See coding conventions in CONTRIBUTING.md 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | 10 | [*.py] 11 | indent_style = tab 12 | indent_size = 4 13 | trim_trailing_whitespace = true 14 | -------------------------------------------------------------------------------- /examples/creds-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "!root-password": "", 3 | "!users": [ 4 | { 5 | "username": "", 6 | "!password": "", 7 | "sudo": false 8 | }, 9 | { 10 | "username": "", 11 | "!password": "", 12 | "sudo": true 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /profiles/applications/httpd.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # Define the package list in order for lib to source 4 | # which packages will be installed by this profile 5 | __packages__ = ["apache"] 6 | 7 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 8 | 9 | archinstall.storage['installation_session'].enable_service('httpd') 10 | -------------------------------------------------------------------------------- /profiles/applications/nginx.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # Define the package list in order for lib to source 4 | # which packages will be installed by this profile 5 | __packages__ = ["nginx"] 6 | 7 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 8 | 9 | archinstall.storage['installation_session'].enable_service('nginx') 10 | -------------------------------------------------------------------------------- /profiles/applications/sshd.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # Define the package list in order for lib to source 4 | # which packages will be installed by this profile 5 | __packages__ = ["openssh"] 6 | 7 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 8 | 9 | archinstall.storage['installation_session'].enable_service('sshd') 10 | -------------------------------------------------------------------------------- /profiles/applications/docker.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # Define the package list in order for lib to source 4 | # which packages will be installed by this profile 5 | __packages__ = ["docker"] 6 | 7 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 8 | 9 | archinstall.storage['installation_session'].enable_service('docker') 10 | -------------------------------------------------------------------------------- /profiles/applications/lighttpd.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # Define the package list in order for lib to source 4 | # which packages will be installed by this profile 5 | __packages__ = ["lighttpd"] 6 | 7 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 8 | 9 | archinstall.storage['installation_session'].enable_service('lighttpd') 10 | -------------------------------------------------------------------------------- /docs/archinstall/Application.rst: -------------------------------------------------------------------------------- 1 | .. _archinstall.Application: 2 | 3 | archinstall.Application 4 | ======================= 5 | 6 | This class enables access to pre-programmed application configurations. 7 | This is not to be confused with :ref:`archinstall.Profile` which is for pre-programmed profiles for a wider set of installation sets. 8 | 9 | 10 | .. autofunction:: archinstall.Application 11 | -------------------------------------------------------------------------------- /docs/archinstall/Installer.rst: -------------------------------------------------------------------------------- 1 | .. _archinstall.Installer: 2 | 3 | archinstall.Installer 4 | ===================== 5 | 6 | The installer is the main class for accessing an installation-instance. 7 | You can look at this class as the installation you have or will perform. 8 | 9 | Anything related to **inside** the installation, will be found in this class. 10 | 11 | .. autofunction:: archinstall.Installer 12 | -------------------------------------------------------------------------------- /profiles/applications/cockpit.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # Define the package list in order for lib to source 4 | # which packages will be installed by this profile 5 | __packages__ = [ 6 | "cockpit", 7 | "udisks2", 8 | "packagekit", 9 | ] 10 | 11 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 12 | 13 | archinstall.storage['installation_session'].enable_service('cockpit.socket') 14 | -------------------------------------------------------------------------------- /archinstall/lib/services.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .general import SysCommand 3 | 4 | 5 | def service_state(service_name: str) -> str: 6 | if os.path.splitext(service_name)[1] != '.service': 7 | service_name += '.service' # Just to be safe 8 | 9 | state = b''.join(SysCommand(f'systemctl show --no-pager -p SubState --value {service_name}', environment_vars={'SYSTEMD_COLORS': '0'})) 10 | 11 | return state.strip().decode('UTF-8') 12 | -------------------------------------------------------------------------------- /.github/workflows/bandit.yaml: -------------------------------------------------------------------------------- 1 | on: [ push, pull_request ] 2 | name: Bandit security checkup 3 | jobs: 4 | flake8: 5 | runs-on: ubuntu-latest 6 | container: 7 | image: archlinux:latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - run: pacman --noconfirm -Syu bandit 11 | - name: Security checkup with Bandit 12 | run: bandit -r archinstall || exit 0 13 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | count = True 3 | # Several of the following could be autofixed or improved by running the code through psf/black 4 | ignore = E123,E126,E128,E203,E231,E261,E302,E402,E722,F541,W191,W292,W293,W503 5 | max-complexity = 40 6 | max-line-length = 236 7 | show-source = True 8 | statistics = True 9 | builtins = _ 10 | per-file-ignores = __init__.py:F401,F403,F405 simple_menu.py:C901,W503 guided.py:C901 network_configuration.py:F821 11 | -------------------------------------------------------------------------------- /archinstall/locales/locales_generator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname "$0")/.. 4 | 5 | find . -type f -iname "*.py" | xargs xgettext --join-existing --no-location --omit-header -d base -o locales/base.pot 6 | 7 | for file in $(find locales/ -name "base.po"); do 8 | echo "Updating: $file" 9 | path=$(dirname $file) 10 | msgmerge --quiet --no-location --width 512 --backup none --update $file locales/base.pot 11 | msgfmt -o $path/base.mo $file 12 | done 13 | -------------------------------------------------------------------------------- /archinstall/lib/menu/text_input.py: -------------------------------------------------------------------------------- 1 | import readline 2 | 3 | 4 | class TextInput: 5 | def __init__(self, prompt: str, prefilled_text=''): 6 | self._prompt = prompt 7 | self._prefilled_text = prefilled_text 8 | 9 | def _hook(self): 10 | readline.insert_text(self._prefilled_text) 11 | readline.redisplay() 12 | 13 | def run(self) -> str: 14 | readline.set_pre_input_hook(self._hook) 15 | result = input(self._prompt) 16 | readline.set_pre_input_hook() 17 | return result 18 | -------------------------------------------------------------------------------- /profiles/applications/postgresql.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # Define the package list in order for lib to source 4 | # which packages will be installed by this profile 5 | __packages__ = ["postgresql"] 6 | 7 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 8 | 9 | archinstall.storage['installation_session'].arch_chroot("initdb -D /var/lib/postgres/data", run_as='postgres') 10 | 11 | archinstall.storage['installation_session'].enable_service('postgresql') 12 | -------------------------------------------------------------------------------- /profiles/applications/mariadb.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # Define the package list in order for lib to source 4 | # which packages will be installed by this profile 5 | __packages__ = ["mariadb"] 6 | 7 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 8 | 9 | archinstall.storage['installation_session'].arch_chroot("mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql") 10 | 11 | archinstall.storage['installation_session'].enable_service('mariadb') 12 | -------------------------------------------------------------------------------- /.github/workflows/flake8.yaml: -------------------------------------------------------------------------------- 1 | on: [ push, pull_request ] 2 | name: flake8 linting (15 ignores) 3 | jobs: 4 | flake8: 5 | runs-on: ubuntu-latest 6 | container: 7 | image: archlinux:latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - run: pacman --noconfirm -Syu python python-pip 11 | - run: python -m pip install --upgrade pip 12 | - run: pip install flake8 13 | - name: Lint with flake8 14 | run: flake8 15 | -------------------------------------------------------------------------------- /archinstall/lib/udev/udevadm.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import pathlib 3 | from ..general import SysCommand 4 | 5 | def udevadm_info(path :pathlib.Path) -> typing.Dict[str, str]: 6 | if path.resolve().exists() is False: 7 | return {} 8 | 9 | result = SysCommand(f"udevadm info {path.resolve()}") 10 | data = {} 11 | for line in result: 12 | if b': ' in line and b'=' in line: 13 | _, obj = line.split(b': ', 1) 14 | key, value = obj.split(b'=', 1) 15 | data[key.decode('UTF-8').lower()] = value.decode('UTF-8').strip() 16 | 17 | return data -------------------------------------------------------------------------------- /profiles/applications/tomcat.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # This is using Tomcat 10 as that is the latest release at the time of implementation. 4 | # This should probably be updated to use newer releases as they come out. 5 | 6 | # Define the package list in order for lib to source 7 | # which packages will be installed by this profile 8 | __packages__ = ["tomcat10"] 9 | 10 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 11 | 12 | archinstall.storage['installation_session'].enable_service('tomcat10') 13 | -------------------------------------------------------------------------------- /archinstall/__main__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import sys 3 | import pathlib 4 | 5 | # Load .git version before the builtin version 6 | if pathlib.Path('./archinstall/__init__.py').absolute().exists(): 7 | spec = importlib.util.spec_from_file_location("archinstall", "./archinstall/__init__.py") 8 | archinstall = importlib.util.module_from_spec(spec) 9 | sys.modules["archinstall"] = archinstall 10 | spec.loader.exec_module(sys.modules["archinstall"]) 11 | else: 12 | import archinstall 13 | 14 | if __name__ == '__main__': 15 | archinstall.run_as_a_module() 16 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yaml: -------------------------------------------------------------------------------- 1 | on: [ push, pull_request ] 2 | name: pytest test validation 3 | jobs: 4 | pytest: 5 | runs-on: ubuntu-latest 6 | container: 7 | image: archlinux:latest 8 | options: --privileged 9 | steps: 10 | - uses: actions/checkout@v3 11 | - run: pacman --noconfirm -Syu python python-pip qemu gcc 12 | - run: python -m pip install --upgrade pip 13 | - run: pip install pytest 14 | - name: Test with pytest 15 | run: python -m pytest || exit 0 16 | -------------------------------------------------------------------------------- /docs/pull_request_template.md: -------------------------------------------------------------------------------- 1 | - This fix issue: 2 | 3 | ## PR Description: 4 | 5 | 6 | 7 | ## Tests and Checks 8 | - [ ] I have tested the code!
9 | 13 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF 17 | formats: 18 | - pdf 19 | 20 | # Optionally set the version of Python and requirements required to build your docs 21 | python: 22 | version: 3.8 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/**__pycache__ 2 | SAFETY_LOCK 3 | **/**old.* 4 | **/**.img 5 | **/**pwfile 6 | **/**build 7 | **/**dist 8 | **/**.egg* 9 | **/**.sh 10 | !archinstall/locales/locales_generator.sh 11 | **/**.egg-info/ 12 | **/**build/ 13 | **/**src/ 14 | **/**pkg/ 15 | **/**dist/ 16 | **/**archinstall.build/ 17 | **/**archinstall-v*/ 18 | **/**.pkg.*.xz 19 | **/**archinstall-*.tar.gz 20 | **/**.zst 21 | **/**.network 22 | **/**.target 23 | **/**.qcow2 24 | /test*.py 25 | **/archiso 26 | /guided.py 27 | /install.log 28 | venv 29 | .venv 30 | .idea/** 31 | **/install.log 32 | .DS_Store 33 | **/cmd_history.txt 34 | **/*.*~ 35 | /*.sig -------------------------------------------------------------------------------- /examples/config-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "audio": null, 3 | "bootloader": "systemd-bootctl", 4 | "harddrives": [ 5 | "/dev/loop0" 6 | ], 7 | "hostname": "", 8 | "kernels": [ 9 | "linux" 10 | ], 11 | "keyboard-layout": "us", 12 | "mirror-region": { 13 | "Worldwide": { 14 | "https://mirror.rackspace.com/archlinux/$repo/os/$arch": true 15 | } 16 | }, 17 | "nic": { 18 | "type": "NM" 19 | }, 20 | "ntp": true, 21 | "packages": [], 22 | "profile": null, 23 | "script": "guided", 24 | "swap": true, 25 | "sys-encoding": "utf-8", 26 | "sys-language": "en_US", 27 | "timezone": "UTC" 28 | } 29 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Dependencies 2 | 3 | In order to build the docs locally, you need to have the following installed: 4 | 5 | - [sphinx-doc](https://www.sphinx-doc.org/en/master/usage/installation.html) 6 | - [sphinx-rdt-theme](https://pypi.org/project/sphinx-rtd-theme/) 7 | 8 | For example, you may install these dependencies using pip: 9 | ``` 10 | pip install -U sphinx sphinx-rtd-theme 11 | ``` 12 | 13 | For other installation methods refer to the docs of the dependencies. 14 | 15 | ## Build 16 | 17 | In `archinstall/docs`, run `make html` (or specify another target) to build locally. The build files will be in `archinstall/docs/_build`. Open `_build/html/index.html` with your browser to see your changes in action. -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /profiles/applications/pipewire.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | import logging 3 | 4 | # Define the package list in order for lib to source 5 | # which packages will be installed by this profile 6 | __packages__ = ["pipewire", "pipewire-alsa", "pipewire-jack", "pipewire-pulse", "gst-plugin-pipewire", "libpulse", "wireplumber"] 7 | 8 | archinstall.log('Installing pipewire', level=logging.INFO) 9 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 10 | 11 | @archinstall.plugin 12 | def on_user_created(installation :archinstall.Installer, user :str): 13 | archinstall.log(f"Enabling pipewire-pulse for {user}", level=logging.INFO) 14 | installation.chroot('systemctl enable --user pipewire-pulse.service', run_as=user) 15 | -------------------------------------------------------------------------------- /examples/unattended.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import archinstall 4 | 5 | archinstall.storage['UPSTREAM_URL'] = 'https://archlinux.life/profiles' 6 | archinstall.storage['PROFILE_DB'] = 'index.json' 7 | 8 | for name, info in archinstall.list_profiles().items(): 9 | # Tailored means it's a match for this machine 10 | # based on it's MAC address (or some other criteria 11 | # that fits the requirements for this machine specifically). 12 | if info['tailored']: 13 | print(f'Found a tailored profile for this machine called: "{name}".') 14 | print('Starting install in:') 15 | for i in range(10, 0, -1): 16 | print(f'{i}...') 17 | time.sleep(1) 18 | 19 | profile = archinstall.Profile(None, info['path']) 20 | profile.install() 21 | break 22 | -------------------------------------------------------------------------------- /docs/archinstall/Profile.rst: -------------------------------------------------------------------------------- 1 | .. _archinstall.Profile: 2 | 3 | archinstall.Profile 4 | =================== 5 | 6 | This class enables access to pre-programmed profiles. 7 | This is not to be confused with :ref:`archinstall.Application` which is for pre-programmed application profiles. 8 | 9 | Profiles in general is a set or group of installation steps. 10 | Where as applications are a specific set of instructions for a very specific application. 11 | 12 | An example would be the *(currently fictional)* profile called `database`. 13 | The profile `database` might contain the application profile `postgresql`. 14 | And that's the difference between :ref:`archinstall.Profile` and :ref:`archinstall.Application`. 15 | 16 | .. autofunction:: archinstall.Profile 17 | -------------------------------------------------------------------------------- /.github/workflows/python-build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build Python packages on every commit. 2 | 3 | name: Build archinstall 4 | 5 | on: [ push, pull_request ] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install build twine 20 | - name: Build archinstall 21 | run: | 22 | python -m build 23 | - uses: actions/upload-artifact@v3 24 | with: 25 | name: archinstall 26 | path: dist/* 27 | -------------------------------------------------------------------------------- /docs/help/discord.rst: -------------------------------------------------------------------------------- 1 | .. _help.discord: 2 | 3 | Discord 4 | ======= 5 | 6 | There's a discord channel which is frequented by some `contributors `_. 7 | 8 | | To join the server, head over to `https://discord.gg/cqXU88y `_ and join in. 9 | | There's not many rules other than common sense and to treat others with respect. The general chat is for off-topic things as well. 10 | 11 | There's the ``@Party Animals`` role if you want notifications of new releases which is posted in the ``#Release Party`` channel. 12 | Another thing is the ``@Contributors`` role can be activated by contributors by writing ``!verify`` and follow the verification process. 13 | 14 | Hop in, we hope to see you there! : ) 15 | -------------------------------------------------------------------------------- /profiles/minimal.py: -------------------------------------------------------------------------------- 1 | # Used to do a minimal install 2 | import archinstall 3 | 4 | is_top_level_profile = True 5 | 6 | __description__ = str(_('A very basic installation that allows you to customize Arch Linux as you see fit.')) 7 | 8 | 9 | def _prep_function(*args, **kwargs): 10 | """ 11 | Magic function called by the importing installer 12 | before continuing any further. For minimal install, 13 | we don't need to do anything special here, but it 14 | needs to exist and return True. 15 | """ 16 | archinstall.storage['profile_minimal'] = True 17 | return True # Do nothing and just return True 18 | 19 | 20 | if __name__ == 'minimal': 21 | """ 22 | This "profile" is a meta-profile. 23 | It is used for a custom minimal installation, without any desktop-specific packages. 24 | """ 25 | -------------------------------------------------------------------------------- /examples/custom-command-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "dry_run": true, 3 | "audio": "none", 4 | "bootloader": "systemd-bootctl", 5 | "debug": false, 6 | "harddrives": [ 7 | "/dev/loop0" 8 | ], 9 | "hostname": "development-box", 10 | "kernels": [ 11 | "linux" 12 | ], 13 | "keyboard-layout": "us", 14 | "mirror-region": "Worldwide", 15 | "nic": { 16 | "type": "NM" 17 | }, 18 | "ntp": true, 19 | "packages": ["docker", "git", "wget", "zsh"], 20 | "services": ["docker"], 21 | "profile": "gnome", 22 | "gfx_driver": "All open-source (default)", 23 | "swap": true, 24 | "sys-encoding": "utf-8", 25 | "sys-language": "en_US", 26 | "timezone": "Europe/Stockholm", 27 | "version": "2.3.1.dev0", 28 | "custom-commands": [ 29 | "cd /home/devel; git clone https://aur.archlinux.org/paru.git", 30 | "chown -R devel:devel /home/devel/paru", 31 | "usermod -aG docker devel" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /archinstall/lib/user_interaction/__init__.py: -------------------------------------------------------------------------------- 1 | from .save_conf import save_config 2 | from .manage_users_conf import ask_for_additional_users 3 | from .backwards_compatible_conf import generic_select, generic_multi_select 4 | from .locale_conf import select_locale_lang, select_locale_enc 5 | from .system_conf import select_kernel, select_harddrives, select_driver, ask_for_bootloader, ask_for_swap 6 | from .network_conf import ask_to_configure_network 7 | from .partitioning_conf import select_partition, select_encrypted_partitions 8 | from .general_conf import (ask_ntp, ask_for_a_timezone, ask_for_audio_selection, select_language, select_mirror_regions, 9 | select_profile, select_archinstall_language, ask_additional_packages_to_install, 10 | select_additional_repositories, ask_hostname, add_number_of_parrallel_downloads) 11 | from .disk_conf import ask_for_main_filesystem_format, select_individual_blockdevice_usage, select_disk_layout, select_disk 12 | from .utils import get_password, do_countdown 13 | -------------------------------------------------------------------------------- /archinstall/lib/pacman.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pathlib 3 | import time 4 | 5 | from .general import SysCommand 6 | from .output import log 7 | 8 | 9 | def run_pacman(args :str, default_cmd :str = 'pacman') -> SysCommand: 10 | """ 11 | A centralized function to call `pacman` from. 12 | It also protects us from colliding with other running pacman sessions (if used locally). 13 | The grace period is set to 10 minutes before exiting hard if another pacman instance is running. 14 | """ 15 | pacman_db_lock = pathlib.Path('/var/lib/pacman/db.lck') 16 | 17 | if pacman_db_lock.exists(): 18 | log(_('Pacman is already running, waiting maximum 10 minutes for it to terminate.'), level=logging.WARNING, fg="red") 19 | 20 | started = time.time() 21 | while pacman_db_lock.exists(): 22 | time.sleep(0.25) 23 | 24 | if time.time() - started > (60 * 10): 25 | log(_('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.'), level=logging.WARNING, fg="red") 26 | exit(1) 27 | 28 | return SysCommand(f'{default_cmd} {args}') 29 | -------------------------------------------------------------------------------- /profiles/applications/awesome.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | __packages__ = [ 4 | "awesome", 5 | "xorg-xrandr", 6 | "xterm", 7 | "feh", 8 | "slock", 9 | "terminus-font", 10 | "gnu-free-fonts", 11 | "ttf-liberation", 12 | "xsel", 13 | ] 14 | 15 | archinstall.storage['installation_session'].install_profile('xorg') 16 | 17 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 18 | 19 | with open(f"{archinstall.storage['installation_session'].target}/etc/X11/xinit/xinitrc", 'r') as xinitrc: 20 | xinitrc_data = xinitrc.read() 21 | 22 | for line in xinitrc_data.split('\n'): 23 | if "twm &" in line: 24 | xinitrc_data = xinitrc_data.replace(line, f"# {line}") 25 | if "xclock" in line: 26 | xinitrc_data = xinitrc_data.replace(line, f"# {line}") 27 | if "xterm" in line: 28 | xinitrc_data = xinitrc_data.replace(line, f"# {line}") 29 | 30 | xinitrc_data += '\n' 31 | xinitrc_data += 'exec awesome\n' 32 | 33 | with open(f"{archinstall.storage['installation_session'].target}/etc/X11/xinit/xinitrc", 'w') as xinitrc: 34 | xinitrc.write(xinitrc_data) 35 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload archinstall to PyPi 5 | 6 | on: 7 | release: 8 | types: [ published ] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install build twine 25 | - name: Build archinstall 26 | run: | 27 | python -m build 28 | - name: Publish archinstall to PyPi 29 | env: 30 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 31 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 32 | run: | 33 | twine upload dist/* 34 | -------------------------------------------------------------------------------- /.github/workflows/mypy.yaml: -------------------------------------------------------------------------------- 1 | on: [ push, pull_request ] 2 | name: mypy type checking 3 | jobs: 4 | mypy: 5 | runs-on: ubuntu-latest 6 | container: 7 | image: archlinux:latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - run: pacman --noconfirm -Syu python mypy python-pip 11 | - run: python -m pip install --upgrade pip 12 | - run: pip install fastapi pydantic 13 | - run: python --version 14 | - run: mypy --version 15 | # one day this will be enabled 16 | # run: mypy --strict --module archinstall || exit 0 17 | - name: run mypy 18 | run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py archinstall/lib/disk/blockdevice.py archinstall/lib/user_interaction/subvolume_config.py archinstall/lib/disk/btrfs/btrfs_helpers.py archinstall/lib/translationhandler.py archinstall/lib/disk/diskinfo.py 19 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = archinstall 3 | version = attr: archinstall.__version__ 4 | description = Arch Linux installer - guided, templates etc. 5 | author = Anton Hvornum 6 | author_email = anton@hvornum.se 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | license = GPL 10 | license_files = 11 | LICENSE 12 | project_urls = 13 | Source = https://github.com/archlinux/archinstall 14 | Documentation = https://archinstall.readthedocs.io/ 15 | classifiers = 16 | Programming Language :: Python :: 3 17 | Programming Language :: Python :: 3.8 18 | Programming Language :: Python :: 3.9 19 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 20 | Operating System :: POSIX :: Linux 21 | 22 | [options] 23 | packages = find: 24 | python_requires = >= 3.8 25 | 26 | [options.packages.find] 27 | include = 28 | archinstall 29 | archinstall.* 30 | 31 | [options.package_data] 32 | archinstall = 33 | examples/*.py 34 | profiles/*.py 35 | profiles/applications/*.py 36 | 37 | [options.entry_points] 38 | console_scripts = 39 | archinstall = archinstall:run_as_a_module 40 | -------------------------------------------------------------------------------- /docs/examples/binary.rst: -------------------------------------------------------------------------------- 1 | .. _examples.binary: 2 | 3 | Binary executable 4 | ================= 5 | 6 | .. warning:: The binary option is limited and stiff. It's hard to modify or create your own installer-scripts this way unless you compile the source manually. If your usecase needs custom scripts, either use the pypi setup method or you'll need to adjust the PKGBUILD prior to building the arch package. 7 | 8 | The binary executable is a standalone compiled version of the library. 9 | It's compiled using `nuitka `_ with the flag `--standalone`. 10 | 11 | Executing the binary 12 | -------------------- 13 | 14 | As an example we'll use the `guided `_ installer. 15 | To run the `guided` installed, all you have to do *(after installing or compiling the binary)*, is run: 16 | 17 | 18 | .. code-block:: console 19 | 20 | ./archinstall guided 21 | 22 | As mentioned, the binary is a bit rudimentary and only supports executing whatever is found directly under `./archinstall/examples`. 23 | Anything else won't be found. This is subject to change in the future to make it a bit more flexible. 24 | -------------------------------------------------------------------------------- /archinstall/lib/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, TYPE_CHECKING 2 | 3 | if TYPE_CHECKING: 4 | from .general import SysCommandWorker 5 | 6 | class RequirementError(BaseException): 7 | pass 8 | 9 | 10 | class DiskError(BaseException): 11 | pass 12 | 13 | 14 | class UnknownFilesystemFormat(BaseException): 15 | pass 16 | 17 | 18 | class ProfileError(BaseException): 19 | pass 20 | 21 | 22 | class SysCallError(BaseException): 23 | def __init__(self, message :str, exit_code :Optional[int] = None, worker :Optional['SysCommandWorker'] = None) -> None: 24 | super(SysCallError, self).__init__(message) 25 | self.message = message 26 | self.exit_code = exit_code 27 | self.worker = worker 28 | 29 | 30 | class PermissionError(BaseException): 31 | pass 32 | 33 | 34 | class ProfileNotFound(BaseException): 35 | pass 36 | 37 | 38 | class HardwareIncompatibilityError(BaseException): 39 | pass 40 | 41 | 42 | class UserError(BaseException): 43 | pass 44 | 45 | 46 | class ServiceException(BaseException): 47 | pass 48 | 49 | 50 | class PackageError(BaseException): 51 | pass 52 | 53 | 54 | class TranslationError(BaseException): 55 | pass 56 | 57 | 58 | class Deprecated(BaseException): 59 | pass -------------------------------------------------------------------------------- /examples/disk_layouts-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "/dev/loop0": { 3 | "partitions": [ 4 | { 5 | "boot": true, 6 | "encrypted": false, 7 | "filesystem": { 8 | "format": "fat32" 9 | }, 10 | "wipe": true, 11 | "mountpoint": "/boot", 12 | "size": "513MB", 13 | "start": "5MB", 14 | "type": "primary" 15 | }, 16 | { 17 | "btrfs": { 18 | "subvolumes": { 19 | "@.snapshots": "/.snapshots", 20 | "@home": "/home", 21 | "@log": "/var/log", 22 | "@pkgs": "/var/cache/pacman/pkg" 23 | } 24 | }, 25 | "encrypted": true, 26 | "filesystem": { 27 | "format": "btrfs" 28 | }, 29 | "wipe": true, 30 | "mountpoint": "/", 31 | "size": "100%", 32 | "start": "518MB", 33 | "type": "primary" 34 | } 35 | ], 36 | "wipe": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /archinstall/lib/disk/diskinfo.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import json 3 | from dataclasses import dataclass, field 4 | from typing import Optional, List 5 | 6 | from ..general import SysCommand 7 | from ..exceptions import DiskError 8 | 9 | @dataclass 10 | class LsblkInfo: 11 | size: int = 0 12 | log_sec: int = 0 13 | pttype: Optional[str] = None 14 | rota: bool = False 15 | tran: Optional[str] = None 16 | ptuuid: Optional[str] = None 17 | partuuid: Optional[str] = None 18 | uuid: Optional[str] = None 19 | fstype: Optional[str] = None 20 | type: Optional[str] = None 21 | mountpoints: List[str] = field(default_factory=list) 22 | 23 | 24 | def get_lsblk_info(dev_path: str) -> LsblkInfo: 25 | fields = [f.name for f in dataclasses.fields(LsblkInfo)] 26 | lsblk_fields = ','.join([f.upper().replace('_', '-') for f in fields]) 27 | 28 | output = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}').decode('UTF-8') 29 | 30 | if output: 31 | block_devices = json.loads(output) 32 | info = block_devices['blockdevices'][0] 33 | lsblk_info = LsblkInfo() 34 | 35 | for f in fields: 36 | setattr(lsblk_info, f, info[f.replace('_', '-')]) 37 | 38 | return lsblk_info 39 | 40 | raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') 41 | -------------------------------------------------------------------------------- /archinstall/lib/user_interaction/locale_conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, TYPE_CHECKING 4 | 5 | from ..locale_helpers import list_locales 6 | from ..menu import Menu 7 | from ..menu.menu import MenuSelectionType 8 | 9 | if TYPE_CHECKING: 10 | _: Any 11 | 12 | 13 | def select_locale_lang(preset: str = None) -> str: 14 | locales = list_locales() 15 | locale_lang = set([locale.split()[0] for locale in locales]) 16 | 17 | selected_locale = Menu( 18 | _('Choose which locale language to use'), 19 | list(locale_lang), 20 | sort=True, 21 | preset_values=preset 22 | ).run() 23 | 24 | match selected_locale.type_: 25 | case MenuSelectionType.Selection: return selected_locale.value 26 | case MenuSelectionType.Esc: return preset 27 | 28 | 29 | def select_locale_enc(preset: str = None) -> str: 30 | locales = list_locales() 31 | locale_enc = set([locale.split()[1] for locale in locales]) 32 | 33 | selected_locale = Menu( 34 | _('Choose which locale encoding to use'), 35 | list(locale_enc), 36 | sort=True, 37 | preset_values=preset 38 | ).run() 39 | 40 | match selected_locale.type_: 41 | case MenuSelectionType.Selection: return selected_locale.value 42 | case MenuSelectionType.Esc: return preset 43 | -------------------------------------------------------------------------------- /archinstall/lib/disk/validators.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def valid_parted_position(pos :str) -> bool: 4 | if not len(pos): 5 | return False 6 | 7 | if pos.isdigit(): 8 | return True 9 | 10 | if pos.lower().endswith('b') and pos[:-1].isdigit(): 11 | return True 12 | 13 | if any(pos.lower().endswith(size) and pos[:-len(size)].replace(".", "", 1).isdigit() 14 | for size in ['%', 'kb', 'mb', 'gb', 'tb', 'kib', 'mib', 'gib', 'tib']): 15 | return True 16 | 17 | return False 18 | 19 | 20 | def fs_types() -> List[str]: 21 | # https://www.gnu.org/software/parted/manual/html_node/mkpart.html 22 | # Above link doesn't agree with `man parted` /mkpart documentation: 23 | """ 24 | fs-type can 25 | be one of "btrfs", "ext2", 26 | "ext3", "ext4", "fat16", 27 | "fat32", "hfs", "hfs+", 28 | "linux-swap", "ntfs", "reis‐ 29 | erfs", "udf", or "xfs". 30 | """ 31 | return [ 32 | "btrfs", 33 | "ext2", 34 | "ext3", "ext4", # `man parted` allows these 35 | "fat16", "fat32", 36 | "hfs", "hfs+", # "hfsx", not included in `man parted` 37 | "linux-swap", 38 | "ntfs", 39 | "reiserfs", 40 | "udf", # "ufs", not included in `man parted` 41 | "xfs", # `man parted` allows this 42 | ] 43 | 44 | 45 | def valid_fs_type(fstype :str) -> bool: 46 | return fstype.lower() in fs_types() 47 | -------------------------------------------------------------------------------- /archinstall/lib/disk/dmcryptdev.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import logging 3 | import json 4 | from dataclasses import dataclass 5 | from typing import Optional 6 | from ..exceptions import SysCallError 7 | from ..general import SysCommand 8 | from ..output import log 9 | from .mapperdev import MapperDev 10 | 11 | @dataclass 12 | class DMCryptDev: 13 | dev_path :pathlib.Path 14 | 15 | @property 16 | def name(self): 17 | with open(f"/sys/devices/virtual/block/{pathlib.Path(self.path).name}/dm/name", "r") as fh: 18 | return fh.read().strip() 19 | 20 | @property 21 | def path(self): 22 | return f"/dev/mapper/{self.dev_path}" 23 | 24 | @property 25 | def blockdev(self): 26 | pass 27 | 28 | @property 29 | def MapperDev(self): 30 | return MapperDev(mappername=self.name) 31 | 32 | @property 33 | def mountpoint(self) -> Optional[str]: 34 | try: 35 | data = json.loads(SysCommand(f"findmnt --json -R {self.dev_path}").decode()) 36 | for filesystem in data['filesystems']: 37 | return filesystem.get('target') 38 | 39 | except SysCallError as error: 40 | # Not mounted anywhere most likely 41 | log(f"Could not locate mount information for {self.dev_path}: {error}", level=logging.WARNING, fg="yellow") 42 | pass 43 | 44 | return None 45 | 46 | @property 47 | def filesystem(self) -> Optional[str]: 48 | return self.MapperDev.filesystem -------------------------------------------------------------------------------- /profiles/cutefish.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "Cutefish" 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | __packages__ = [ 8 | "cutefish", 9 | "noto-fonts", 10 | "sddm" 11 | ] 12 | 13 | 14 | def _prep_function(*args, **kwargs): 15 | """ 16 | Magic function called by the importing installer 17 | before continuing any further. It also avoids executing any 18 | other code in this stage. So it's a safe way to ask the user 19 | for more input before any other installer steps start. 20 | """ 21 | 22 | # Cutefish requires a functional xorg installation. 23 | profile = archinstall.Profile(None, "xorg") 24 | with profile.load_instructions(namespace="xorg.py") as imported: 25 | if hasattr(imported, "_prep_function"): 26 | return imported._prep_function() 27 | else: 28 | print("Deprecated (??): xorg profile has no _prep_function() anymore") 29 | 30 | 31 | # Ensures that this code only gets executed if executed 32 | # through importlib.util.spec_from_file_location("cutefish", "/somewhere/cutefish.py") 33 | # or through conventional import cutefish 34 | if __name__ == "cutefish": 35 | # Install dependency profiles 36 | archinstall.storage["installation_session"].install_profile("xorg") 37 | 38 | # Install the Cutefish packages 39 | archinstall.storage["installation_session"].add_additional_packages(__packages__) 40 | 41 | archinstall.storage["installation_session"].enable_service("sddm") 42 | -------------------------------------------------------------------------------- /profiles/mate.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "MATE" 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | __packages__ = [ 8 | "mate", 9 | "mate-extra", 10 | "lightdm", 11 | "lightdm-gtk-greeter", 12 | ] 13 | 14 | 15 | def _prep_function(*args, **kwargs): 16 | """ 17 | Magic function called by the importing installer 18 | before continuing any further. It also avoids executing any 19 | other code in this stage. So it's a safe way to ask the user 20 | for more input before any other installer steps start. 21 | """ 22 | 23 | # MATE requires a functional xorg installation. 24 | profile = archinstall.Profile(None, 'xorg') 25 | with profile.load_instructions(namespace='xorg.py') as imported: 26 | if hasattr(imported, '_prep_function'): 27 | return imported._prep_function() 28 | else: 29 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 30 | 31 | 32 | # Ensures that this code only gets executed if executed 33 | # through importlib.util.spec_from_file_location("mate", "/somewhere/mate.py") 34 | # or through conventional import mate 35 | if __name__ == 'mate': 36 | # Install dependency profiles 37 | archinstall.storage['installation_session'].install_profile('xorg') 38 | 39 | # Install the MATE packages 40 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 41 | 42 | archinstall.storage['installation_session'].enable_service('lightdm') # Light Display Manager 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.5.1,<4", "setuptools>=45", "wheel"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "archinstall" 7 | dynamic = ["version"] 8 | description = "Arch Linux installer - guided, templates etc." 9 | authors = [ 10 | {name = "Anton Hvornum", email = "anton@hvornum.se"}, 11 | ] 12 | readme = "README.md" 13 | requires-python = ">=3.10" 14 | 15 | keywords = ["linux", "arch", "archinstall", "installer"] 16 | 17 | classifiers = [ 18 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 22 | "Operating System :: POSIX :: Linux", 23 | ] 24 | 25 | [project.urls] 26 | Home = "https://archlinux.org" 27 | Documentation = "https://archinstall.readthedocs.io/" 28 | Source = "https://github.com/archlinux/archinstall" 29 | 30 | [project.scripts] 31 | archinstall = "archinstall:run_as_a_module" 32 | 33 | [project.optional-dependencies] 34 | doc = ["sphinx"] 35 | 36 | [tool.flit.sdist] 37 | include = ["docs/", "profiles", "examples", "archinstall/profiles", "archinstall/examples"] 38 | exclude = ["docs/*.html", "docs/_static", "docs/*.png", "docs/*.psd"] 39 | 40 | [tool.mypy] 41 | python_version = "3.10" 42 | exclude = "tests" 43 | 44 | [tool.bandit] 45 | targets = ["ourkvm"] 46 | exclude = ["/tests"] 47 | -------------------------------------------------------------------------------- /profiles/bspwm.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using the bspwm window manager. 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | __packages__ = [ 8 | 'bspwm', 9 | 'sxhkd', 10 | 'dmenu', 11 | 'xdo', 12 | 'rxvt-unicode', 13 | 'lightdm', 14 | 'lightdm-gtk-greeter', 15 | ] 16 | 17 | def _prep_function(*args, **kwargs): 18 | """ 19 | Magic function called by the importing installer 20 | before continuing any further. It also avoids executing any 21 | other code in this stage. So it's a safe way to ask the user 22 | for more input before any other installer steps start. 23 | """ 24 | 25 | # bspwm requires a functioning Xorg installation. 26 | profile = archinstall.Profile(None, 'xorg') 27 | with profile.load_instructions(namespace='xorg.py') as imported: 28 | if hasattr(imported, '_prep_function'): 29 | return imported._prep_function() 30 | else: 31 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 32 | 33 | 34 | # Ensures that this code only gets executed if executed 35 | # through importlib.util.spec_from_file_location("bspwm", "/somewhere/bspwm.py") 36 | # or through conventional import bspwm 37 | if __name__ == 'bspwm': 38 | # Install dependency profiles 39 | archinstall.storage['installation_session'].install_profile('xorg') 40 | # Install bspwm packages 41 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 42 | # Set up LightDM for login 43 | archinstall.storage['installation_session'].enable_service('lightdm') 44 | -------------------------------------------------------------------------------- /profiles/xfce4.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "Xfce4" 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | __packages__ = [ 8 | "xfce4", 9 | "xfce4-goodies", 10 | "pavucontrol", 11 | "lightdm", 12 | "lightdm-gtk-greeter", 13 | "gvfs", 14 | "xarchiver" 15 | ] 16 | 17 | 18 | def _prep_function(*args, **kwargs): 19 | """ 20 | Magic function called by the importing installer 21 | before continuing any further. It also avoids executing any 22 | other code in this stage. So it's a safe way to ask the user 23 | for more input before any other installer steps start. 24 | """ 25 | 26 | # XFCE requires a functional xorg installation. 27 | profile = archinstall.Profile(None, 'xorg') 28 | with profile.load_instructions(namespace='xorg.py') as imported: 29 | if hasattr(imported, '_prep_function'): 30 | return imported._prep_function() 31 | else: 32 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 33 | 34 | 35 | # Ensures that this code only gets executed if executed 36 | # through importlib.util.spec_from_file_location("xfce4", "/somewhere/xfce4.py") 37 | # or through conventional import xfce4 38 | if __name__ == 'xfce4': 39 | # Install dependency profiles 40 | archinstall.storage['installation_session'].install_profile('xorg') 41 | 42 | # Install the XFCE4 packages 43 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 44 | 45 | archinstall.storage['installation_session'].enable_service('lightdm') # Light Display Manager 46 | -------------------------------------------------------------------------------- /profiles/deepin.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "Deepin". 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | __packages__ = [ 8 | "deepin", 9 | "deepin-terminal", 10 | "deepin-editor", 11 | "lightdm", 12 | "lightdm-deepin-greeter", 13 | ] 14 | 15 | 16 | def _prep_function(*args, **kwargs): 17 | """ 18 | Magic function called by the importing installer 19 | before continuing any further. It also avoids executing any 20 | other code in this stage. So it's a safe way to ask the user 21 | for more input before any other installer steps start. 22 | """ 23 | 24 | # Deepin requires a functioning Xorg installation. 25 | profile = archinstall.Profile(None, 'xorg') 26 | with profile.load_instructions(namespace='xorg.py') as imported: 27 | if hasattr(imported, '_prep_function'): 28 | return imported._prep_function() 29 | else: 30 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 31 | 32 | 33 | # Ensures that this code only gets executed if executed 34 | # through importlib.util.spec_from_file_location("deepin", "/somewhere/deepin.py") 35 | # or through conventional import deepin 36 | if __name__ == 'deepin': 37 | # Install dependency profiles 38 | archinstall.storage['installation_session'].install_profile('xorg') 39 | 40 | # Install the Deepin packages 41 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 42 | 43 | # Enable autostart of Deepin for all users 44 | archinstall.storage['installation_session'].enable_service('lightdm') 45 | -------------------------------------------------------------------------------- /profiles/enlightenment.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "Enlightenment". 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | __packages__ = [ 8 | "enlightenment", 9 | "terminology", 10 | "lightdm", 11 | "lightdm-gtk-greeter", 12 | ] 13 | 14 | 15 | def _prep_function(*args, **kwargs): 16 | """ 17 | Magic function called by the importing installer 18 | before continuing any further. It also avoids executing any 19 | other code in this stage. So it's a safe way to ask the user 20 | for more input before any other installer steps start. 21 | """ 22 | 23 | # Enlightenment requires a functioning Xorg installation. 24 | profile = archinstall.Profile(None, 'xorg') 25 | with profile.load_instructions(namespace='xorg.py') as imported: 26 | if hasattr(imported, '_prep_function'): 27 | return imported._prep_function() 28 | else: 29 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 30 | 31 | 32 | # Ensures that this code only gets executed if executed 33 | # through importlib.util.spec_from_file_location("enlightenment", "/somewhere/enlightenment.py") 34 | # or through conventional import enlightenment 35 | if __name__ == 'enlightenment': 36 | # Install dependency profiles 37 | archinstall.storage['installation_session'].install_profile('xorg') 38 | 39 | # Install the enlightenment packages 40 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 41 | 42 | # Enable autostart of enlightenment for all users 43 | archinstall.storage['installation_session'].enable_service('lightdm') 44 | -------------------------------------------------------------------------------- /archinstall/lib/storage.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # There's a few scenarios of execution: 4 | # 1. In the git repository, where ./profiles/ exist 5 | # 2. When executing from a remote directory, but targeted a script that starts from the git repository 6 | # 3. When executing as a python -m archinstall module where profiles exist one step back for library reasons. 7 | # (4. Added the ~/.config directory as an additional option for future reasons) 8 | # 9 | # And Keeping this in dict ensures that variables are shared across imports. 10 | from typing import Any, Dict 11 | 12 | storage: Dict[str, Any] = { 13 | 'PROFILE_PATH': [ 14 | './profiles', 15 | '~/.config/archinstall/profiles', 16 | os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'profiles'), 17 | # os.path.abspath(f'{os.path.dirname(__file__)}/../examples') 18 | ], 19 | 'UPSTREAM_URL': 'https://raw.githubusercontent.com/archlinux/archinstall/master/profiles', 20 | 'PROFILE_DB': None, # Used in cases when listing profiles is desired, not mandatory for direct profile grabbing. 21 | 'LOG_PATH': '/var/log/archinstall', 22 | 'LOG_FILE': 'install.log', 23 | 'MOUNT_POINT': '/mnt/archinstall', 24 | 'ENC_IDENTIFIER': 'ainst', 25 | 'DISK_TIMEOUTS' : 1, # seconds 26 | 'DISK_RETRY_ATTEMPTS' : 5, # RETRY_ATTEMPTS * DISK_TIMEOUTS is used in disk operations 27 | 'CMD_LOCALE':{'LC_ALL':'C'}, # default locale for execution commands. Can be overridden with set_cmd_locale() 28 | 'CMD_LOCALE_DEFAULT':{'LC_ALL':'C'}, # should be the same as the former. Not be used except in reset_cmd_locale() 29 | } 30 | -------------------------------------------------------------------------------- /profiles/cinnamon.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "Cinnamon" 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | __packages__ = [ 8 | "cinnamon", 9 | "system-config-printer", 10 | "gnome-keyring", 11 | "gnome-terminal", 12 | "blueberry", 13 | "metacity", 14 | "lightdm", 15 | "lightdm-gtk-greeter", 16 | ] 17 | 18 | 19 | def _prep_function(*args, **kwargs): 20 | """ 21 | Magic function called by the importing installer 22 | before continuing any further. It also avoids executing any 23 | other code in this stage. So it's a safe way to ask the user 24 | for more input before any other installer steps start. 25 | """ 26 | 27 | # Cinnamon requires a functioning Xorg installation. 28 | profile = archinstall.Profile(None, 'xorg') 29 | with profile.load_instructions(namespace='xorg.py') as imported: 30 | if hasattr(imported, '_prep_function'): 31 | return imported._prep_function() 32 | else: 33 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 34 | 35 | 36 | # Ensures that this code only gets executed if executed 37 | # through importlib.util.spec_from_file_location("cinnamon", "/somewhere/cinnamon.py") 38 | # or through conventional import cinnamon 39 | if __name__ == 'cinnamon': 40 | # Install dependency profiles 41 | archinstall.storage['installation_session'].install_profile('xorg') 42 | 43 | # Install the Cinnamon packages 44 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 45 | 46 | archinstall.storage['installation_session'].enable_service('lightdm') # Light Display Manager 47 | -------------------------------------------------------------------------------- /profiles/qtile.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "qtile" window manager with common packages. 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | # New way of defining packages for a profile, which is iterable and can be used out side 8 | # of the profile to get a list of "what packages will be installed". 9 | __packages__ = [ 10 | 'qtile', 11 | 'alacritty', 12 | 'lightdm-gtk-greeter', 13 | 'lightdm', 14 | ] 15 | 16 | def _prep_function(*args, **kwargs): 17 | """ 18 | Magic function called by the importing installer 19 | before continuing any further. It also avoids executing any 20 | other code in this stage. So it's a safe way to ask the user 21 | for more input before any other installer steps start. 22 | """ 23 | 24 | # qtile optionally supports xorg, we'll install it since it also 25 | # includes graphic driver setups (this might change in the future) 26 | profile = archinstall.Profile(None, 'xorg') 27 | with profile.load_instructions(namespace='xorg.py') as imported: 28 | if hasattr(imported, '_prep_function'): 29 | return imported._prep_function() 30 | else: 31 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 32 | 33 | 34 | if __name__ == 'qtile': 35 | # Install dependency profiles 36 | archinstall.storage['installation_session'].install_profile('xorg') 37 | 38 | # Install packages for qtile 39 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 40 | 41 | # Auto start lightdm for all users 42 | archinstall.storage['installation_session'].enable_service('lightdm') # Light Display Manager 43 | -------------------------------------------------------------------------------- /profiles/budgie.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "budgie" 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | # "It is recommended also to install the gnome group, which contains applications required for the standard GNOME experience." - Arch Wiki 8 | __packages__ = [ 9 | "budgie-desktop", 10 | "gnome", 11 | "lightdm", 12 | "lightdm-gtk-greeter", 13 | ] 14 | 15 | 16 | def _prep_function(*args, **kwargs): 17 | """ 18 | Magic function called by the importing installer 19 | before continuing any further. It also avoids executing any 20 | other code in this stage. So it's a safe way to ask the user 21 | for more input before any other installer steps start. 22 | """ 23 | 24 | # budgie requires a functioning Xorg installation. 25 | profile = archinstall.Profile(None, 'xorg') 26 | with profile.load_instructions(namespace='xorg.py') as imported: 27 | if hasattr(imported, '_prep_function'): 28 | return imported._prep_function() 29 | else: 30 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 31 | 32 | 33 | # Ensures that this code only gets executed if executed 34 | # through importlib.util.spec_from_file_location("budgie", "/somewhere/budgie.py") 35 | # or through conventional import budgie 36 | if __name__ == 'budgie': 37 | # Install dependency profiles 38 | archinstall.storage['installation_session'].install_profile('xorg') 39 | 40 | # Install the Budgie packages 41 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 42 | 43 | archinstall.storage['installation_session'].enable_service('lightdm') # Light Display Manager 44 | -------------------------------------------------------------------------------- /docs/installing/python.rst: -------------------------------------------------------------------------------- 1 | .. _installing.python: 2 | 3 | Python library 4 | ============== 5 | 6 | Archinstall ships on `PyPi `_ as `archinstall `_. 7 | But the library can be installed manually as well. 8 | 9 | .. warning:: 10 | These steps are not required if you want to use archinstall on the official Arch Linux ISO. 11 | 12 | Installing with pacman 13 | ---------------------- 14 | 15 | Archinstall is on the `official repositories `_. 16 | And it will also install archinstall as a python library. 17 | 18 | To install both the library and the archinstall script: 19 | 20 | .. code-block:: console 21 | 22 | pacman -S archinstall 23 | 24 | Alternatively, you can install only the library and not the helper executable using the ``python-archinstall`` package. 25 | 26 | Installing with PyPi 27 | -------------------- 28 | 29 | The basic concept of PyPi applies using `pip`. 30 | 31 | .. code-block:: console 32 | 33 | pip install archinstall 34 | 35 | .. _installing.python.manual: 36 | 37 | Install using source code 38 | ------------------------- 39 | 40 | | You can also install using the source code. 41 | | For sake of simplicity we will use ``git clone`` in this example. 42 | 43 | .. code-block:: console 44 | 45 | git clone https://github.com/archlinux/archinstall 46 | 47 | You can either move the folder into your project and simply do 48 | 49 | .. code-block:: python 50 | 51 | import archinstall 52 | 53 | Or you can use `setuptools `_ to install it into the module path. 54 | 55 | .. code-block:: console 56 | 57 | sudo python setup.py install -------------------------------------------------------------------------------- /profiles/gnome.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "Gnome" 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | # Note: GDM should be part of the gnome group, but adding it here for clarity 8 | __packages__ = [ 9 | "gnome", 10 | "gnome-tweaks", 11 | "gdm" 12 | ] 13 | 14 | 15 | def _prep_function(*args, **kwargs): 16 | """ 17 | Magic function called by the importing installer 18 | before continuing any further. It also avoids executing any 19 | other code in this stage. So it's a safe way to ask the user 20 | for more input before any other installer steps start. 21 | """ 22 | 23 | # Gnome optionally supports xorg, we'll install it since it also 24 | # includes graphic driver setups (this might change in the future) 25 | profile = archinstall.Profile(None, 'xorg') 26 | with profile.load_instructions(namespace='xorg.py') as imported: 27 | if hasattr(imported, '_prep_function'): 28 | return imported._prep_function() 29 | else: 30 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 31 | 32 | 33 | # Ensures that this code only gets executed if executed 34 | # through importlib.util.spec_from_file_location("gnome", "/somewhere/gnome.py") 35 | # or through conventional import gnome 36 | if __name__ == 'gnome': 37 | # Install dependency profiles 38 | archinstall.storage['installation_session'].install_profile('xorg') 39 | 40 | # Install the GNOME packages 41 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 42 | 43 | archinstall.storage['installation_session'].enable_service('gdm') # Gnome Display Manager 44 | # We could also start it via xinitrc since we do have Xorg, 45 | # but for gnome that's deprecated and wayland is preferred. 46 | -------------------------------------------------------------------------------- /profiles/lxqt.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "LXQt" 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | # NOTE: SDDM is the only officially supported greeter for LXQt, so unlike other DEs, lightdm is not used here. 8 | # LXQt works with lightdm, but since this is not supported, we will not default to this. 9 | # https://github.com/lxqt/lxqt/issues/795 10 | __packages__ = [ 11 | "lxqt", 12 | "breeze-icons", 13 | "oxygen-icons", 14 | "xdg-utils", 15 | "ttf-freefont", 16 | "leafpad", 17 | "slock", 18 | "sddm", 19 | ] 20 | 21 | 22 | def _prep_function(*args, **kwargs): 23 | """ 24 | Magic function called by the importing installer 25 | before continuing any further. It also avoids executing any 26 | other code in this stage. So it's a safe way to ask the user 27 | for more input before any other installer steps start. 28 | """ 29 | 30 | # LXQt requires a functional xorg installation. 31 | profile = archinstall.Profile(None, 'xorg') 32 | with profile.load_instructions(namespace='xorg.py') as imported: 33 | if hasattr(imported, '_prep_function'): 34 | return imported._prep_function() 35 | else: 36 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 37 | 38 | 39 | # Ensures that this code only gets executed if executed 40 | # through importlib.util.spec_from_file_location("lxqt", "/somewhere/lxqt.py") 41 | # or through conventional import lxqt 42 | if __name__ == 'lxqt': 43 | # Install dependency profiles 44 | archinstall.storage['installation_session'].install_profile('xorg') 45 | 46 | # Install the LXQt packages 47 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 48 | 49 | # Enable autostart of LXQt for all users 50 | archinstall.storage['installation_session'].enable_service('sddm') 51 | -------------------------------------------------------------------------------- /docs/help/issues.rst: -------------------------------------------------------------------------------- 1 | .. _help.issues: 2 | 3 | Issue tracker & bugs 4 | ==================== 5 | 6 | Issues and bugs should be reported over at `https://github.com/archlinux/archinstall/issues `_. 7 | 8 | General questions, enhancements and security issues can be reported over there too. 9 | For quick issues or if you need help, head over to the Discord server which has a help channel. 10 | 11 | Log files 12 | --------- 13 | 14 | | When submitting a help ticket, please include the :code:`/var/log/archinstall/install.log`. 15 | | It can be found both on the live ISO but also in the installed filesystem if the base packages were strapped in. 16 | 17 | .. tip:: 18 | | An easy way to submit logs is ``curl -F'file=@/var/log/archinstall/install.log' https://0x0.st``. 19 | | Use caution when submitting other log files, but ``archinstall`` pledges to keep ``install.log`` safe for posting publicly! 20 | 21 | | There are additional log files under ``/var/log/archinstall/`` that can be useful: 22 | 23 | - ``/var/log/archinstall/user_configuration.json`` - Stores most of the guided answers in the installer 24 | - ``/var/log/archinstall/user_credentials.json`` - Stores any usernames or passwords, can be passed to ``--creds`` 25 | - ``/var/log/archinstall/user_disk_layouts.json`` - Stores the chosen disks and their layouts 26 | - ``/var/log/archinstall/install.log`` - A log file over what steps were taken by archinstall 27 | - ``/var/log/archinstall/cmd_history.txt`` - A complete command history, command by command in order 28 | - ``/var/log/archinstall/cmd_output.txt`` - A raw output from all the commands that were executed by archinstall 29 | 30 | .. warning:: 31 | 32 | We only try to guarantee that ``/var/log/archinstall/install.log`` is free from sensitive information. 33 | Any other log file should be pasted with **utmost care**! 34 | -------------------------------------------------------------------------------- /profiles/kde.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "KDE". 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | __packages__ = [ 8 | "plasma-meta", 9 | "konsole", 10 | "kwrite", 11 | "dolphin", 12 | "ark", 13 | "sddm", 14 | "plasma-wayland-session", 15 | "egl-wayland" 16 | ] 17 | 18 | 19 | # TODO: Remove hard dependency of bash (due to .bash_profile) 20 | 21 | 22 | def _prep_function(*args, **kwargs): 23 | """ 24 | Magic function called by the importing installer 25 | before continuing any further. It also avoids executing any 26 | other code in this stage. So it's a safe way to ask the user 27 | for more input before any other installer steps start. 28 | """ 29 | 30 | # KDE requires a functioning Xorg installation. 31 | profile = archinstall.Profile(None, 'xorg') 32 | with profile.load_instructions(namespace='xorg.py') as imported: 33 | if hasattr(imported, '_prep_function'): 34 | return imported._prep_function() 35 | else: 36 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 37 | 38 | 39 | """ 40 | def _post_install(*args, **kwargs): 41 | if "nvidia" in _gfx_driver_packages: 42 | print("Plasma Wayland has known compatibility issues with the proprietary Nvidia driver") 43 | print("After booting, you can choose between Wayland and Xorg using the drop-down menu") 44 | return True 45 | """ 46 | 47 | # Ensures that this code only gets executed if executed 48 | # through importlib.util.spec_from_file_location("kde", "/somewhere/kde.py") 49 | # or through conventional import kde 50 | if __name__ == 'kde': 51 | # Install dependency profiles 52 | archinstall.storage['installation_session'].install_profile('xorg') 53 | 54 | # Install the KDE packages 55 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 56 | 57 | # Enable autostart of KDE for all users 58 | archinstall.storage['installation_session'].enable_service('sddm') 59 | -------------------------------------------------------------------------------- /profiles/server.py: -------------------------------------------------------------------------------- 1 | # Used to select various server application profiles on top of a minimal installation. 2 | 3 | import logging 4 | from typing import Any, TYPE_CHECKING 5 | 6 | import archinstall 7 | from archinstall import Menu 8 | from archinstall.lib.menu.menu import MenuSelectionType 9 | 10 | if TYPE_CHECKING: 11 | _: Any 12 | 13 | is_top_level_profile = True 14 | 15 | __description__ = str(_('Provides a selection of various server packages to install and enable, e.g. httpd, nginx, mariadb')) 16 | 17 | available_servers = [ 18 | "cockpit", 19 | "docker", 20 | "httpd", 21 | "lighttpd", 22 | "mariadb", 23 | "nginx", 24 | "postgresql", 25 | "sshd", 26 | "tomcat", 27 | ] 28 | 29 | 30 | def _prep_function(*args, **kwargs): 31 | """ 32 | Magic function called by the importing installer 33 | before continuing any further. 34 | """ 35 | choice = Menu(str(_( 36 | 'Choose which servers to install, if none then a minimal installation will be done')), 37 | available_servers, 38 | preset_values=kwargs['servers'], 39 | multi=True 40 | ).run() 41 | 42 | if choice.type_ != MenuSelectionType.Selection: 43 | return False 44 | 45 | if choice.value: 46 | archinstall.storage['_selected_servers'] = choice.value 47 | return True 48 | 49 | return False 50 | 51 | 52 | if __name__ == 'server': 53 | """ 54 | This "profile" is a meta-profile. 55 | """ 56 | archinstall.log('Now installing the selected servers.', level=logging.INFO) 57 | archinstall.log(archinstall.storage['_selected_servers'], level=logging.DEBUG) 58 | for server in archinstall.storage['_selected_servers']: 59 | archinstall.log(f'Installing {server} ...', level=logging.INFO) 60 | app = archinstall.Application(archinstall.storage['installation_session'], server) 61 | app.install() 62 | 63 | archinstall.log('If your selections included multiple servers with the same port, you may have to reconfigure them.', fg="yellow", level=logging.INFO) 64 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: David Runge 2 | # Maintainer: Giancarlo Razzolini 3 | # Contributor: Anton Hvornum 4 | # Contributor: demostanis worlds 5 | 6 | pkgname=archinstall 7 | pkgver=2.5.0 8 | #pkgver=$(git describe --long | sed 's/\([^-]*-g\)/r\1/;s/-/./g') 9 | pkgrel=1 10 | pkgdesc="Just another guided/automated Arch Linux installer with a twist" 11 | arch=(any) 12 | url="https://github.com/archlinux/archinstall" 13 | license=(GPL3) 14 | depends=(python) 15 | makedepends=(python-build python-installer python-flit python-setuptools python-sphinx python-wheel) 16 | provides=(python-archinstall) 17 | conflicts=(python-archinstall) 18 | replaces=(python-archinstall) 19 | source=( 20 | $pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz 21 | $pkgname-$pkgver.tar.gz.sig::$url/releases/download/v$pkgver/$pkgname-$pkgver.tar.gz.sig 22 | ) 23 | sha512sums=('9516719c4e4fe0423224a35b4846cf5c8daeb931cff6fed588957840edc5774e9c6fe18619d2356a6d76681ae3216ba19f5d0f0bd89c6301b4ff9b128d037d13' 24 | 'SKIP') 25 | b2sums=('a29ae767756f74ce296d53e31bb8376cfa7db19a53b8c3997b2d8747a60842ba88e8b18c505bc56a36d685f73f7a6d9e53adff17953c8a4ebaabc67c6db8e583' 26 | 'SKIP') 27 | validpgpkeys=('256F73CEEFC6705C6BBAB20E5FBBB32941E3740A') # Anton Hvornum (Torxed) 28 | 29 | prepare() { 30 | cd $pkgname-$pkgver 31 | # use real directories for examples and profiles, as symlinks do not work 32 | # with flit or setuptools PEP517 backends 33 | rm -fv $pkgname/{examples,profiles} 34 | mv -v examples profiles $pkgname/ 35 | } 36 | 37 | build() { 38 | cd $pkgname-$pkgver 39 | python -m build --wheel --no-isolation 40 | PYTHONDONTWRITEBYTECODE=1 make man -C docs 41 | } 42 | 43 | package() { 44 | cd "$pkgname-$pkgver" 45 | python -m installer --destdir="$pkgdir" dist/*.whl 46 | install -vDm 644 docs/_build/man/archinstall.1 -t "$pkgdir/usr/share/man/man1/" 47 | } -------------------------------------------------------------------------------- /archinstall/lib/models/subvolume.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Any, Dict 3 | 4 | 5 | @dataclass 6 | class Subvolume: 7 | name: str 8 | mountpoint: str 9 | compress: bool = False 10 | nodatacow: bool = False 11 | 12 | def display(self) -> str: 13 | options_str = ','.join(self.options) 14 | return f'{_("Subvolume")}: {self.name:15} {_("Mountpoint")}: {self.mountpoint:20} {_("Options")}: {options_str}' 15 | 16 | @property 17 | def options(self) -> List[str]: 18 | options = [ 19 | 'compress' if self.compress else '', 20 | 'nodatacow' if self.nodatacow else '' 21 | ] 22 | return [o for o in options if len(o)] 23 | 24 | def json(self) -> Dict[str, Any]: 25 | return { 26 | 'name': self.name, 27 | 'mountpoint': self.mountpoint, 28 | 'compress': self.compress, 29 | 'nodatacow': self.nodatacow 30 | } 31 | 32 | @classmethod 33 | def _parse(cls, config_subvolumes: List[Dict[str, Any]]) -> List['Subvolume']: 34 | subvolumes = [] 35 | for entry in config_subvolumes: 36 | if not entry.get('name', None) or not entry.get('mountpoint', None): 37 | continue 38 | 39 | subvolumes.append( 40 | Subvolume( 41 | entry['name'], 42 | entry['mountpoint'], 43 | entry.get('compress', False), 44 | entry.get('nodatacow', False) 45 | ) 46 | ) 47 | 48 | return subvolumes 49 | 50 | @classmethod 51 | def _parse_backwards_compatible(cls, config_subvolumes) -> List['Subvolume']: 52 | subvolumes = [] 53 | for name, mountpoint in config_subvolumes.items(): 54 | if not name or not mountpoint: 55 | continue 56 | 57 | subvolumes.append(Subvolume(name, mountpoint)) 58 | 59 | return subvolumes 60 | 61 | @classmethod 62 | def parse_arguments(cls, config_subvolumes: Any) -> List['Subvolume']: 63 | if isinstance(config_subvolumes, list): 64 | return cls._parse(config_subvolumes) 65 | elif isinstance(config_subvolumes, dict): 66 | return cls._parse_backwards_compatible(config_subvolumes) 67 | 68 | raise ValueError('Unknown disk layout btrfs subvolume format') 69 | -------------------------------------------------------------------------------- /docs/installing/binary.rst: -------------------------------------------------------------------------------- 1 | .. _installing.binary: 2 | 3 | Binary executable 4 | ================= 5 | 6 | Archinstall can be compiled into a standalone executable. 7 | For Arch Linux based systems, there's a package for this called `archinstall `_. 8 | 9 | .. warning:: 10 | This is not required if you're running archinstall on a pre-built ISO. The installation is only required if you're creating your own scripted installations. 11 | 12 | Using pacman 13 | ------------ 14 | 15 | Archinstall is on the `official repositories `_. 16 | 17 | .. code-block:: console 18 | 19 | sudo pacman -S archinstall 20 | 21 | Using PKGBUILD 22 | -------------- 23 | 24 | The `source `_ contains a binary `PKGBUILD `_ which can be either copied straight off the website or cloned using :code:`git clone https://github.com/Torxed/archinstall`. 25 | 26 | Once you've obtained the `PKGBUILD`, building it is pretty straight forward. 27 | 28 | .. code-block:: console 29 | 30 | makepkg -s 31 | 32 | Which should produce an `archinstall-X.x.z-1.pkg.tar.zst` which can be installed using: 33 | 34 | .. code-block:: console 35 | 36 | sudo pacman -U archinstall-X.x.z-1.pkg.tar.zst 37 | 38 | .. note:: 39 | 40 | For a complete guide on the build process, please consult the `PKGBUILD on ArchWiki `_. 41 | 42 | Manual compilation 43 | ------------------ 44 | 45 | You can compile the source manually without using a custom mirror or the `PKGBUILD` that is shipped. 46 | Simply clone or download the source, and while standing in the cloned folder `./archinstall`, execute: 47 | 48 | .. code-block:: console 49 | 50 | nuitka3 --standalone --show-progress archinstall 51 | 52 | This requires the `nuitka `_ package as well as `python3` to be installed locally. 53 | -------------------------------------------------------------------------------- /profiles/sway.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "Sway" 2 | import archinstall 3 | from archinstall import Menu 4 | 5 | is_top_level_profile = False 6 | 7 | __packages__ = [ 8 | "sway", 9 | "swaylock", 10 | "swayidle", 11 | "waybar", 12 | "dmenu", 13 | "light", 14 | "grim", 15 | "slurp", 16 | "pavucontrol", 17 | "foot", 18 | ] 19 | 20 | 21 | def _check_driver() -> bool: 22 | packages = archinstall.storage.get("gfx_driver_packages", []) 23 | 24 | if packages and "nvidia" in packages: 25 | prompt = 'The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?' 26 | choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run() 27 | 28 | if choice.value == Menu.no(): 29 | return False 30 | 31 | return True 32 | 33 | 34 | def _prep_function(*args, **kwargs): 35 | """ 36 | Magic function called by the importing installer 37 | before continuing any further. It also avoids executing any 38 | other code in this stage. So it's a safe way to ask the user 39 | for more input before any other installer steps start. 40 | """ 41 | driver = archinstall.select_driver() 42 | 43 | if driver: 44 | archinstall.storage["gfx_driver_packages"] = driver 45 | if not _check_driver(): 46 | return _prep_function(args, kwargs) 47 | return True 48 | 49 | return False 50 | 51 | 52 | # Ensures that this code only gets executed if executed 53 | # through importlib.util.spec_from_file_location("sway", "/somewhere/sway.py") 54 | # or through conventional import sway 55 | if __name__ == "sway": 56 | if not _check_driver(): 57 | raise archinstall.lib.exceptions.HardwareIncompatibilityError("Sway does not support the proprietary nvidia drivers.") 58 | 59 | # Install the Sway packages 60 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 61 | 62 | # Install the graphics driver packages 63 | archinstall.storage['installation_session'].add_additional_packages(f"xorg-server xorg-xinit {' '.join(archinstall.storage.get('gfx_driver_packages', None))}") 64 | -------------------------------------------------------------------------------- /archinstall/lib/user_interaction/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import getpass 4 | import signal 5 | import sys 6 | import time 7 | from typing import Any, Optional, TYPE_CHECKING 8 | 9 | from ..menu import Menu 10 | from ..models.password_strength import PasswordStrength 11 | from ..output import log 12 | 13 | if TYPE_CHECKING: 14 | _: Any 15 | 16 | # used for signal handler 17 | SIG_TRIGGER = None 18 | 19 | 20 | def get_password(prompt: str = '') -> Optional[str]: 21 | if not prompt: 22 | prompt = _("Enter a password: ") 23 | 24 | while password := getpass.getpass(prompt): 25 | if len(password.strip()) <= 0: 26 | break 27 | 28 | strength = PasswordStrength.strength(password) 29 | log(f'Password strength: {strength.value}', fg=strength.color()) 30 | 31 | passwd_verification = getpass.getpass(prompt=_('And one more time for verification: ')) 32 | if password != passwd_verification: 33 | log(' * Passwords did not match * ', fg='red') 34 | continue 35 | 36 | return password 37 | 38 | return None 39 | 40 | 41 | def do_countdown() -> bool: 42 | SIG_TRIGGER = False 43 | 44 | def kill_handler(sig: int, frame: Any) -> None: 45 | print() 46 | exit(0) 47 | 48 | def sig_handler(sig: int, frame: Any) -> None: 49 | global SIG_TRIGGER 50 | SIG_TRIGGER = True 51 | signal.signal(signal.SIGINT, kill_handler) 52 | 53 | original_sigint_handler = signal.getsignal(signal.SIGINT) 54 | signal.signal(signal.SIGINT, sig_handler) 55 | 56 | for i in range(5, 0, -1): 57 | print(f"{i}", end='') 58 | 59 | for x in range(4): 60 | sys.stdout.flush() 61 | time.sleep(0.25) 62 | print(".", end='') 63 | 64 | if SIG_TRIGGER: 65 | prompt = _('Do you really want to abort?') 66 | choice = Menu(prompt, Menu.yes_no(), skip=False).run() 67 | if choice.value == Menu.yes(): 68 | exit(0) 69 | 70 | if SIG_TRIGGER is False: 71 | sys.stdin.read() 72 | 73 | SIG_TRIGGER = False 74 | signal.signal(signal.SIGINT, sig_handler) 75 | 76 | print() 77 | signal.signal(signal.SIGINT, original_sigint_handler) 78 | 79 | return True 80 | -------------------------------------------------------------------------------- /profiles/awesome.py: -------------------------------------------------------------------------------- 1 | # A desktop environment using "Awesome" window manager. 2 | 3 | import archinstall 4 | 5 | is_top_level_profile = False 6 | 7 | # New way of defining packages for a profile, which is iterable and can be used out side 8 | # of the profile to get a list of "what packages will be installed". 9 | __packages__ = [ 10 | "alacritty", 11 | ] 12 | 13 | 14 | def _prep_function(*args, **kwargs): 15 | """ 16 | Magic function called by the importing installer 17 | before continuing any further. It also avoids executing any 18 | other code in this stage. So it's a safe way to ask the user 19 | for more input before any other installer steps start. 20 | """ 21 | 22 | # Awesome WM requires that xorg is installed 23 | profile = archinstall.Profile(None, 'xorg') 24 | with profile.load_instructions(namespace='xorg.py') as imported: 25 | if hasattr(imported, '_prep_function'): 26 | return imported._prep_function() 27 | else: 28 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 29 | 30 | 31 | # Ensures that this code only gets executed if executed 32 | # through importlib.util.spec_from_file_location("awesome", "/somewhere/awesome.py") 33 | # or through conventional import awesome 34 | if __name__ == 'awesome': 35 | # Install the application awesome from the template under /applications/ 36 | awesome = archinstall.Application(archinstall.storage['installation_session'], 'awesome') 37 | awesome.install() 38 | 39 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 40 | 41 | # TODO: Copy a full configuration to ~/.config/awesome/rc.lua instead. 42 | with open(f"{archinstall.storage['installation_session'].target}/etc/xdg/awesome/rc.lua", 'r') as fh: 43 | awesome_lua = fh.read() 44 | 45 | # Replace xterm with alacritty for a smoother experience. 46 | awesome_lua = awesome_lua.replace('"xterm"', '"alacritty"') 47 | 48 | with open(f"{archinstall.storage['installation_session'].target}/etc/xdg/awesome/rc.lua", 'w') as fh: 49 | fh.write(awesome_lua) 50 | 51 | # TODO: Configure the right-click-menu to contain the above packages that were installed. (as a user config) -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | archinstall Documentation 2 | ========================= 3 | 4 | | **archinstall** is library which can be used to install Arch Linux. 5 | | The library comes packaged with different pre-configured installers, such as the default :ref:`guided` installer. 6 | | 7 | | A demo of the :ref:`guided` installer can be seen here: `https://www.youtube.com/watch?v=9Xt7X_Iqg6E `_. 8 | 9 | Some of the features of Archinstall are: 10 | 11 | * **No external dependencies or installation requirements.** Runs without any external requirements or installation processes. 12 | 13 | * **Context friendly.** The library always executes calls in sequential order to ensure installation-steps don't overlap or execute in the wrong order. It also supports *(and uses)* context wrappers to ensure cleanup and final tasks such as ``mkinitcpio`` are called when needed. 14 | 15 | * **Full transparency** Logs and insights can be found at ``/var/log/archinstall`` both in the live ISO and the installed system. 16 | 17 | * **Accessibility friendly** Archinstall works with ``espeakup`` and other accessibility tools thanks to the use of a TUI. 18 | 19 | .. toctree:: 20 | :maxdepth: 3 21 | :caption: Running the installer 22 | 23 | installing/guided 24 | 25 | .. toctree:: 26 | :maxdepth: 3 27 | :caption: Getting help 28 | 29 | help/discord 30 | help/issues 31 | 32 | .. toctree:: 33 | :maxdepth: 3 34 | :caption: Archinstall as a library 35 | 36 | installing/python 37 | examples/python 38 | 39 | .. toctree:: 40 | :maxdepth: 3 41 | :caption: Archinstall as a binary 42 | 43 | installing/binary 44 | examples/binary 45 | .. 46 | examples/scripting 47 | 48 | .. 49 | .. toctree:: 50 | :maxdepth: 3 51 | :caption: Programming Guide 52 | 53 | .. 54 | programming_guide/requirements 55 | programming_guide/basic_concept 56 | 57 | .. toctree:: 58 | :maxdepth: 3 59 | :caption: API Reference 60 | 61 | archinstall/Installer 62 | archinstall/Profile 63 | archinstall/Application 64 | 65 | .. toctree:: 66 | :maxdepth: 3 67 | :caption: API Helper functions 68 | 69 | archinstall/general 70 | -------------------------------------------------------------------------------- /.github/workflows/iso-build.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will build an Arch Linux ISO file with the commit on it 2 | 3 | name: Build Arch ISO with ArchInstall Commit 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | - main # In case we adopt this convention in the future 10 | pull_request: 11 | paths-ignore: 12 | - 'docs/**' 13 | - '**.editorconfig' 14 | - '**.gitignore' 15 | - '**.md' 16 | - 'LICENSE' 17 | - 'PKGBUILD' 18 | release: 19 | types: 20 | - created 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | container: 26 | image: archlinux:latest 27 | options: --privileged 28 | steps: 29 | - uses: actions/checkout@v3 30 | - run: pwd 31 | - run: find . 32 | - run: cat /etc/os-release 33 | - run: pacman-key --init 34 | - run: pacman --noconfirm -Sy archlinux-keyring 35 | - run: mkdir -p /tmp/archlive/airootfs/root/archinstall-git; cp -r . /tmp/archlive/airootfs/root/archinstall-git 36 | - 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 37 | - run: echo "echo \"This is an unofficial ISO for development and testing of archinstall. No support will be provided.\"" >> /tmp/archlive/airootfs/root/.zprofile 38 | - run: echo "echo \"This ISO was built from Git SHA $GITHUB_SHA\"" >> /tmp/archlive/airootfs/root/.zprofile 39 | - run: echo "echo \"Type archinstall to launch the installer.\"" >> /tmp/archlive/airootfs/root/.zprofile 40 | - run: cat /tmp/archlive/airootfs/root/.zprofile 41 | - run: pacman -Sy; pacman --noconfirm -S git archiso 42 | - run: cp -r /usr/share/archiso/configs/releng/* /tmp/archlive 43 | - run: echo -e "git\npython\npython-pip\npython-build\npython-flit\npython-setuptools\npython-wheel" >> /tmp/archlive/packages.x86_64 44 | - run: find /tmp/archlive 45 | - run: cd /tmp/archlive; mkarchiso -v -w work/ -o out/ ./ 46 | - uses: actions/upload-artifact@v3 47 | with: 48 | name: Arch Live ISO 49 | path: /tmp/archlive/out/*.iso 50 | -------------------------------------------------------------------------------- /profiles/52-54-00-12-34-56.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # import json 4 | # import urllib.request 5 | 6 | __packages__ = ['nano', 'wget', 'git'] 7 | 8 | if __name__ == '52-54-00-12-34-56': 9 | awesome = archinstall.Application(archinstall.storage['installation_session'], 'postgresql') 10 | awesome.install() 11 | 12 | """ 13 | # Unmount and close previous runs (Mainly only used for re-runs, but won't hurt.) 14 | archinstall.sys_command(f'umount -R /mnt', suppress_errors=True) 15 | archinstall.sys_command(f'cryptsetup close /dev/mapper/luksloop', suppress_errors=True) 16 | 17 | # Select a harddrive and a disk password 18 | harddrive = archinstall.all_blockdevices()['/dev/sda'] 19 | disk_password = '1234' 20 | 21 | with archinstall.Filesystem(harddrive) as fs: 22 | # Use the entire disk instead of setting up partitions on your own 23 | fs.use_entire_disk('luks2') 24 | 25 | if harddrive.partition[1].size == '512M': 26 | raise OSError('Trying to encrypt the boot partition for Pete's sake..') 27 | harddrive.partition[0].format('fat32') 28 | 29 | with archinstall.luks2(harddrive.partition[1], 'luksloop', disk_password) as unlocked_device: 30 | unlocked_device.format('btrfs') 31 | 32 | with archinstall.Installer( 33 | unlocked_device, 34 | boot_partition=harddrive.partition[0], 35 | hostname="testmachine" 36 | ) as installation: 37 | if installation.minimal_installation(): 38 | installation.add_bootloader() 39 | 40 | installation.add_additional_packages(__packages__) 41 | installation.install_profile('awesome') 42 | 43 | user = User('devel', 'devel', False) 44 | installation.create_users(user) 45 | installation.user_set_pw('root', 'toor') 46 | 47 | print(f'Submitting {archinstall.__version__}: success') 48 | 49 | conditions = { 50 | "project": "archinstall", 51 | "profile": "52-54-00-12-34-56", 52 | "status": "success", 53 | "version": archinstall.__version__ 54 | } 55 | req = urllib.request.Request("https://api.archlinux.life/build/success", 56 | data=json.dumps(conditions).encode('utf8'), 57 | headers={'content-type': 'application/json'}) 58 | try: 59 | urllib.request.urlopen(req, timeout=5) 60 | except: 61 | pass 62 | """ 63 | -------------------------------------------------------------------------------- /docs/flowcharts/DiskSelectionProcess.drawio: -------------------------------------------------------------------------------- 1 | 7VvZdqM4EP2anHlKDpsxPMZ20sl0kl7S05meNwVkYCIjt5C3+fqRjFgFNnbwksQv3a5CCKG6dWsROdP7o/knAsb+PXYhOtMUd36mD840TVUsi/3HNYtYY5tC4ZHAFYMyxWPwH0zuFNpJ4MKoMJBijGgwLiodHIbQoQUdIATPisOGGBWfOgYelBSPDkCy9ilwqR9rLa2b6W9g4PnJk1XTjq+MQDJYvEnkAxfPcir96kzvE4xp/Gs070PENy/Zl6fbxRO6ezE//fkt+g3+6n3+8fDzPJ7sepNb0lcgMKRbT/3y2bLtaXf4oFrf7ga/rYfB/dW5IV6NLpL9gi7bPiFiQn3s4RCgq0zbI3gSupDPqjIpG3OH8Vgo/4WULgQWwIRipvLpCImr7C3I4m8mKBedRPzFxUQYzAvSQkhDHNI+RpgsV6obpmF3BkwfUYJfYO5K17IuL3V+R4BQTt/T+vZlbzmve80upetxLznSmOggEEWBs5wUECoGKYmcDAtxyHcieoHU8cWAeCf59pWAt8ZqYlyEJ8SBK8bpwnkA8eCq+cwUmsynIR5BtoXsPgIRoMG0uDggnMtLx2UAYj8EhjaAqljkFKCJeNIjRNytNaWHsPMygNPAYXRQRt3MDyh8HIPlDswYExURwy0v4KSyl+t53EypYbj1E9/mo1NHVSS01gFiBYRqQLeZdaeQUDjP7b1soOSqKehG8K0lxFlGXmrCSH6OuAzl9SatpAhNMukDPrFGBUHskwzMhmSgKtVga50NKqEjs8GvCt8/IHbUA2HnbUScCpBp3mx4N50TtftTuRl3H7/9Y4fnmrGS0c6Vi65qCCw0Bp6Y7isOQpobgofDiC2mjMz0qduHLlMC69VoTPl7xCEswKEEXeLj0fOELa23JoAVQhJH1TUYBYjD7AaiKaSBAyrCHECBF3KMMHtCUh3r2COD0GOSmUk/lm7Bkru3H/7UbsP4Z7UQ/6rRXcNiCp9WU7YKhs6ETJdGkTOUgodvQXWbU1OdieE8oDnOZFJKmex3xphcSAhz2yBd4LpqSizxpiDEjFeVjVGZJ8Bq02sNw2y3koprQa5cqIZmv44QxVRGp+QvZskP4rWLu/Ll4f6YtSsza+iQxZg//Tsvn0/VQB1SOnrJvPahywHlSMqBd5nWJ02ztSlXZ13G9XqCaZUXKqGkSlA6VHmwx0j3YXBbPbCG7vbTnVJl8rqf8FY0g5Gm9AZ/yOg7Zfi7zvC1piFtdxm+Kpn9naTwbbt5coCzLjxZDfPhPbm9HGjSrjSb8HmZgvbTig6ELvsXj3nFD/jkI2ZdeupZN3dpQ99jlloJQLul1KJd723dS+tzw2N20srVqHKVuFVh8arucCGRy/K62qbFG7G0ah6VqTuSpftsv0AQ8q7aV5bGBpx6T7nY7olb7xxbLqbKzL1/GtjskOjgNGC1TQO5gxvdFj3Qo2gjVKfvbQX7nab0FWd5pYq98lCwXOQfvjJ4FQS140o6LAk5X5CbrwoUH/Cg5LBdhKHcqP64EQkQR6xU0/cYoAz14AHKOkWjlVSgNv02pemh2WaRh1EoWOQGjHkcinYRd1T5i4G0rRAfaynXtwhGi4jCkQSaU/eg7oxLb+rjO+seaHKFcmzpRFt5gAsiP11Wy0yQ7FrDD4iOJitI1p0/JuAtwNXV6RqHLgGgnA9sdiQkM0Cd3fO8sS8H3ucnqyvxlDPgbcjS7eWd/DMunuCNKvK57W0o2crSnnXTbGIrtwMt1yjZquCXzA70C0/tKN9hY5e21I0js6Xe1inxqbrbnsiblncJERwLkcv13VMwhrH/96q+PPq4Bd3OCKVUwXWMTjNG2VkFpx9dMve+2aN9Uqj+FNJUS3WEYRenqPkUsq16MHnP/LkGgoDwnIN3lUzEdrP3zGTT47/iwtA9F50mZqWKuHYipNazVaMBIRntEBITs78PjXGW/ZWtfvU/ -------------------------------------------------------------------------------- /archinstall/lib/disk/btrfs/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import pathlib 3 | import glob 4 | import logging 5 | from typing import Union, Dict, TYPE_CHECKING 6 | 7 | # https://stackoverflow.com/a/39757388/929999 8 | if TYPE_CHECKING: 9 | from ...installer import Installer 10 | 11 | from .btrfs_helpers import ( 12 | subvolume_info_from_path as subvolume_info_from_path, 13 | find_parent_subvolume as find_parent_subvolume, 14 | setup_subvolumes as setup_subvolumes, 15 | mount_subvolume as mount_subvolume 16 | ) 17 | from .btrfssubvolumeinfo import BtrfsSubvolumeInfo as BtrfsSubvolume 18 | from .btrfspartition import BTRFSPartition as BTRFSPartition 19 | 20 | from ...exceptions import DiskError, Deprecated 21 | from ...general import SysCommand 22 | from ...output import log 23 | 24 | 25 | def create_subvolume(installation: Installer, subvolume_location :Union[pathlib.Path, str]) -> bool: 26 | """ 27 | This function uses btrfs to create a subvolume. 28 | 29 | @installation: archinstall.Installer instance 30 | @subvolume_location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot 31 | """ 32 | 33 | installation_mountpoint = installation.target 34 | if type(installation_mountpoint) == str: 35 | installation_mountpoint = pathlib.Path(installation_mountpoint) 36 | # Set up the required physical structure 37 | if type(subvolume_location) == str: 38 | subvolume_location = pathlib.Path(subvolume_location) 39 | 40 | target = installation_mountpoint / subvolume_location.relative_to(subvolume_location.anchor) 41 | 42 | # Difference from mount_subvolume: 43 | # We only check if the parent exists, since we'll run in to "target path already exists" otherwise 44 | if not target.parent.exists(): 45 | target.parent.mkdir(parents=True) 46 | 47 | if glob.glob(str(target / '*')): 48 | raise DiskError(f"Cannot create subvolume at {target} because it contains data (non-empty folder target)") 49 | 50 | # Remove the target if it exists 51 | if target.exists(): 52 | target.rmdir() 53 | 54 | log(f"Creating a subvolume on {target}", level=logging.INFO) 55 | if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0: 56 | raise DiskError(f"Could not create a subvolume at {target}: {cmd}") 57 | -------------------------------------------------------------------------------- /archinstall/lib/hsm/fido.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import pathlib 3 | import getpass 4 | import logging 5 | from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes 6 | from ..disk.partition import Partition 7 | from ..general import log 8 | 9 | def get_fido2_devices() -> typing.Dict[str, typing.Dict[str, str]]: 10 | """ 11 | Uses systemd-cryptenroll to list the FIDO2 devices 12 | connected that supports FIDO2. 13 | Some devices might show up in udevadm as FIDO2 compliant 14 | when they are in fact not. 15 | 16 | The drawback of systemd-cryptenroll is that it uses human readable format. 17 | That means we get this weird table like structure that is of no use. 18 | 19 | So we'll look for `MANUFACTURER` and `PRODUCT`, we take their index 20 | and we split each line based on those positions. 21 | """ 22 | worker = clear_vt100_escape_codes(SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8')) 23 | 24 | MANUFACTURER_POS = 0 25 | PRODUCT_POS = 0 26 | devices = {} 27 | for line in worker.split('\r\n'): 28 | if '/dev' not in line: 29 | MANUFACTURER_POS = line.find('MANUFACTURER') 30 | PRODUCT_POS = line.find('PRODUCT') 31 | continue 32 | 33 | path = line[:MANUFACTURER_POS].rstrip() 34 | manufacturer = line[MANUFACTURER_POS:PRODUCT_POS].rstrip() 35 | product = line[PRODUCT_POS:] 36 | 37 | devices[path] = { 38 | 'manufacturer' : manufacturer, 39 | 'product' : product 40 | } 41 | 42 | return devices 43 | 44 | def fido2_enroll(hsm_device_path :pathlib.Path, partition :Partition, password :str) -> bool: 45 | worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device_path} {partition.real_device}", peak_output=True) 46 | pw_inputted = False 47 | pin_inputted = False 48 | while worker.is_alive(): 49 | if pw_inputted is False and bytes(f"please enter current passphrase for disk {partition.real_device}", 'UTF-8') in worker._trace_log.lower(): 50 | worker.write(bytes(password, 'UTF-8')) 51 | pw_inputted = True 52 | 53 | elif pin_inputted is False and bytes(f"please enter security token pin", 'UTF-8') in worker._trace_log.lower(): 54 | worker.write(bytes(getpass.getpass(" "), 'UTF-8')) 55 | pin_inputted = True 56 | 57 | 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") -------------------------------------------------------------------------------- /archinstall/lib/models/users.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Dict, List, Union, Any, TYPE_CHECKING 3 | 4 | from .password_strength import PasswordStrength 5 | 6 | if TYPE_CHECKING: 7 | _: Any 8 | 9 | 10 | @dataclass 11 | class User: 12 | username: str 13 | password: str 14 | sudo: bool 15 | 16 | @property 17 | def groups(self) -> List[str]: 18 | # this property should be transferred into a class attr instead 19 | # if it's every going to be used 20 | return [] 21 | 22 | def json(self) -> Dict[str, Any]: 23 | return { 24 | 'username': self.username, 25 | '!password': self.password, 26 | 'sudo': self.sudo 27 | } 28 | 29 | def display(self) -> str: 30 | password = '*' * (len(self.password) if self.password else 0) 31 | if password: 32 | strength = PasswordStrength.strength(self.password) 33 | password += f' ({strength.value})' 34 | return f'{_("Username")}: {self.username:16} {_("Password")}: {password:20} sudo: {str(self.sudo)}' 35 | 36 | @classmethod 37 | def _parse(cls, config_users: List[Dict[str, Any]]) -> List['User']: 38 | users = [] 39 | 40 | for entry in config_users: 41 | username = entry.get('username', None) 42 | password = entry.get('!password', '') 43 | sudo = entry.get('sudo', False) 44 | 45 | if username is None: 46 | continue 47 | 48 | user = User(username, password, sudo) 49 | users.append(user) 50 | 51 | return users 52 | 53 | @classmethod 54 | def _parse_backwards_compatible(cls, config_users: Dict, sudo: bool) -> List['User']: 55 | if len(config_users.keys()) > 0: 56 | username = list(config_users.keys())[0] 57 | password = config_users[username]['!password'] 58 | 59 | if password: 60 | return [User(username, password, sudo)] 61 | 62 | return [] 63 | 64 | @classmethod 65 | def parse_arguments( 66 | cls, 67 | config_users: Union[List[Dict[str, str]], Dict[str, str]], 68 | config_superusers: Union[List[Dict[str, str]], Dict[str, str]] 69 | ) -> List['User']: 70 | users = [] 71 | 72 | # backwards compatibility 73 | if isinstance(config_users, dict): 74 | users += cls._parse_backwards_compatible(config_users, False) 75 | else: 76 | users += cls._parse(config_users) 77 | 78 | # backwards compatibility 79 | if isinstance(config_superusers, dict): 80 | users += cls._parse_backwards_compatible(config_superusers, True) 81 | 82 | return users 83 | -------------------------------------------------------------------------------- /docs/examples/python.rst: -------------------------------------------------------------------------------- 1 | .. _examples.python: 2 | 3 | Python module 4 | ============= 5 | 6 | Archinstall supports running in `module mode `_. 7 | The way the library is invoked in module mode is limited to executing scripts under the **example** folder. 8 | 9 | It's therefore important to place any script or profile you wish to invoke in the examples folder prior to building and installing. 10 | 11 | Pre-requisites 12 | -------------- 13 | 14 | We'll assume you've followed the :ref:`installing.python.manual` method. 15 | Before actually installing the library, you will need to place your custom installer-scripts under `./archinstall/examples/` as a python file. 16 | 17 | More on how you create these in the next section. 18 | 19 | .. warning:: 20 | 21 | This is subject to change in the future as this method is currently a bit stiff. The script path will become a parameter. But for now, this is by design. 22 | 23 | Creating a script 24 | ----------------- 25 | 26 | Lets create a `test_installer` - installer as an example. This is assuming that the folder `./archinstall` is a git-clone of the main repo. 27 | We begin by creating `./archinstall/examples/test_installer.py`. The placement here is important later. 28 | 29 | This script can now already be called using `python -m archinstall test_installer` after a successful installation of the library itself. 30 | But the script won't do much. So we'll do something simple like list all the hard drives as an example. 31 | 32 | To do this, we'll begin by importing `archinstall` in our `./archinstall/examples/test_installer.py` and call some functions. 33 | 34 | .. code-block:: python 35 | 36 | import archinstall 37 | 38 | all_drives = archinstall.list_drives() 39 | print(all_drives) 40 | 41 | This should print out a list of drives and some meta-information about them. 42 | As an example, this will do just fine. 43 | 44 | Now, go ahead and install the library either as a user-module or system-wide. 45 | 46 | Calling a module 47 | ---------------- 48 | 49 | Assuming you've followed the example in `Creating a script`_, you can now safely call it with: 50 | 51 | .. code-block:: console 52 | 53 | python -m archinstall test_installer 54 | 55 | This should now print all available drives on your system. 56 | 57 | .. note:: 58 | 59 | This should work on any system, not just Arch Linux based ones. But note that other functions in the library rely heavily on Arch Linux based commands to execute the installation steps. Such as `arch-chroot`. 60 | -------------------------------------------------------------------------------- /archinstall/lib/user_interaction/save_conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from typing import Any, Dict, TYPE_CHECKING 5 | 6 | from ..configuration import ConfigurationOutput 7 | from ..menu import Menu 8 | from ..menu.menu import MenuSelectionType 9 | from ..output import log 10 | 11 | if TYPE_CHECKING: 12 | _: Any 13 | 14 | 15 | def save_config(config: Dict): 16 | 17 | def preview(selection: str): 18 | if options['user_config'] == selection: 19 | json_config = config_output.user_config_to_json() 20 | return f'{config_output.user_configuration_file}\n{json_config}' 21 | elif options['user_creds'] == selection: 22 | if json_config := config_output.user_credentials_to_json(): 23 | return f'{config_output.user_credentials_file}\n{json_config}' 24 | else: 25 | return str(_('No configuration')) 26 | elif options['disk_layout'] == selection: 27 | if json_config := config_output.disk_layout_to_json(): 28 | return f'{config_output.disk_layout_file}\n{json_config}' 29 | else: 30 | return str(_('No configuration')) 31 | elif options['all'] == selection: 32 | output = f'{config_output.user_configuration_file}\n' 33 | if json_config := config_output.user_credentials_to_json(): 34 | output += f'{config_output.user_credentials_file}\n' 35 | if json_config := config_output.disk_layout_to_json(): 36 | output += f'{config_output.disk_layout_file}\n' 37 | return output[:-1] 38 | return None 39 | 40 | config_output = ConfigurationOutput(config) 41 | 42 | options = { 43 | 'user_config': str(_('Save user configuration')), 44 | 'user_creds': str(_('Save user credentials')), 45 | 'disk_layout': str(_('Save disk layout')), 46 | 'all': str(_('Save all')) 47 | } 48 | 49 | choice = Menu( 50 | _('Choose which configuration to save'), 51 | list(options.values()), 52 | sort=False, 53 | skip=True, 54 | preview_size=0.75, 55 | preview_command=preview 56 | ).run() 57 | 58 | if choice.type_ == MenuSelectionType.Esc: 59 | return 60 | 61 | while True: 62 | path = input(_('Enter a directory for the configuration(s) to be saved: ')).strip(' ') 63 | dest_path = Path(path) 64 | if dest_path.exists() and dest_path.is_dir(): 65 | break 66 | log(_('Not a valid directory: {}').format(dest_path), fg='red') 67 | 68 | if options['user_config'] == choice.value: 69 | config_output.save_user_config(dest_path) 70 | elif options['user_creds'] == choice.value: 71 | config_output.save_user_creds(dest_path) 72 | elif options['disk_layout'] == choice.value: 73 | config_output.save_disk_layout(dest_path) 74 | elif options['all'] == choice.value: 75 | config_output.save_user_config(dest_path) 76 | config_output.save_user_creds(dest_path) 77 | config_output.save_disk_layout(dest_path) 78 | -------------------------------------------------------------------------------- /archinstall/lib/models/password_strength.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class PasswordStrength(Enum): 5 | VERY_WEAK = 'very weak' 6 | WEAK = 'weak' 7 | MODERATE = 'moderate' 8 | STRONG = 'strong' 9 | 10 | @property 11 | def value(self): 12 | match self: 13 | case PasswordStrength.VERY_WEAK: return str(_('very weak')) 14 | case PasswordStrength.WEAK: return str(_('weak')) 15 | case PasswordStrength.MODERATE: return str(_('moderate')) 16 | case PasswordStrength.STRONG: return str(_('strong')) 17 | 18 | def color(self): 19 | match self: 20 | case PasswordStrength.VERY_WEAK: return 'red' 21 | case PasswordStrength.WEAK: return 'red' 22 | case PasswordStrength.MODERATE: return 'yellow' 23 | case PasswordStrength.STRONG: return 'green' 24 | 25 | @classmethod 26 | def strength(cls, password: str) -> 'PasswordStrength': 27 | digit = any(character.isdigit() for character in password) 28 | upper = any(character.isupper() for character in password) 29 | lower = any(character.islower() for character in password) 30 | symbol = any(not character.isalnum() for character in password) 31 | return cls._check_password_strength(digit, upper, lower, symbol, len(password)) 32 | 33 | @classmethod 34 | def _check_password_strength( 35 | cls, 36 | digit: bool, 37 | upper: bool, 38 | lower: bool, 39 | symbol: bool, 40 | length: int 41 | ) -> 'PasswordStrength': 42 | # suggested evaluation 43 | # https://github.com/archlinux/archinstall/issues/1304#issuecomment-1146768163 44 | if digit and upper and lower and symbol: 45 | match length: 46 | case num if 13 <= num: 47 | return PasswordStrength.STRONG 48 | case num if 11 <= num <= 12: 49 | return PasswordStrength.MODERATE 50 | case num if 7 <= num <= 10: 51 | return PasswordStrength.WEAK 52 | case num if num <= 6: 53 | return PasswordStrength.VERY_WEAK 54 | elif digit and upper and lower: 55 | match length: 56 | case num if 14 <= num: 57 | return PasswordStrength.STRONG 58 | case num if 11 <= num <= 13: 59 | return PasswordStrength.MODERATE 60 | case num if 7 <= num <= 10: 61 | return PasswordStrength.WEAK 62 | case num if num <= 6: 63 | return PasswordStrength.VERY_WEAK 64 | elif upper and lower: 65 | match length: 66 | case num if 15 <= num: 67 | return PasswordStrength.STRONG 68 | case num if 12 <= num <= 14: 69 | return PasswordStrength.MODERATE 70 | case num if 7 <= num <= 11: 71 | return PasswordStrength.WEAK 72 | case num if num <= 6: 73 | return PasswordStrength.VERY_WEAK 74 | elif lower or upper: 75 | match length: 76 | case num if 18 <= num: 77 | return PasswordStrength.STRONG 78 | case num if 14 <= num <= 17: 79 | return PasswordStrength.MODERATE 80 | case num if 9 <= num <= 13: 81 | return PasswordStrength.WEAK 82 | case num if num <= 8: 83 | return PasswordStrength.VERY_WEAK 84 | 85 | return PasswordStrength.VERY_WEAK 86 | -------------------------------------------------------------------------------- /archinstall/locales/README.md: -------------------------------------------------------------------------------- 1 | # Nationalization 2 | 3 | Archinstall supports multiple languages, which depend on translations coming from the community :) 4 | 5 | ## Important Note 6 | Before starting a new language translation be aware that a font for that language may not be 7 | available on the ISO. We are using the pre-installed font `/usr/share/kbd/consolefonts/LatGrkCyr-8x16.psfu.gz` in archinstall 8 | which should cover a fair amount of different languages but unfortunately not all of them. 9 | 10 | We have the option to provide a custom font in case the above is not covering a specific language, which can 11 | be achieved by installing the font yourself on the ISO and saving it to `/usr/share/kbd/consolefonts/archinstall_font.psfu.gz`. 12 | If this font is present it will be automatically loaded and all languages which are not supported by the default font will 13 | be enabled (but only some might actually work). 14 | 15 | Please make sure that the provided language works with the default font on the ISO, and if not mark it in the `languages.json` 16 | that it needs an external dependency 17 | ``` 18 | {"abbr": "ur", "lang": "Urdu", "translated_lang": "اردو", "external_dep": true}, 19 | ``` 20 | 21 | ## Adding new languages 22 | 23 | New languages can be added simply by creating a new folder with the proper language abbreviation (see list `languages.json` if unsure). 24 | Run the following command to create a new template for a language 25 | ``` 26 | mkdir -p /LC_MESSAGES/ && touch /LC_MESSAGES/base.po 27 | ``` 28 | 29 | After that run the script `./locales_generator.sh` it will automatically populate the new `base.po` file with the strings that 30 | need to be translated into the new language. 31 | For example the `base.po` might contain something like the following now 32 | ``` 33 | #: lib/user_interaction.py:82 34 | msgid "Do you really want to abort?" 35 | msgstr "" 36 | ``` 37 | 38 | The `msgid` is the identifier of the string in the code as well as the default text to be displayed, meaning that if no 39 | translation is provided for a language then this is the text that is going to be shown. 40 | 41 | To perform translations for a language this file can be edited manually or the neat `poedit` can be used (https://poedit.net/). 42 | If editing the file manually, write the translation in the `msgstr` part 43 | 44 | ``` 45 | #: lib/user_interaction.py:82 46 | msgid "Do you really want to abort?" 47 | msgstr "Wollen sie wirklich abbrechen?" 48 | ``` 49 | 50 | After the translations have been written, run the script once more `./locales_generator.sh` and it will auto-generate the `base.mo` file with the included translations. 51 | After that you're all ready to go and enjoy Archinstall in the new language :) 52 | 53 | To display the language inside Archinstall in your own tongue, please edit the file `languages.json` and 54 | add a `translated_lang` entry to the respective language, e.g. 55 | 56 | ``` 57 | {"abbr": "pl", "lang": "Polish", "translated_lang": "Polski"} 58 | ``` 59 | -------------------------------------------------------------------------------- /profiles/i3.py: -------------------------------------------------------------------------------- 1 | # Common package for i3, lets user select which i3 configuration they want. 2 | 3 | import archinstall 4 | from archinstall import Menu 5 | from archinstall.lib.menu.menu import MenuSelectionType 6 | 7 | is_top_level_profile = False 8 | 9 | # New way of defining packages for a profile, which is iterable and can be used out side 10 | # of the profile to get a list of "what packages will be installed". 11 | __packages__ = [ 12 | 'i3lock', 13 | 'i3status', 14 | 'i3blocks', 15 | 'xterm', 16 | 'lightdm-gtk-greeter', 17 | 'lightdm', 18 | 'dmenu', 19 | ] 20 | 21 | 22 | def _prep_function(*args, **kwargs): 23 | """ 24 | Magic function called by the importing installer 25 | before continuing any further. It also avoids executing any 26 | other code in this stage. So it's a safe way to ask the user 27 | for more input before any other installer steps start. 28 | """ 29 | 30 | supported_configurations = ['i3-wm', 'i3-gaps'] 31 | 32 | choice = Menu('Select your desired configuration', supported_configurations).run() 33 | 34 | if choice.type_ != MenuSelectionType.Selection: 35 | return False 36 | 37 | if choice.value: 38 | # Temporarily store the selected desktop profile 39 | # in a session-safe location, since this module will get reloaded 40 | # the next time it gets executed. 41 | archinstall.storage['_i3_configuration'] = choice.value 42 | 43 | # i3 requires a functioning Xorg installation. 44 | profile = archinstall.Profile(None, 'xorg') 45 | with profile.load_instructions(namespace='xorg.py') as imported: 46 | if hasattr(imported, '_prep_function'): 47 | return imported._prep_function() 48 | else: 49 | print('Deprecated (??): xorg profile has no _prep_function() anymore') 50 | 51 | return False 52 | 53 | 54 | if __name__ == 'i3': 55 | """ 56 | This "profile" is a meta-profile. 57 | There are no desktop-specific steps, it simply routes 58 | the installer to whichever desktop environment/window manager was chosen. 59 | 60 | Maybe in the future, a network manager or similar things *could* be added here. 61 | We should honor that Arch Linux does not officially endorse a desktop-setup, nor is 62 | it trying to be a turn-key desktop distribution. 63 | 64 | There are plenty of desktop-turn-key-solutions based on Arch Linux, 65 | this is therefore just a helper to get started 66 | """ 67 | 68 | # Install common packages for all i3 configurations 69 | archinstall.storage['installation_session'].add_additional_packages(__packages__[:4]) 70 | 71 | # Install dependency profiles 72 | archinstall.storage['installation_session'].install_profile('xorg') 73 | 74 | # gaps is installed by default so we are overriding it here with lightdm 75 | archinstall.storage['installation_session'].add_additional_packages(__packages__[4:]) 76 | 77 | # Auto start lightdm for all users 78 | archinstall.storage['installation_session'].enable_service('lightdm') 79 | 80 | # install the i3 group now 81 | archinstall.storage['installation_session'].add_additional_packages(archinstall.storage['_i3_configuration']) 82 | -------------------------------------------------------------------------------- /docs/archinstall/general.rst: -------------------------------------------------------------------------------- 1 | .. _archinstall.helpers: 2 | 3 | .. warning:: 4 | All these helper functions are mostly, if not all, related to outside-installation-instructions. Meaning the calls will affect your current running system - and not touch your installed system. 5 | 6 | Profile related helpers 7 | ======================= 8 | 9 | .. autofunction:: archinstall.list_profiles 10 | 11 | Packages 12 | ======== 13 | 14 | .. autofunction:: archinstall.find_package 15 | 16 | .. autofunction:: archinstall.find_packages 17 | 18 | Locale related 19 | ============== 20 | 21 | .. autofunction:: archinstall.list_keyboard_languages 22 | 23 | .. autofunction:: archinstall.search_keyboard_layout 24 | 25 | .. autofunction:: archinstall.set_keyboard_language 26 | 27 | .. 28 | autofunction:: archinstall.Installer.set_keyboard_layout 29 | 30 | Services 31 | ======== 32 | 33 | .. autofunction:: archinstall.service_state 34 | 35 | Mirrors 36 | ======= 37 | 38 | .. autofunction:: archinstall.filter_mirrors_by_region 39 | 40 | .. autofunction:: archinstall.add_custom_mirrors 41 | 42 | .. autofunction:: archinstall.insert_mirrors 43 | 44 | .. autofunction:: archinstall.use_mirrors 45 | 46 | .. autofunction:: archinstall.re_rank_mirrors 47 | 48 | .. autofunction:: archinstall.list_mirrors 49 | 50 | Disk related 51 | ============ 52 | 53 | .. autofunction:: archinstall.BlockDevice 54 | 55 | .. autofunction:: archinstall.Partition 56 | 57 | .. autofunction:: archinstall.Filesystem 58 | 59 | .. autofunction:: archinstall.device_state 60 | 61 | .. autofunction:: archinstall.all_blockdevices 62 | 63 | Luks (Disk encryption) 64 | ====================== 65 | 66 | .. autofunction:: archinstall.luks2 67 | 68 | Networking 69 | ========== 70 | 71 | .. autofunction:: archinstall.get_hw_addr 72 | 73 | .. autofunction:: archinstall.list_interfaces 74 | 75 | .. autofunction:: archinstall.check_mirror_reachable 76 | 77 | .. autofunction:: archinstall.update_keyring 78 | 79 | .. autofunction:: archinstall.enrich_iface_types 80 | 81 | .. autofunction:: archinstall.get_interface_from_mac 82 | 83 | .. autofunction:: archinstall.wireless_scan 84 | 85 | .. autofunction:: archinstall.get_wireless_networks 86 | 87 | General 88 | ======= 89 | 90 | .. autofunction:: archinstall.log 91 | 92 | .. autofunction:: archinstall.locate_binary 93 | 94 | .. autofunction:: archinstall.SysCommand 95 | 96 | .. autofunction:: archinstall.SysCommandWorker 97 | 98 | Exceptions 99 | ========== 100 | 101 | .. autofunction:: archinstall.RequirementError 102 | 103 | .. autofunction:: archinstall.DiskError 104 | 105 | .. autofunction:: archinstall.ProfileError 106 | 107 | .. autofunction:: archinstall.SysCallError 108 | 109 | .. autofunction:: archinstall.ProfileNotFound 110 | 111 | .. autofunction:: archinstall.HardwareIncompatibilityError 112 | 113 | .. autofunction:: archinstall.PermissionError 114 | 115 | .. autofunction:: archinstall.UserError 116 | 117 | .. autofunction:: archinstall.ServiceException 118 | -------------------------------------------------------------------------------- /examples/minimal.py: -------------------------------------------------------------------------------- 1 | import archinstall 2 | 3 | # Select a harddrive and a disk password 4 | from archinstall import User 5 | 6 | archinstall.log("Minimal only supports:") 7 | archinstall.log(" * Being installed to a single disk") 8 | 9 | if archinstall.arguments.get('help', None): 10 | archinstall.log(" - Optional disk encryption via --!encryption-password=") 11 | archinstall.log(" - Optional filesystem type via --filesystem=") 12 | archinstall.log(" - Optional systemd network via --network") 13 | 14 | archinstall.arguments['harddrive'] = archinstall.select_disk(archinstall.all_blockdevices()) 15 | 16 | 17 | def install_on(mountpoint): 18 | # We kick off the installer by telling it where the 19 | with archinstall.Installer(mountpoint) as installation: 20 | # Strap in the base system, add a boot loader and configure 21 | # some other minor details as specified by this profile and user. 22 | if installation.minimal_installation(): 23 | installation.set_hostname('minimal-arch') 24 | installation.add_bootloader() 25 | 26 | # Optionally enable networking: 27 | if archinstall.arguments.get('network', None): 28 | installation.copy_iso_network_config(enable_services=True) 29 | 30 | installation.add_additional_packages(['nano', 'wget', 'git']) 31 | installation.install_profile('minimal') 32 | 33 | user = User('devel', 'devel', False) 34 | installation.create_users(user) 35 | 36 | # Once this is done, we output some useful information to the user 37 | # And the installation is complete. 38 | archinstall.log("There are two new accounts in your installation after reboot:") 39 | archinstall.log(" * root (password: airoot)") 40 | archinstall.log(" * devel (password: devel)") 41 | 42 | 43 | if archinstall.arguments['harddrive']: 44 | archinstall.arguments['harddrive'].keep_partitions = False 45 | 46 | print(f" ! Formatting {archinstall.arguments['harddrive']} in ", end='') 47 | archinstall.do_countdown() 48 | 49 | # First, we configure the basic filesystem layout 50 | with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs: 51 | # We use the entire disk instead of setting up partitions on your own 52 | if archinstall.arguments['harddrive'].keep_partitions is False: 53 | fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs')) 54 | 55 | boot = fs.find_partition('/boot') 56 | root = fs.find_partition('/') 57 | 58 | boot.format('fat32') 59 | 60 | # We encrypt the root partition if we got a password to do so with, 61 | # Otherwise we just skip straight to formatting and installation 62 | if archinstall.arguments.get('!encryption-password', None): 63 | root.encrypted = True 64 | root.encrypt(password=archinstall.arguments.get('!encryption-password', None)) 65 | 66 | with archinstall.luks2(root, 'luksloop', archinstall.arguments.get('!encryption-password', None)) as unlocked_root: 67 | unlocked_root.format(root.filesystem) 68 | unlocked_root.mount('/mnt') 69 | else: 70 | root.format(root.filesystem) 71 | root.mount('/mnt') 72 | 73 | boot.mount('/mnt/boot') 74 | 75 | install_on('/mnt') 76 | -------------------------------------------------------------------------------- /archinstall/lib/disk/mapperdev.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import pathlib 3 | import logging 4 | import json 5 | from dataclasses import dataclass 6 | from typing import Optional, List, Dict, Any, Iterator, TYPE_CHECKING 7 | 8 | from ..exceptions import SysCallError 9 | from ..general import SysCommand 10 | from ..output import log 11 | 12 | if TYPE_CHECKING: 13 | from .btrfs import BtrfsSubvolumeInfo 14 | 15 | @dataclass 16 | class MapperDev: 17 | mappername :str 18 | 19 | @property 20 | def name(self): 21 | return self.mappername 22 | 23 | @property 24 | def path(self): 25 | return f"/dev/mapper/{self.mappername}" 26 | 27 | @property 28 | def partition(self): 29 | from .helpers import uevent, get_parent_of_partition 30 | from .partition import Partition 31 | from .blockdevice import BlockDevice 32 | 33 | for mapper in glob.glob('/dev/mapper/*'): 34 | path_obj = pathlib.Path(mapper) 35 | if path_obj.name == self.mappername and pathlib.Path(mapper).is_symlink(): 36 | dm_device = (pathlib.Path("/dev/mapper/") / path_obj.readlink()).resolve() 37 | 38 | for slave in glob.glob(f"/sys/class/block/{dm_device.name}/slaves/*"): 39 | partition_belonging_to_dmcrypt_device = pathlib.Path(slave).name 40 | 41 | try: 42 | uevent_data = SysCommand(f"blkid -o export /dev/{partition_belonging_to_dmcrypt_device}").decode() 43 | except SysCallError as error: 44 | log(f"Could not get information on device /dev/{partition_belonging_to_dmcrypt_device}: {error}", level=logging.ERROR, fg="red") 45 | 46 | information = uevent(uevent_data) 47 | block_device = BlockDevice(get_parent_of_partition('/dev/' / pathlib.Path(information['DEVNAME']))) 48 | 49 | return Partition(information['DEVNAME'], block_device=block_device) 50 | 51 | raise ValueError(f"Could not convert {self.mappername} to a real dm-crypt device") 52 | 53 | @property 54 | def mountpoint(self) -> Optional[pathlib.Path]: 55 | try: 56 | data = json.loads(SysCommand(f"findmnt --json -R {self.path}").decode()) 57 | for filesystem in data['filesystems']: 58 | return pathlib.Path(filesystem.get('target')) 59 | 60 | except SysCallError as error: 61 | # Not mounted anywhere most likely 62 | log(f"Could not locate mount information for {self.path}: {error}", level=logging.WARNING, fg="yellow") 63 | pass 64 | 65 | return None 66 | 67 | @property 68 | def mountpoints(self) -> List[Dict[str, Any]]: 69 | return [obj['target'] for obj in self.mount_information] 70 | 71 | @property 72 | def mount_information(self) -> List[Dict[str, Any]]: 73 | from .helpers import find_mountpoint 74 | return [{**obj, 'target' : pathlib.Path(obj.get('target', '/dev/null'))} for obj in find_mountpoint(self.path)] 75 | 76 | @property 77 | def filesystem(self) -> Optional[str]: 78 | from .helpers import get_filesystem_type 79 | return get_filesystem_type(self.path) 80 | 81 | @property 82 | def subvolumes(self) -> Iterator['BtrfsSubvolumeInfo']: 83 | from .btrfs import subvolume_info_from_path 84 | 85 | for mountpoint in self.mount_information: 86 | if target := mountpoint.get('target'): 87 | if subvolume := subvolume_info_from_path(pathlib.Path(target)): 88 | yield subvolume 89 | -------------------------------------------------------------------------------- /archinstall/lib/user_interaction/disk_conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Dict, TYPE_CHECKING, Optional 4 | 5 | from .partitioning_conf import manage_new_and_existing_partitions, get_default_partition_layout 6 | from ..disk import BlockDevice 7 | from ..exceptions import DiskError 8 | from ..menu import Menu 9 | from ..menu.menu import MenuSelectionType 10 | 11 | if TYPE_CHECKING: 12 | _: Any 13 | 14 | 15 | def ask_for_main_filesystem_format(advanced_options=False) -> str: 16 | options = {'btrfs': 'btrfs', 'ext4': 'ext4', 'xfs': 'xfs', 'f2fs': 'f2fs'} 17 | 18 | advanced = {'ntfs': 'ntfs'} 19 | 20 | if advanced_options: 21 | options.update(advanced) 22 | 23 | prompt = _('Select which filesystem your main partition should use') 24 | choice = Menu(prompt, options, skip=False).run() 25 | return choice.value 26 | 27 | 28 | def select_individual_blockdevice_usage(block_devices: list) -> Dict[str, Any]: 29 | result = {} 30 | 31 | for device in block_devices: 32 | layout = manage_new_and_existing_partitions(device) 33 | result[device.path] = layout 34 | 35 | return result 36 | 37 | 38 | def select_disk_layout(preset: Optional[Dict[str, Any]], block_devices: list, advanced_options=False) -> Optional[Dict[str, Any]]: 39 | wipe_mode = str(_('Wipe all selected drives and use a best-effort default partition layout')) 40 | custome_mode = str(_('Select what to do with each individual drive (followed by partition usage)')) 41 | modes = [wipe_mode, custome_mode] 42 | 43 | warning = str(_('Are you sure you want to reset this setting?')) 44 | 45 | choice = Menu( 46 | _('Select what you wish to do with the selected block devices'), 47 | modes, 48 | raise_error_on_interrupt=True, 49 | raise_error_warning_msg=warning 50 | ).run() 51 | 52 | match choice.type_: 53 | case MenuSelectionType.Esc: return preset 54 | case MenuSelectionType.Ctrl_c: return None 55 | case MenuSelectionType.Selection: 56 | if choice.value == wipe_mode: 57 | return get_default_partition_layout(block_devices, advanced_options) 58 | else: 59 | return select_individual_blockdevice_usage(block_devices) 60 | 61 | 62 | def select_disk(dict_o_disks: Dict[str, BlockDevice]) -> Optional[BlockDevice]: 63 | """ 64 | Asks the user to select a harddrive from the `dict_o_disks` selection. 65 | Usually this is combined with :ref:`archinstall.list_drives`. 66 | 67 | :param dict_o_disks: A `dict` where keys are the drive-name, value should be a dict containing drive information. 68 | :type dict_o_disks: dict 69 | 70 | :return: The name/path (the dictionary key) of the selected drive 71 | :rtype: str 72 | """ 73 | drives = sorted(list(dict_o_disks.keys())) 74 | if len(drives) >= 1: 75 | title = str(_('You can skip selecting a drive and partitioning and use whatever drive-setup is mounted at /mnt (experimental)')) + '\n' 76 | title += str(_('Select one of the disks or skip and use /mnt as default')) 77 | 78 | choice = Menu(title, drives).run() 79 | 80 | if choice.type_ == MenuSelectionType.Esc: 81 | return None 82 | 83 | drive = dict_o_disks[choice.value] 84 | return drive 85 | 86 | raise DiskError('select_disk() requires a non-empty dictionary of disks to select from.') 87 | -------------------------------------------------------------------------------- /profiles/desktop.py: -------------------------------------------------------------------------------- 1 | # A desktop environment selector. 2 | from typing import Any, TYPE_CHECKING 3 | 4 | import archinstall 5 | from archinstall import log, Menu 6 | from archinstall.lib.menu.menu import MenuSelectionType 7 | 8 | if TYPE_CHECKING: 9 | _: Any 10 | 11 | is_top_level_profile = True 12 | 13 | __description__ = str(_('Provides a selection of desktop environments and tiling window managers, e.g. gnome, kde, sway')) 14 | 15 | # New way of defining packages for a profile, which is iterable and can be used out side 16 | # of the profile to get a list of "what packages will be installed". 17 | __packages__ = [ 18 | 'nano', 19 | 'vim', 20 | 'openssh', 21 | 'htop', 22 | 'wget', 23 | 'iwd', 24 | 'wireless_tools', 25 | 'wpa_supplicant', 26 | 'smartmontools', 27 | 'xdg-utils', 28 | ] 29 | 30 | __supported__ = [ 31 | 'gnome', 32 | 'kde', 33 | 'awesome', 34 | 'sway', 35 | 'cinnamon', 36 | 'xfce4', 37 | 'lxqt', 38 | 'i3', 39 | 'bspwm', 40 | 'budgie', 41 | 'mate', 42 | 'deepin', 43 | 'enlightenment', 44 | 'qtile' 45 | ] 46 | 47 | 48 | def _prep_function(*args, **kwargs) -> bool: 49 | """ 50 | Magic function called by the importing installer 51 | before continuing any further. It also avoids executing any 52 | other code in this stage. So it's a safe way to ask the user 53 | for more input before any other installer steps start. 54 | """ 55 | choice = Menu(str(_('Select your desired desktop environment')), __supported__).run() 56 | 57 | if choice.type_ != MenuSelectionType.Selection: 58 | return False 59 | 60 | if choice.value: 61 | # Temporarily store the selected desktop profile 62 | # in a session-safe location, since this module will get reloaded 63 | # the next time it gets executed. 64 | if not archinstall.storage.get('_desktop_profile', None): 65 | archinstall.storage['_desktop_profile'] = choice.value 66 | if not archinstall.arguments.get('desktop-environment', None): 67 | archinstall.arguments['desktop-environment'] = choice.value 68 | profile = archinstall.Profile(None, choice.value) 69 | # Loading the instructions with a custom namespace, ensures that a __name__ comparison is never triggered. 70 | with profile.load_instructions(namespace=f"{choice.value}.py") as imported: 71 | if hasattr(imported, '_prep_function'): 72 | return imported._prep_function() 73 | else: 74 | log(f"Deprecated (??): {choice.value} profile has no _prep_function() anymore") 75 | exit(1) 76 | 77 | return False 78 | 79 | 80 | if __name__ == 'desktop': 81 | """ 82 | This "profile" is a meta-profile. 83 | There are no desktop-specific steps, it simply routes 84 | the installer to whichever desktop environment/window manager was chosen. 85 | 86 | Maybe in the future, a network manager or similar things *could* be added here. 87 | We should honor that Arch Linux does not officially endorse a desktop-setup, nor is 88 | it trying to be a turn-key desktop distribution. 89 | 90 | There are plenty of desktop-turn-key-solutions based on Arch Linux, 91 | this is therefore just a helper to get started 92 | """ 93 | 94 | # Install common packages for all desktop environments 95 | archinstall.storage['installation_session'].add_additional_packages(__packages__) 96 | 97 | archinstall.storage['installation_session'].install_profile(archinstall.storage['_desktop_profile']) 98 | -------------------------------------------------------------------------------- /profiles/xorg.py: -------------------------------------------------------------------------------- 1 | # A system with "xorg" installed 2 | 3 | import archinstall 4 | import logging 5 | from archinstall.lib.hardware import __packages__ as __hwd__packages__ 6 | 7 | is_top_level_profile = True 8 | 9 | __description__ = str(_('Installs a minimal system as well as xorg and graphics drivers.')) 10 | 11 | __packages__ = [ 12 | 'dkms', 13 | 'xorg-server', 14 | 'xorg-xinit', 15 | 'nvidia-dkms', 16 | *__hwd__packages__, 17 | ] 18 | 19 | 20 | def _prep_function(*args, **kwargs): 21 | """ 22 | Magic function called by the importing installer 23 | before continuing any further. It also avoids executing any 24 | other code in this stage. So it's a safe way to ask the user 25 | for more input before any other installer steps start. 26 | """ 27 | 28 | driver = archinstall.select_driver() 29 | 30 | if driver: 31 | archinstall.storage["gfx_driver_packages"] = driver 32 | return True 33 | 34 | # TODO: Add language section and/or merge it with the locale selected 35 | # earlier in for instance guided.py installer. 36 | 37 | return False 38 | 39 | 40 | # Ensures that this code only gets executed if executed 41 | # through importlib.util.spec_from_file_location("xorg", "/somewhere/xorg.py") 42 | # or through conventional import xorg 43 | if __name__ == 'xorg': 44 | try: 45 | if "nvidia" in archinstall.storage.get("gfx_driver_packages", []): 46 | if "linux-zen" in archinstall.storage['installation_session'].base_packages or "linux-lts" in archinstall.storage['installation_session'].base_packages: 47 | for kernel in archinstall.storage['installation_session'].kernels: 48 | archinstall.storage['installation_session'].add_additional_packages(f"{kernel}-headers") # Fixes https://github.com/archlinux/archinstall/issues/585 49 | archinstall.storage['installation_session'].add_additional_packages("dkms") # I've had kernel regen fail if it wasn't installed before nvidia-dkms 50 | archinstall.storage['installation_session'].add_additional_packages("xorg-server", "xorg-xinit", "nvidia-dkms") 51 | else: 52 | archinstall.storage['installation_session'].add_additional_packages(f"xorg-server", "xorg-xinit", *archinstall.storage.get('gfx_driver_packages', [])) 53 | elif 'amdgpu' in archinstall.storage.get("gfx_driver_packages", []): 54 | # The order of these two are important if amdgpu is installed #808 55 | if 'amdgpu' in archinstall.storage['installation_session'].MODULES: 56 | archinstall.storage['installation_session'].MODULES.remove('amdgpu') 57 | archinstall.storage['installation_session'].MODULES.append('amdgpu') 58 | 59 | if 'radeon' in archinstall.storage['installation_session'].MODULES: 60 | archinstall.storage['installation_session'].MODULES.remove('radeon') 61 | archinstall.storage['installation_session'].MODULES.append('radeon') 62 | 63 | archinstall.storage['installation_session'].add_additional_packages(f"xorg-server", "xorg-xinit", *archinstall.storage.get('gfx_driver_packages', [])) 64 | else: 65 | archinstall.storage['installation_session'].add_additional_packages(f"xorg-server", "xorg-xinit", *archinstall.storage.get('gfx_driver_packages', [])) 66 | except Exception as err: 67 | archinstall.log(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}", level=logging.WARNING, fg="yellow") 68 | archinstall.storage['installation_session'].add_additional_packages("xorg-server", "xorg-xinit") # Prep didn't run, so there's no driver to install 69 | -------------------------------------------------------------------------------- /archinstall/lib/models/pydantic.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from pydantic import BaseModel 3 | 4 | """ 5 | This python file is not in use. 6 | Pydantic is not a builtin, and we use the dataclasses.py instead! 7 | """ 8 | 9 | class VersionDef(BaseModel): 10 | version_string: str 11 | 12 | @classmethod 13 | def parse_version(self) -> List[str]: 14 | if '.' in self.version_string: 15 | versions = self.version_string.split('.') 16 | else: 17 | versions = [self.version_string] 18 | 19 | return versions 20 | 21 | @classmethod 22 | def major(self) -> str: 23 | return self.parse_version()[0] 24 | 25 | @classmethod 26 | def minor(self) -> str: 27 | versions = self.parse_version() 28 | if len(versions) >= 2: 29 | return versions[1] 30 | 31 | @classmethod 32 | def patch(self) -> str: 33 | versions = self.parse_version() 34 | if '-' in versions[-1]: 35 | _, patch_version = versions[-1].split('-', 1) 36 | return patch_version 37 | 38 | def __eq__(self, other :'VersionDef') -> bool: 39 | if other.major == self.major and \ 40 | other.minor == self.minor and \ 41 | other.patch == self.patch: 42 | 43 | return True 44 | return False 45 | 46 | def __lt__(self, other :'VersionDef') -> bool: 47 | if self.major > other.major: 48 | return False 49 | elif self.minor and other.minor and self.minor > other.minor: 50 | return False 51 | elif self.patch and other.patch and self.patch > other.patch: 52 | return False 53 | 54 | def __str__(self) -> str: 55 | return self.version_string 56 | 57 | 58 | class PackageSearchResult(BaseModel): 59 | pkgname: str 60 | pkgbase: str 61 | repo: str 62 | arch: str 63 | pkgver: str 64 | pkgrel: str 65 | epoch: int 66 | pkgdesc: str 67 | url: str 68 | filename: str 69 | compressed_size: int 70 | installed_size: int 71 | build_date: str 72 | last_update: str 73 | flag_date: Optional[str] 74 | maintainers: List[str] 75 | packager: str 76 | groups: List[str] 77 | licenses: List[str] 78 | conflicts: List[str] 79 | provides: List[str] 80 | replaces: List[str] 81 | depends: List[str] 82 | optdepends: List[str] 83 | makedepends: List[str] 84 | checkdepends: List[str] 85 | 86 | @property 87 | def pkg_version(self) -> str: 88 | return self.pkgver 89 | 90 | def __eq__(self, other :'VersionDef') -> bool: 91 | return self.pkg_version == other.pkg_version 92 | 93 | def __lt__(self, other :'VersionDef') -> bool: 94 | return self.pkg_version < other.pkg_version 95 | 96 | 97 | class PackageSearch(BaseModel): 98 | version: int 99 | limit: int 100 | valid: bool 101 | results: List[PackageSearchResult] 102 | 103 | 104 | class LocalPackage(BaseModel): 105 | name: str 106 | version: str 107 | description:str 108 | architecture: str 109 | url: str 110 | licenses: str 111 | groups: str 112 | depends_on: str 113 | optional_deps: str 114 | required_by: str 115 | optional_for: str 116 | conflicts_with: str 117 | replaces: str 118 | installed_size: str 119 | packager: str 120 | build_date: str 121 | install_date: str 122 | install_reason: str 123 | install_script: str 124 | validated_by: str 125 | 126 | @property 127 | def pkg_version(self) -> str: 128 | return self.version 129 | 130 | def __eq__(self, other :'VersionDef') -> bool: 131 | return self.pkg_version == other.pkg_version 132 | 133 | def __lt__(self, other :'VersionDef') -> bool: 134 | return self.pkg_version < other.pkg_version -------------------------------------------------------------------------------- /archinstall/lib/models/dataclasses.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, List 3 | 4 | @dataclass 5 | class VersionDef: 6 | version_string: str 7 | 8 | @classmethod 9 | def parse_version(self) -> List[str]: 10 | if '.' in self.version_string: 11 | versions = self.version_string.split('.') 12 | else: 13 | versions = [self.version_string] 14 | 15 | return versions 16 | 17 | @classmethod 18 | def major(self) -> str: 19 | return self.parse_version()[0] 20 | 21 | @classmethod 22 | def minor(self) -> str: 23 | versions = self.parse_version() 24 | if len(versions) >= 2: 25 | return versions[1] 26 | 27 | @classmethod 28 | def patch(self) -> str: 29 | versions = self.parse_version() 30 | if '-' in versions[-1]: 31 | _, patch_version = versions[-1].split('-', 1) 32 | return patch_version 33 | 34 | def __eq__(self, other :'VersionDef') -> bool: 35 | if other.major == self.major and \ 36 | other.minor == self.minor and \ 37 | other.patch == self.patch: 38 | 39 | return True 40 | return False 41 | 42 | def __lt__(self, other :'VersionDef') -> bool: 43 | if self.major > other.major: 44 | return False 45 | elif self.minor and other.minor and self.minor > other.minor: 46 | return False 47 | elif self.patch and other.patch and self.patch > other.patch: 48 | return False 49 | 50 | def __str__(self) -> str: 51 | return self.version_string 52 | 53 | @dataclass 54 | class PackageSearchResult: 55 | pkgname: str 56 | pkgbase: str 57 | repo: str 58 | arch: str 59 | pkgver: str 60 | pkgrel: str 61 | epoch: int 62 | pkgdesc: str 63 | url: str 64 | filename: str 65 | compressed_size: int 66 | installed_size: int 67 | build_date: str 68 | last_update: str 69 | flag_date: Optional[str] 70 | maintainers: List[str] 71 | packager: str 72 | groups: List[str] 73 | licenses: List[str] 74 | conflicts: List[str] 75 | provides: List[str] 76 | replaces: List[str] 77 | depends: List[str] 78 | optdepends: List[str] 79 | makedepends: List[str] 80 | checkdepends: List[str] 81 | 82 | @property 83 | def pkg_version(self) -> str: 84 | return self.pkgver 85 | 86 | def __eq__(self, other :'VersionDef') -> bool: 87 | return self.pkg_version == other.pkg_version 88 | 89 | def __lt__(self, other :'VersionDef') -> bool: 90 | return self.pkg_version < other.pkg_version 91 | 92 | @dataclass 93 | class PackageSearch: 94 | version: int 95 | limit: int 96 | valid: bool 97 | num_pages: int 98 | page: int 99 | results: List[PackageSearchResult] 100 | 101 | def __post_init__(self): 102 | self.results = [PackageSearchResult(**x) for x in self.results] 103 | 104 | @dataclass 105 | class LocalPackage: 106 | name: str 107 | version: str 108 | description:str 109 | architecture: str 110 | url: str 111 | licenses: str 112 | groups: str 113 | depends_on: str 114 | optional_deps: str 115 | required_by: str 116 | optional_for: str 117 | conflicts_with: str 118 | replaces: str 119 | installed_size: str 120 | packager: str 121 | build_date: str 122 | install_date: str 123 | install_reason: str 124 | install_script: str 125 | validated_by: str 126 | provides: str 127 | 128 | @property 129 | def pkg_version(self) -> str: 130 | return self.version 131 | 132 | def __eq__(self, other :'VersionDef') -> bool: 133 | return self.pkg_version == other.pkg_version 134 | 135 | def __lt__(self, other :'VersionDef') -> bool: 136 | return self.pkg_version < other.pkg_version -------------------------------------------------------------------------------- /archinstall/lib/user_interaction/subvolume_config.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional, Any, TYPE_CHECKING 2 | 3 | from ..menu.list_manager import ListManager 4 | from ..menu.menu import MenuSelectionType 5 | from ..menu.text_input import TextInput 6 | from ..menu import Menu 7 | from ..models.subvolume import Subvolume 8 | from ... import FormattedOutput 9 | 10 | if TYPE_CHECKING: 11 | _: Any 12 | 13 | 14 | class SubvolumeList(ListManager): 15 | def __init__(self, prompt: str, subvolumes: List[Subvolume]): 16 | self._actions = [ 17 | str(_('Add subvolume')), 18 | str(_('Edit subvolume')), 19 | str(_('Delete subvolume')) 20 | ] 21 | super().__init__(prompt, subvolumes, [self._actions[0]], self._actions[1:]) 22 | 23 | def reformat(self, data: List[Subvolume]) -> Dict[str, Optional[Subvolume]]: 24 | table = FormattedOutput.as_table(data) 25 | rows = table.split('\n') 26 | 27 | # these are the header rows of the table and do not map to any User obviously 28 | # we're adding 2 spaces as prefix because the menu selector '> ' will be put before 29 | # the selectable rows so the header has to be aligned 30 | display_data: Dict[str, Optional[Subvolume]] = {f' {rows[0]}': None, f' {rows[1]}': None} 31 | 32 | for row, subvol in zip(rows[2:], data): 33 | row = row.replace('|', '\\|') 34 | display_data[row] = subvol 35 | 36 | return display_data 37 | 38 | def selected_action_display(self, subvolume: Subvolume) -> str: 39 | return subvolume.name 40 | 41 | def _prompt_options(self, editing: Optional[Subvolume] = None) -> List[str]: 42 | preset_options = [] 43 | if editing: 44 | preset_options = editing.options 45 | 46 | choice = Menu( 47 | str(_("Select the desired subvolume options ")), 48 | ['nodatacow','compress'], 49 | skip=True, 50 | preset_values=preset_options, 51 | multi=True 52 | ).run() 53 | 54 | if choice.type_ == MenuSelectionType.Selection: 55 | return choice.value # type: ignore 56 | 57 | return [] 58 | 59 | def _add_subvolume(self, editing: Optional[Subvolume] = None) -> Optional[Subvolume]: 60 | name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run() 61 | 62 | if not name: 63 | return None 64 | 65 | mountpoint = TextInput(f'\n{_("Subvolume mountpoint")}: ', editing.mountpoint if editing else '').run() 66 | 67 | if not mountpoint: 68 | return None 69 | 70 | options = self._prompt_options(editing) 71 | 72 | subvolume = Subvolume(name, mountpoint) 73 | subvolume.compress = 'compress' in options 74 | subvolume.nodatacow = 'nodatacow' in options 75 | 76 | return subvolume 77 | 78 | def handle_action(self, action: str, entry: Optional[Subvolume], data: List[Subvolume]) -> List[Subvolume]: 79 | if action == self._actions[0]: # add 80 | new_subvolume = self._add_subvolume() 81 | 82 | if new_subvolume is not None: 83 | # in case a user with the same username as an existing user 84 | # was created we'll replace the existing one 85 | data = [d for d in data if d.name != new_subvolume.name] 86 | data += [new_subvolume] 87 | elif entry is not None: 88 | if action == self._actions[1]: # edit subvolume 89 | new_subvolume = self._add_subvolume(entry) 90 | 91 | if new_subvolume is not None: 92 | # we'll remove the original subvolume and add the modified version 93 | data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name] 94 | data += [new_subvolume] 95 | elif action == self._actions[2]: # delete 96 | data = [d for d in data if d != entry] 97 | 98 | return data 99 | -------------------------------------------------------------------------------- /archinstall/lib/user_interaction/manage_users_conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from typing import Any, Dict, TYPE_CHECKING, List, Optional 5 | 6 | from .utils import get_password 7 | from ..menu import Menu 8 | from ..menu.list_manager import ListManager 9 | from ..models.users import User 10 | from ..output import FormattedOutput 11 | 12 | if TYPE_CHECKING: 13 | _: Any 14 | 15 | 16 | class UserList(ListManager): 17 | """ 18 | subclass of ListManager for the managing of user accounts 19 | """ 20 | 21 | def __init__(self, prompt: str, lusers: List[User]): 22 | self._actions = [ 23 | str(_('Add a user')), 24 | str(_('Change password')), 25 | str(_('Promote/Demote user')), 26 | str(_('Delete User')) 27 | ] 28 | super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) 29 | 30 | def reformat(self, data: List[User]) -> Dict[str, User]: 31 | table = FormattedOutput.as_table(data) 32 | rows = table.split('\n') 33 | 34 | # these are the header rows of the table and do not map to any User obviously 35 | # we're adding 2 spaces as prefix because the menu selector '> ' will be put before 36 | # the selectable rows so the header has to be aligned 37 | display_data = {f' {rows[0]}': None, f' {rows[1]}': None} 38 | 39 | for row, user in zip(rows[2:], data): 40 | row = row.replace('|', '\\|') 41 | display_data[row] = user 42 | 43 | return display_data 44 | 45 | def selected_action_display(self, user: User) -> str: 46 | return user.username 47 | 48 | def handle_action(self, action: str, entry: Optional[User], data: List[User]) -> List[User]: 49 | if action == self._actions[0]: # add 50 | new_user = self._add_user() 51 | if new_user is not None: 52 | # in case a user with the same username as an existing user 53 | # was created we'll replace the existing one 54 | data = [d for d in data if d.username != new_user.username] 55 | data += [new_user] 56 | elif action == self._actions[1]: # change password 57 | prompt = str(_('Password for user "{}": ').format(entry.username)) 58 | new_password = get_password(prompt=prompt) 59 | if new_password: 60 | user = next(filter(lambda x: x == entry, data)) 61 | user.password = new_password 62 | elif action == self._actions[2]: # promote/demote 63 | user = next(filter(lambda x: x == entry, data)) 64 | user.sudo = False if user.sudo else True 65 | elif action == self._actions[3]: # delete 66 | data = [d for d in data if d != entry] 67 | 68 | return data 69 | 70 | def _check_for_correct_username(self, username: str) -> bool: 71 | if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: 72 | return True 73 | return False 74 | 75 | def _add_user(self) -> Optional[User]: 76 | prompt = '\n\n' + str(_('Enter username (leave blank to skip): ')) 77 | 78 | while True: 79 | username = input(prompt).strip(' ') 80 | if not username: 81 | return None 82 | if not self._check_for_correct_username(username): 83 | prompt = str(_("The username you entered is invalid. Try again")) + '\n' + prompt 84 | else: 85 | break 86 | 87 | password = get_password(prompt=str(_('Password for user "{}": ').format(username))) 88 | 89 | choice = Menu( 90 | str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(), 91 | skip=False, 92 | default_option=Menu.no(), 93 | clear_screen=False, 94 | show_search_hint=False 95 | ).run() 96 | 97 | sudo = True if choice.value == Menu.yes() else False 98 | return User(username, password, sudo) 99 | 100 | 101 | def ask_for_additional_users(prompt: str = '', defined_users: List[User] = []) -> List[User]: 102 | users = UserList(prompt, defined_users).run() 103 | return users 104 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # This file contains GitLab CI/CD configuration for the ArchInstall project. 2 | # It defines several jobs that get run when a new commit is made, and is comparable to the GitHub workflows. 3 | # There is an expectation that a runner exists that has the --privileged flag enabled for the build ISO job to run correctly. 4 | # These jobs should leverage the same tag as that runner. If necessary, change the tag from 'docker' to the one it uses. 5 | # All jobs will be run in the official archlinux container image, so we will declare that here. 6 | 7 | image: archlinux:latest 8 | 9 | # This can be used to handle common actions. In this case, we do a pacman -Sy to make sure repos are ready to use. 10 | before_script: 11 | - pacman -Sy 12 | 13 | stages: 14 | - lint 15 | - test 16 | - build 17 | - publish 18 | 19 | mypy: 20 | stage: lint 21 | tags: 22 | - docker 23 | script: 24 | - pacman --noconfirm -Syu python mypy 25 | - mypy . --ignore-missing-imports || exit 0 26 | 27 | flake8: 28 | stage: lint 29 | tags: 30 | - docker 31 | script: 32 | - pacman --noconfirm -Syu python python-pip 33 | - python -m pip install --upgrade pip 34 | - pip install flake8 35 | - flake8 . --count --select=E9,F63,F7 --show-source --statistics 36 | - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | 38 | # We currently do not have unit tests implemented but this stage is written in anticipation of their future usage. 39 | # When a stage name is preceeded with a '.' it's treated as "disabled" by GitLab and is not executed, so it's fine for it to be declared. 40 | .pytest: 41 | stage: test 42 | tags: 43 | - docker 44 | script: 45 | - pacman --noconfirm -Syu python python-pip 46 | - python -m pip install --upgrade pip 47 | - pip install pytest 48 | - pytest 49 | 50 | # This stage might fail with exit code 137 on a shared runner. This is probably due to the CPU/memory consumption needed to run the build. 51 | build_iso: 52 | stage: build 53 | tags: 54 | - docker 55 | script: 56 | - pwd 57 | - find . 58 | - cat /etc/os-release 59 | - mkdir -p /tmp/archlive/airootfs/root/archinstall-git; cp -r . /tmp/archlive/airootfs/root/archinstall-git 60 | - echo "pip uninstall archinstall -y; cd archinstall-git; python setup.py install" > /tmp/archlive/airootfs/root/.zprofile 61 | - echo "echo \"This is an unofficial ISO for development and testing of archinstall. No support will be provided.\"" >> /tmp/archlive/airootfs/root/.zprofile 62 | - echo "echo \"This ISO was built from Git SHA $CI_COMMIT_SHA\"" >> /tmp/archlive/airootfs/root/.zprofile 63 | - echo "echo \"Type archinstall to launch the installer.\"" >> /tmp/archlive/airootfs/root/.zprofile 64 | - cat /tmp/archlive/airootfs/root/.zprofile 65 | - pacman --noconfirm -S git archiso 66 | - cp -r /usr/share/archiso/configs/releng/* /tmp/archlive 67 | - echo -e "git\npython\npython-pip\npython-setuptools" >> /tmp/archlive/packages.x86_64 68 | - find /tmp/archlive 69 | - cd /tmp/archlive; mkarchiso -v -w work/ -o out/ ./ 70 | artifacts: 71 | name: "Arch Live ISO" 72 | paths: 73 | - /tmp/archlive/out/*.iso 74 | expire_in: 1 week 75 | 76 | ## This job only runs when a tag is created on the master branch. This is because we do not want to try to publish to PyPi every time we commit. 77 | ## The following CI/CD variables need to be set to the PyPi username and password in the GitLab project's settings for this stage to work. 78 | # * FLIT_USERNAME 79 | # * FLIT_PASSWORD 80 | publish_pypi: 81 | stage: publish 82 | tags: 83 | - docker 84 | script: 85 | - pacman --noconfirm -S python python-pip 86 | - python -m pip install --upgrade pip 87 | - pip install setuptools wheel flit 88 | - flit 89 | only: 90 | - tags 91 | except: 92 | - branches 93 | -------------------------------------------------------------------------------- /archinstall/lib/networking.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import socket 4 | import struct 5 | from typing import Union, Dict, Any, List 6 | 7 | from .exceptions import HardwareIncompatibilityError, SysCallError 8 | from .general import SysCommand 9 | from .output import log 10 | from .pacman import run_pacman 11 | from .storage import storage 12 | 13 | 14 | def get_hw_addr(ifname :str) -> str: 15 | import fcntl 16 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 17 | info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', bytes(ifname, 'utf-8')[:15])) 18 | return ':'.join('%02x' % b for b in info[18:24]) 19 | 20 | 21 | def list_interfaces(skip_loopback :bool = True) -> Dict[str, str]: 22 | interfaces = {} 23 | 24 | for index, iface in socket.if_nameindex(): 25 | if skip_loopback and iface == "lo": 26 | continue 27 | 28 | mac = get_hw_addr(iface).replace(':', '-').lower() 29 | interfaces[mac] = iface 30 | 31 | return interfaces 32 | 33 | 34 | def check_mirror_reachable() -> bool: 35 | log("Testing connectivity to the Arch Linux mirrors ...", level=logging.INFO) 36 | try: 37 | if run_pacman("-Sy").exit_code == 0: 38 | return True 39 | elif os.geteuid() != 0: 40 | log("check_mirror_reachable() uses 'pacman -Sy' which requires root.", level=logging.ERROR, fg="red") 41 | except SysCallError as err: 42 | log(err, level=logging.DEBUG) 43 | 44 | return False 45 | 46 | 47 | def update_keyring() -> bool: 48 | log("Updating archlinux-keyring ...", level=logging.INFO) 49 | if run_pacman("-Sy --noconfirm archlinux-keyring").exit_code == 0: 50 | return True 51 | 52 | elif os.geteuid() != 0: 53 | log("update_keyring() uses 'pacman -Sy archlinux-keyring' which requires root.", level=logging.ERROR, fg="red") 54 | 55 | return False 56 | 57 | 58 | def enrich_iface_types(interfaces: Union[Dict[str, Any], List[str]]) -> Dict[str, str]: 59 | result = {} 60 | 61 | for iface in interfaces: 62 | if os.path.isdir(f"/sys/class/net/{iface}/bridge/"): 63 | result[iface] = 'BRIDGE' 64 | elif os.path.isfile(f"/sys/class/net/{iface}/tun_flags"): 65 | # ethtool -i {iface} 66 | result[iface] = 'TUN/TAP' 67 | elif os.path.isdir(f"/sys/class/net/{iface}/device"): 68 | if os.path.isdir(f"/sys/class/net/{iface}/wireless/"): 69 | result[iface] = 'WIRELESS' 70 | else: 71 | result[iface] = 'PHYSICAL' 72 | else: 73 | result[iface] = 'UNKNOWN' 74 | 75 | return result 76 | 77 | 78 | def get_interface_from_mac(mac :str) -> str: 79 | return list_interfaces().get(mac.lower(), None) 80 | 81 | 82 | def wireless_scan(interface :str) -> None: 83 | interfaces = enrich_iface_types(list_interfaces().values()) 84 | if interfaces[interface] != 'WIRELESS': 85 | raise HardwareIncompatibilityError(f"Interface {interface} is not a wireless interface: {interfaces}") 86 | 87 | if not (output := SysCommand(f"iwctl station {interface} scan")).exit_code == 0: 88 | raise SystemError(f"Could not scan for wireless networks: {output}") 89 | 90 | if '_WIFI' not in storage: 91 | storage['_WIFI'] = {} 92 | if interface not in storage['_WIFI']: 93 | storage['_WIFI'][interface] = {} 94 | 95 | storage['_WIFI'][interface]['scanning'] = True 96 | 97 | 98 | # TODO: Full WiFi experience might get evolved in the future, pausing for now 2021-01-25 99 | def get_wireless_networks(interface :str) -> None: 100 | # TODO: Make this oneliner pritter to check if the interface is scanning or not. 101 | # TODO: Rename this to list_wireless_networks() as it doesn't return anything 102 | if '_WIFI' not in storage or interface not in storage['_WIFI'] or storage['_WIFI'][interface].get('scanning', False) is False: 103 | import time 104 | 105 | wireless_scan(interface) 106 | time.sleep(5) 107 | 108 | for line in SysCommand(f"iwctl station {interface} get-networks"): 109 | print(line) 110 | -------------------------------------------------------------------------------- /archinstall/lib/user_interaction/backwards_compatible_conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import sys 5 | from collections.abc import Iterable 6 | from typing import Any, Union, TYPE_CHECKING 7 | 8 | from ..exceptions import RequirementError 9 | from ..menu import Menu 10 | from ..output import log 11 | 12 | if TYPE_CHECKING: 13 | _: Any 14 | 15 | 16 | def generic_select( 17 | p_options: Union[list, dict], 18 | input_text: str = '', 19 | allow_empty_input: bool = True, 20 | options_output: bool = True, # function not available 21 | sort: bool = False, 22 | multi: bool = False, 23 | default: Any = None) -> Any: 24 | """ 25 | A generic select function that does not output anything 26 | other than the options and their indexes. As an example: 27 | 28 | generic_select(["first", "second", "third option"]) 29 | > first 30 | second 31 | third option 32 | When the user has entered the option correctly, 33 | this function returns an item from list, a string, or None 34 | 35 | Options can be any iterable. 36 | 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() 37 | Default value if not on the list of options will be added as the first element 38 | sort will be handled by Menu() 39 | """ 40 | # We check that the options are iterable. If not we abort. Else we copy them to lists 41 | # it options is a dictionary we use the values as entries of the list 42 | # if options is a string object, each character becomes an entry 43 | # if options is a list, we implictily build a copy to maintain immutability 44 | if not isinstance(p_options, Iterable): 45 | log(f"Objects of type {type(p_options)} is not iterable, and are not supported at generic_select", fg="red") 46 | log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>", level=logging.WARNING) 47 | raise RequirementError("generic_select() requires an iterable as option.") 48 | 49 | input_text = input_text if input_text else _('Select one of the values shown below: ') 50 | 51 | if isinstance(p_options, dict): 52 | options = list(p_options.values()) 53 | else: 54 | options = list(p_options) 55 | # check that the default value is in the list. If not it will become the first entry 56 | if default and default not in options: 57 | options.insert(0, default) 58 | 59 | # one of the drawbacks of the new interface is that in only allows string like options, so we do a conversion 60 | # also for the default value if it exists 61 | soptions = list(map(str, options)) 62 | default_value = options[options.index(default)] if default else None 63 | 64 | selected_option = Menu(input_text, 65 | soptions, 66 | skip=allow_empty_input, 67 | multi=multi, 68 | default_option=default_value, 69 | sort=sort).run() 70 | # we return the original objects, not the strings. 71 | # options is the list with the original objects and soptions the list with the string values 72 | # thru the map, we get from the value selected in soptions it index, and thu it the original object 73 | if not selected_option: 74 | return selected_option 75 | elif isinstance(selected_option, list): # for multi True 76 | selected_option = list(map(lambda x: options[soptions.index(x)], selected_option)) 77 | else: # for multi False 78 | selected_option = options[soptions.index(selected_option)] 79 | return selected_option 80 | 81 | 82 | def generic_multi_select(p_options: Union[list, dict], 83 | text: str = '', 84 | sort: bool = False, 85 | default: Any = None, 86 | allow_empty: bool = False) -> Any: 87 | 88 | text = text if text else _("Select one or more of the options below: ") 89 | 90 | return generic_select(p_options, 91 | input_text=text, 92 | allow_empty_input=allow_empty, 93 | sort=sort, 94 | multi=True, 95 | default=default) 96 | -------------------------------------------------------------------------------- /archinstall/lib/packages/packages.py: -------------------------------------------------------------------------------- 1 | import json 2 | import ssl 3 | from typing import Dict, Any, Tuple, List 4 | from urllib.error import HTTPError 5 | from urllib.parse import urlencode 6 | from urllib.request import urlopen 7 | 8 | from ..exceptions import PackageError, SysCallError 9 | from ..models.dataclasses import PackageSearch, PackageSearchResult, LocalPackage 10 | from ..pacman import run_pacman 11 | 12 | BASE_URL_PKG_SEARCH = 'https://archlinux.org/packages/search/json/' 13 | # BASE_URL_PKG_CONTENT = 'https://archlinux.org/packages/search/json/' 14 | BASE_GROUP_URL = 'https://archlinux.org/groups/search/json/' 15 | 16 | 17 | def _make_request(url: str, params: Dict) -> Any: 18 | ssl_context = ssl.create_default_context() 19 | ssl_context.check_hostname = False 20 | ssl_context.verify_mode = ssl.CERT_NONE 21 | 22 | encoded = urlencode(params) 23 | full_url = f'{url}?{encoded}' 24 | 25 | return urlopen(full_url, context=ssl_context) 26 | 27 | 28 | def group_search(name :str) -> List[PackageSearchResult]: 29 | # TODO UPSTREAM: Implement /json/ for the groups search 30 | try: 31 | response = _make_request(BASE_GROUP_URL, {'name': name}) 32 | except HTTPError as err: 33 | if err.code == 404: 34 | return [] 35 | else: 36 | raise err 37 | 38 | # Just to be sure some code didn't slip through the exception 39 | data = response.read().decode('UTF-8') 40 | 41 | return [PackageSearchResult(**package) for package in json.loads(data)['results']] 42 | 43 | 44 | def package_search(package :str) -> PackageSearch: 45 | """ 46 | Finds a specific package via the package database. 47 | It makes a simple web-request, which might be a bit slow. 48 | """ 49 | # TODO UPSTREAM: Implement bulk search, either support name=X&name=Y or split on space (%20 or ' ') 50 | # TODO: utilize pacman cache first, upstream second. 51 | response = _make_request(BASE_URL_PKG_SEARCH, {'name': package}) 52 | 53 | if response.code != 200: 54 | raise PackageError(f"Could not locate package: [{response.code}] {response}") 55 | 56 | data = response.read().decode('UTF-8') 57 | 58 | return PackageSearch(**json.loads(data)) 59 | 60 | 61 | def find_package(package :str) -> List[PackageSearchResult]: 62 | data = package_search(package) 63 | results = [] 64 | 65 | for result in data.results: 66 | if result.pkgname == package: 67 | results.append(result) 68 | 69 | # If we didn't find the package in the search results, 70 | # odds are it's a group package 71 | if not results: 72 | # Check if the package is actually a group 73 | for result in group_search(package): 74 | results.append(result) 75 | 76 | return results 77 | 78 | 79 | def find_packages(*names :str) -> Dict[str, Any]: 80 | """ 81 | This function returns the search results for many packages. 82 | The function itself is rather slow, so consider not sending to 83 | many packages to the search query. 84 | """ 85 | result = {} 86 | for package in names: 87 | for found_package in find_package(package): 88 | result[package] = found_package 89 | 90 | return result 91 | 92 | 93 | def validate_package_list(packages :list) -> Tuple[list, list]: 94 | """ 95 | Validates a list of given packages. 96 | return: Tuple of lists containing valid packavges in the first and invalid 97 | packages in the second entry 98 | """ 99 | valid_packages = {package for package in packages if find_package(package)} 100 | invalid_packages = set(packages) - valid_packages 101 | 102 | return list(valid_packages), list(invalid_packages) 103 | 104 | 105 | def installed_package(package :str) -> LocalPackage: 106 | package_info = {} 107 | try: 108 | for line in run_pacman(f"-Q --info {package}"): 109 | if b':' in line: 110 | key, value = line.decode().split(':', 1) 111 | package_info[key.strip().lower().replace(' ', '_')] = value.strip() 112 | except SysCallError: 113 | pass 114 | 115 | return LocalPackage(**package_info) 116 | -------------------------------------------------------------------------------- /archinstall/lib/plugins.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import importlib 3 | import logging 4 | import os 5 | import sys 6 | import pathlib 7 | import urllib.parse 8 | import urllib.request 9 | from importlib import metadata 10 | from typing import Optional, List 11 | from types import ModuleType 12 | 13 | from .output import log 14 | from .storage import storage 15 | 16 | plugins = {} 17 | 18 | # 1: List archinstall.plugin definitions 19 | # 2: Load the plugin entrypoint 20 | # 3: Initiate the plugin and store it as .name in plugins 21 | for plugin_definition in metadata.entry_points().select(group='archinstall.plugin'): 22 | plugin_entrypoint = plugin_definition.load() 23 | try: 24 | plugins[plugin_definition.name] = plugin_entrypoint() 25 | except Exception as err: 26 | log(err, level=logging.ERROR) 27 | log(f"The above error was detected when loading the plugin: {plugin_definition}", fg="red", level=logging.ERROR) 28 | 29 | 30 | # The following functions and core are support structures for load_plugin() 31 | def localize_path(profile_path :str) -> str: 32 | if (url := urllib.parse.urlparse(profile_path)).scheme and url.scheme in ('https', 'http'): 33 | converted_path = f"/tmp/{os.path.basename(profile_path).replace('.py', '')}_{hashlib.md5(os.urandom(12)).hexdigest()}.py" 34 | 35 | with open(converted_path, "w") as temp_file: 36 | temp_file.write(urllib.request.urlopen(url.geturl()).read().decode('utf-8')) 37 | 38 | return converted_path 39 | else: 40 | return profile_path 41 | 42 | 43 | def import_via_path(path :str, namespace :Optional[str] = None) -> ModuleType: 44 | if not namespace: 45 | namespace = os.path.basename(path) 46 | 47 | if namespace == '__init__.py': 48 | path = pathlib.PurePath(path) 49 | namespace = path.parent.name 50 | 51 | try: 52 | spec = importlib.util.spec_from_file_location(namespace, path) 53 | imported = importlib.util.module_from_spec(spec) 54 | sys.modules[namespace] = imported 55 | spec.loader.exec_module(sys.modules[namespace]) 56 | 57 | return namespace 58 | except Exception as err: 59 | log(err, level=logging.ERROR) 60 | log(f"The above error was detected when loading the plugin: {path}", fg="red", level=logging.ERROR) 61 | 62 | try: 63 | del(sys.modules[namespace]) # noqa: E275 64 | except: 65 | pass 66 | 67 | def find_nth(haystack :List[str], needle :str, n :int) -> int: 68 | start = haystack.find(needle) 69 | while start >= 0 and n > 1: 70 | start = haystack.find(needle, start + len(needle)) 71 | n -= 1 72 | return start 73 | 74 | def load_plugin(path :str) -> ModuleType: 75 | parsed_url = urllib.parse.urlparse(path) 76 | log(f"Loading plugin {parsed_url}.", fg="gray", level=logging.INFO) 77 | 78 | # The Profile was not a direct match on a remote URL 79 | if not parsed_url.scheme: 80 | # Path was not found in any known examples, check if it's an absolute path 81 | if os.path.isfile(path): 82 | namespace = import_via_path(path) 83 | elif parsed_url.scheme in ('https', 'http'): 84 | namespace = import_via_path(localize_path(path)) 85 | 86 | if namespace in sys.modules: 87 | # Version dependency via __archinstall__version__ variable (if present) in the plugin 88 | # Any errors in version inconsistency will be handled through normal error handling if not defined. 89 | if hasattr(sys.modules[namespace], '__archinstall__version__'): 90 | archinstall_major_and_minor_version = float(storage['__version__'][:find_nth(storage['__version__'], '.', 2)]) 91 | 92 | if sys.modules[namespace].__archinstall__version__ < archinstall_major_and_minor_version: 93 | log(f"Plugin {sys.modules[namespace]} does not support the current Archinstall version.", fg="red", level=logging.ERROR) 94 | 95 | # Locate the plugin entry-point called Plugin() 96 | # This in accordance with the entry_points() from setup.cfg above 97 | if hasattr(sys.modules[namespace], 'Plugin'): 98 | try: 99 | plugins[namespace] = sys.modules[namespace].Plugin() 100 | log(f"Plugin {plugins[namespace]} has been loaded.", fg="gray", level=logging.INFO) 101 | except Exception as err: 102 | log(err, level=logging.ERROR) 103 | log(f"The above error was detected when initiating the plugin: {path}", fg="red", level=logging.ERROR) 104 | else: 105 | log(f"Plugin '{path}' is missing a valid entry-point or is corrupt.", fg="yellow", level=logging.WARNING) 106 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | 5 | sys.path.insert(0, os.path.abspath('..')) 6 | 7 | 8 | def process_docstring(app, what, name, obj, options, lines): 9 | spaces_pat = re.compile(r"( {8})") 10 | ll = [spaces_pat.sub(" ", line) for line in lines] 11 | lines[:] = ll 12 | 13 | 14 | def setup(app): 15 | app.connect('autodoc-process-docstring', process_docstring) 16 | 17 | 18 | # Configuration file for the Sphinx documentation builder. 19 | # 20 | # This file only contains a selection of the most common options. For a full 21 | # list see the documentation: 22 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 23 | 24 | # -- Path setup -------------------------------------------------------------- 25 | 26 | # If extensions (or modules to document with autodoc) are in another directory, 27 | # add these directories to sys.path here. If the directory is relative to the 28 | # documentation root, use os.path.abspath to make it absolute, like shown here. 29 | # 30 | # import os 31 | # import sys 32 | # sys.path.insert(0, os.path.abspath('.')) 33 | 34 | 35 | # -- Project information ----------------------------------------------------- 36 | 37 | project = 'python-archinstall' 38 | copyright = '2022, Anton Hvornum' 39 | author = 'Anton Hvornum' 40 | 41 | # The full version, including alpha/beta/rc tags 42 | release = 'v2.3.0' 43 | 44 | # -- General configuration --------------------------------------------------- 45 | 46 | master_doc = 'index' 47 | # Add any Sphinx extension module names here, as strings. They can be 48 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 49 | # ones. 50 | extensions = [ 51 | 'sphinx.ext.autodoc', 52 | 'sphinx.ext.inheritance_diagram', 53 | 'sphinx.ext.todo' 54 | ] 55 | 56 | # Add any paths that contain templates here, relative to this directory. 57 | templates_path = ['_templates'] 58 | 59 | # List of patterns, relative to source directory, that match files and 60 | # directories to ignore when looking for source files. 61 | # This pattern also affects html_static_path and html_extra_path. 62 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 63 | 64 | # -- Options for HTML output ------------------------------------------------- 65 | 66 | # The theme to use for HTML and HTML Help pages. See the documentation for 67 | # a list of builtin themes. 68 | # 69 | # html_theme = 'alabaster' 70 | html_theme = 'sphinx_rtd_theme' 71 | 72 | html_logo = "_static/logo.png" 73 | 74 | # Add any paths that contain custom static files (such as style sheets) here, 75 | # relative to this directory. They are copied after the builtin static files, 76 | # so a file named "default.css" will overwrite the builtin "default.css". 77 | html_static_path = ['_static'] 78 | 79 | # If false, no module index is generated. 80 | html_domain_indices = True 81 | 82 | # If false, no index is generated. 83 | html_use_index = True 84 | 85 | # If true, the index is split into individual pages for each letter. 86 | html_split_index = True 87 | 88 | # If true, links to the reST sources are added to the pages. 89 | html_show_sourcelink = False 90 | 91 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 92 | # html_show_sphinx = True 93 | 94 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 95 | # html_show_copyright = True 96 | 97 | # If true, an OpenSearch description file will be output, and all pages will 98 | # contain a tag referring to it. The value of this option must be the 99 | # base URL from which the finished HTML is served. 100 | # html_use_opensearch = '' 101 | 102 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 103 | # html_file_suffix = None 104 | 105 | # Output file base name for HTML help builder. 106 | htmlhelp_basename = 'archinstalldoc' 107 | 108 | # -- Options for manual page output -------------------------------------------- 109 | 110 | # One entry per manual page. List of tuples 111 | # (source start file, name, description, authors, manual section). 112 | man_pages = [("index", "archinstall", u"archinstall Documentation", [u"Anton Hvornum"], 1)] 113 | 114 | # If true, show URL addresses after external links. 115 | # man_show_urls = False 116 | 117 | 118 | # -- Options for Texinfo output ------------------------------------------------ 119 | 120 | # Grouping the document tree into Texinfo files. List of tuples 121 | # (source start file, target name, title, author, 122 | # dir menu entry, description, category) 123 | texinfo_documents = [ 124 | ("index", "archinstall", u"archinstall Documentation", u"Anton Hvornum", "archinstall", "Simple and minimal HTTP server."), 125 | ] 126 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to archinstall 2 | 3 | Any contributions through pull requests are welcome as this project aims to be a community based project to ease some Arch Linux installation steps. 4 | Bear in mind that in the future this repo might be transferred to the official [GitLab repo under Arch Linux](http://gitlab.archlinux.org/archlinux/) *(if GitLab becomes open to the general public)*. 5 | 6 | Therefore, guidelines and style changes to the code might come into effect as well as guidelines surrounding bug reporting and discussions. 7 | 8 | ## Branches 9 | 10 | `master` is currently the default branch, and that's where all future feature work is being done, this means that `master` is a living entity and will most likely never be in a fully stable state. 11 | For stable releases, please see the tagged commits. 12 | 13 | Patch releases will be done against their own branches, branched from stable tagged releases and will be named according to the version it will become on release. 14 | *(Patches to `v2.1.4` will be done on branch `v2.1.5` for instance)*. 15 | 16 | ## Discussions 17 | 18 | Currently, questions, bugs and suggestions should be reported through [GitHub issue tracker](https://github.com/archlinux/archinstall/issues).
19 | For less formal discussions there is also an [archinstall Discord server](https://discord.gg/cqXU88y). 20 | 21 | ## Coding convention 22 | 23 | ArchInstall's goal is to follow [PEP8](https://www.python.org/dev/peps/pep-0008/) as best as it can with some minor exceptions.
24 | 25 | The exceptions to PEP8 are: 26 | 27 | * Archinstall uses [tabs instead of spaces](https://www.python.org/dev/peps/pep-0008/#tabs-or-spaces) simply to make it 28 | easier for non-IDE developers to navigate the code *(Tab display-width should be equal to 4 spaces)*. Exception to the 29 | rule are comments that need fine-tuned indentation for documentation purposes. 30 | * [Line length](https://www.python.org/dev/peps/pep-0008/#maximum-line-length) should aim for no more than 100 31 | characters, but not strictly enforced. 32 | * [Line breaks before/after binary operator](https://www.python.org/dev/peps/pep-0008/#should-a-line-break-before-or-after-a-binary-operator) 33 | is not enforced, as long as the style of line breaks is consistent within the same code block. 34 | * Archinstall should always be saved with **Unix-formatted line endings** and no other platform-specific formats. 35 | * [String quotes](https://www.python.org/dev/peps/pep-0008/#string-quotes) follow PEP8, the exception being when 36 | creating formatted strings, double-quoted strings are *preferred* but not required on the outer edges *( 37 | Example: `f"Welcome {name}"` rather than `f'Welcome {name}'`)*. 38 | 39 | Most of these style guidelines have been put into place after the fact *(in an attempt to clean up the code)*.
40 | There might therefore be older code which does not follow the coding convention and the code is subject to change. 41 | 42 | ## Documentation 43 | 44 | If you'd like to contribute to the documentation, refer to [this guide](docs/README.md) on how to build the documentation locally. 45 | 46 | ## Submitting Changes 47 | 48 | Archinstall uses GitHub's pull-request workflow and all contributions in terms of code should be done through pull requests.
49 | 50 | Anyone interested in archinstall may review your code. One of the core developers will merge your pull request when they 51 | think it is ready. For every pull request, we aim to promptly either merge it or say why it is not yet ready; if you go 52 | a few days without a reply, please feel free to ping the thread by adding a new comment. 53 | 54 | To get your pull request merged sooner, you should explain why you are making the change. For example, you can point to 55 | a code sample that is outdated in terms of Arch Linux command lines. It is also helpful to add links to online 56 | documentation or to the implementation of the code you are changing. 57 | 58 | Also, do not squash your commits after you have submitted a pull request, as this erases context during review. We will 59 | squash commits when the pull request is merged. 60 | 61 | At present the current contributors are (alphabetically): 62 | 63 | * Anton Hvornum ([@Torxed](https://github.com/Torxed)) 64 | * Borislav Kosharov ([@nikibobi](https://github.com/nikibobi)) 65 | * demostanis ([@demostanis](https://github.com/demostanis)) 66 | * Dylan Taylor ([@dylanmtaylor](https://github.com/dylanmtaylor)) 67 | * Giancarlo Razzolini (@[grazzolini](https://github.com/grazzolini)) 68 | * j-james ([@j-james](https://github.com/j-james)) 69 | * Jerker Bengtsson ([@jaybent](https://github.com/jaybent)) 70 | * Ninchester ([@ninchester](https://github.com/ninchester)) 71 | * Philipp Schaffrath ([@phisch](https://github.com/phisch)) 72 | * Varun Madiath ([@vamega](https://github.com/vamega)) 73 | * nullrequest ([@advaithm](https://github.com/advaithm)) 74 | -------------------------------------------------------------------------------- /archinstall/lib/disk/btrfs/btrfspartition.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import pathlib 3 | import logging 4 | from typing import Optional, TYPE_CHECKING 5 | 6 | from ...exceptions import DiskError 7 | from ...storage import storage 8 | from ...output import log 9 | from ...general import SysCommand 10 | from ..partition import Partition 11 | from ..helpers import findmnt 12 | from .btrfs_helpers import ( 13 | subvolume_info_from_path 14 | ) 15 | 16 | if TYPE_CHECKING: 17 | from ...installer import Installer 18 | from .btrfssubvolumeinfo import BtrfsSubvolumeInfo 19 | 20 | 21 | class BTRFSPartition(Partition): 22 | def __init__(self, *args, **kwargs): 23 | Partition.__init__(self, *args, **kwargs) 24 | 25 | @property 26 | def subvolumes(self): 27 | for filesystem in findmnt(pathlib.Path(self.path), recurse=True).get('filesystems', []): 28 | if '[' in filesystem.get('source', ''): 29 | yield subvolume_info_from_path(filesystem['target']) 30 | 31 | def iterate_children(struct): 32 | for c in struct.get('children', []): 33 | if '[' in child.get('source', ''): 34 | yield subvolume_info_from_path(c['target']) 35 | 36 | for sub_child in iterate_children(c): 37 | yield sub_child 38 | 39 | for child in iterate_children(filesystem): 40 | yield child 41 | 42 | def create_subvolume(self, subvolume :pathlib.Path, installation :Optional['Installer'] = None) -> 'BtrfsSubvolumeInfo': 43 | """ 44 | Subvolumes have to be created within a mountpoint. 45 | This means we need to get the current installation target. 46 | After we get it, we need to verify it is a btrfs subvolume filesystem. 47 | Finally, the destination must be empty. 48 | """ 49 | 50 | # Allow users to override the installation session 51 | if not installation: 52 | installation = storage.get('installation_session') 53 | 54 | # Determain if the path given, is an absolute path or a relative path. 55 | # We do this by checking if the path contains a known mountpoint. 56 | if str(subvolume)[0] == '/': 57 | if filesystems := findmnt(subvolume, traverse=True).get('filesystems'): 58 | if (target := filesystems[0].get('target')) and target != '/' and str(subvolume).startswith(target): 59 | # Path starts with a known mountpoint which isn't / 60 | # Which means it's an absolute path to a mounted location. 61 | pass 62 | else: 63 | # Since it's not an absolute position with a known start. 64 | # We omit the anchor ('/' basically) and make sure it's appendable 65 | # to the installation.target later 66 | subvolume = subvolume.relative_to(subvolume.anchor) 67 | # else: We don't need to do anything about relative paths, they should be appendable to installation.target as-is. 68 | 69 | # If the subvolume is not absolute, then we do two checks: 70 | # 1. Check if the partition itself is mounted somewhere, and use that as a root 71 | # 2. Use an active Installer().target as the root, assuming it's filesystem is btrfs 72 | # If both above fail, we need to warn the user that such setup is not supported. 73 | if str(subvolume)[0] != '/': 74 | if self.mountpoint is None and installation is None: 75 | 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.") 76 | elif self.mountpoint: 77 | subvolume = self.mountpoint / subvolume 78 | elif installation: 79 | ongoing_installation_destination = installation.target 80 | if type(ongoing_installation_destination) == str: 81 | ongoing_installation_destination = pathlib.Path(ongoing_installation_destination) 82 | 83 | subvolume = ongoing_installation_destination / subvolume 84 | 85 | subvolume.parent.mkdir(parents=True, exist_ok=True) 86 | 87 | # 95 | 96 | log(f'Attempting to create subvolume at {subvolume}', level=logging.DEBUG, fg="grey") 97 | 98 | if glob.glob(str(subvolume / '*')): 99 | raise DiskError(f"Cannot create subvolume at {subvolume} because it contains data (non-empty folder target is not supported by BTRFS)") 100 | # Ideally we would like to check if the destination is already a subvolume. 101 | # But then we would need the mount-point at this stage as well. 102 | # So we'll comment out this check: 103 | # elif subvolinfo := subvolume_info_from_path(subvolume): 104 | # raise DiskError(f"Destination {subvolume} is already a subvolume: {subvolinfo}") 105 | 106 | # And deal with it here: 107 | SysCommand(f"btrfs subvolume create {subvolume}") 108 | 109 | return subvolume_info_from_path(subvolume) 110 | -------------------------------------------------------------------------------- /archinstall/lib/systemd.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from typing import Iterator 4 | from .exceptions import SysCallError 5 | from .general import SysCommand, SysCommandWorker, locate_binary 6 | from .installer import Installer 7 | from .output import log 8 | from .storage import storage 9 | 10 | 11 | class Ini: 12 | def __init__(self, *args :str, **kwargs :str): 13 | """ 14 | Limited INI handler for now. 15 | Supports multiple keywords through dictionary list items. 16 | """ 17 | self.kwargs = kwargs 18 | 19 | def __str__(self) -> str: 20 | result = '' 21 | first_row_done = False 22 | for top_level in self.kwargs: 23 | if first_row_done: 24 | result += f"\n[{top_level}]\n" 25 | else: 26 | result += f"[{top_level}]\n" 27 | first_row_done = True 28 | 29 | for key, val in self.kwargs[top_level].items(): 30 | if type(val) == list: 31 | for item in val: 32 | result += f"{key}={item}\n" 33 | else: 34 | result += f"{key}={val}\n" 35 | 36 | return result 37 | 38 | 39 | class Systemd(Ini): 40 | """ 41 | Placeholder class to do systemd specific setups. 42 | """ 43 | 44 | 45 | class Networkd(Systemd): 46 | """ 47 | Placeholder class to do systemd-network specific setups. 48 | """ 49 | 50 | 51 | class Boot: 52 | def __init__(self, installation: Installer): 53 | self.instance = installation 54 | self.container_name = 'archinstall' 55 | self.session = None 56 | self.ready = False 57 | 58 | def __enter__(self) -> 'Boot': 59 | if (existing_session := storage.get('active_boot', None)) and existing_session.instance != self.instance: 60 | raise KeyError("Archinstall only supports booting up one instance, and a active session is already active and it is not this one.") 61 | 62 | if existing_session: 63 | self.session = existing_session.session 64 | self.ready = existing_session.ready 65 | else: 66 | self.session = SysCommandWorker([ 67 | '/usr/bin/systemd-nspawn', 68 | '-D', self.instance.target, 69 | '--timezone=off', 70 | '-b', 71 | '--no-pager', 72 | '--machine', self.container_name 73 | ]) 74 | # '-P' or --console=pipe could help us not having to do a bunch of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual. 75 | 76 | if not self.ready: 77 | while self.session.is_alive(): 78 | if b' login:' in self.session: 79 | self.ready = True 80 | break 81 | 82 | storage['active_boot'] = self 83 | return self 84 | 85 | def __exit__(self, *args :str, **kwargs :str) -> None: 86 | # b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync. 87 | # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager 88 | 89 | if len(args) >= 2 and args[1]: 90 | log(args[1], level=logging.ERROR, fg='red') 91 | log(f"The error above occurred in a temporary boot-up of the installation {self.instance}", level=logging.ERROR, fg="red") 92 | 93 | shutdown = None 94 | shutdown_exit_code = -1 95 | 96 | try: 97 | shutdown = SysCommand(f'systemd-run --machine={self.container_name} --pty shutdown now') 98 | except SysCallError as error: 99 | shutdown_exit_code = error.exit_code 100 | # if error.exit_code == 256: 101 | # pass 102 | 103 | while self.session.is_alive(): 104 | time.sleep(0.25) 105 | 106 | if shutdown: 107 | shutdown_exit_code = shutdown.exit_code 108 | 109 | if self.session.exit_code == 0 or shutdown_exit_code == 0: 110 | storage['active_boot'] = None 111 | else: 112 | raise SysCallError(f"Could not shut down temporary boot of {self.instance}: {self.session.exit_code}/{shutdown_exit_code}", exit_code=next(filter(bool, [self.session.exit_code, shutdown_exit_code]))) 113 | 114 | def __iter__(self) -> Iterator[str]: 115 | if self.session: 116 | for value in self.session: 117 | yield value 118 | 119 | def __contains__(self, key: bytes) -> bool: 120 | if self.session is None: 121 | return False 122 | 123 | return key in self.session 124 | 125 | def is_alive(self) -> bool: 126 | if self.session is None: 127 | return False 128 | 129 | return self.session.is_alive() 130 | 131 | def SysCommand(self, cmd: list, *args, **kwargs) -> SysCommand: 132 | if cmd[0][0] != '/' and cmd[0][:2] != './': 133 | # This check is also done in SysCommand & SysCommandWorker. 134 | # However, that check is done for `machinectl` and not for our chroot command. 135 | # So this wrapper for SysCommand will do this additionally. 136 | 137 | cmd[0] = locate_binary(cmd[0]) 138 | 139 | return SysCommand(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) 140 | 141 | def SysCommandWorker(self, cmd: list, *args, **kwargs) -> SysCommandWorker: 142 | if cmd[0][0] != '/' and cmd[0][:2] != './': 143 | cmd[0] = locate_binary(cmd[0]) 144 | 145 | return SysCommandWorker(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) 146 | -------------------------------------------------------------------------------- /archinstall/lib/menu/list_manager.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from os import system 3 | from typing import Any, TYPE_CHECKING, Dict, Optional, Tuple, List 4 | 5 | from .menu import Menu 6 | 7 | if TYPE_CHECKING: 8 | _: Any 9 | 10 | 11 | class ListManager: 12 | def __init__( 13 | self, 14 | prompt: str, 15 | entries: List[Any], 16 | base_actions: List[str], 17 | sub_menu_actions: List[str] 18 | ): 19 | """ 20 | :param prompt: Text which will appear at the header 21 | type param: string | DeferredTranslation 22 | 23 | :param entries: list/dict of option to be shown / manipulated 24 | type param: list 25 | 26 | :param base_actions: list of actions that is displayed in the main list manager, 27 | usually global actions such as 'Add...' 28 | type param: list 29 | 30 | :param sub_menu_actions: list of actions available for a chosen entry 31 | type param: list 32 | """ 33 | self._original_data = copy.deepcopy(entries) 34 | self._data = copy.deepcopy(entries) 35 | 36 | explainer = str(_('\n Choose an object from the list, and select one of the available actions for it to execute')) 37 | self._prompt = prompt + explainer if prompt else explainer 38 | 39 | self._separator = '' 40 | self._confirm_action = str(_('Confirm and exit')) 41 | self._cancel_action = str(_('Cancel')) 42 | 43 | self._terminate_actions = [self._confirm_action, self._cancel_action] 44 | self._base_actions = base_actions 45 | self._sub_menu_actions = sub_menu_actions 46 | 47 | self._last_choice = None 48 | 49 | @property 50 | def last_choice(self): 51 | return self._last_choice 52 | 53 | def run(self): 54 | while True: 55 | # this will return a dictionary with the key as the menu entry to be displayed 56 | # and the value is the original value from the self._data container 57 | data_formatted = self.reformat(self._data) 58 | options, header = self._prepare_selection(data_formatted) 59 | 60 | system('clear') 61 | 62 | choice = Menu( 63 | self._prompt, 64 | options, 65 | sort=False, 66 | clear_screen=False, 67 | clear_menu_on_exit=False, 68 | header=header, 69 | skip_empty_entries=True, 70 | skip=False, 71 | show_search_hint=False 72 | ).run() 73 | 74 | if choice.value in self._base_actions: 75 | self._data = self.handle_action(choice.value, None, self._data) 76 | elif choice.value in self._terminate_actions: 77 | break 78 | else: # an entry of the existing selection was choosen 79 | selected_entry = data_formatted[choice.value] 80 | self._run_actions_on_entry(selected_entry) 81 | 82 | self._last_choice = choice 83 | if choice.value == self._cancel_action: 84 | return self._original_data # return the original list 85 | else: 86 | return self._data 87 | 88 | def _prepare_selection(self, data_formatted: Dict[str, Any]) -> Tuple[List[str], str]: 89 | # header rows are mapped to None so make sure 90 | # to exclude those from the selectable data 91 | options: List[str] = [key for key, val in data_formatted.items() if val is not None] 92 | header = '' 93 | 94 | if len(options) > 0: 95 | table_header = [key for key, val in data_formatted.items() if val is None] 96 | header = '\n'.join(table_header) 97 | 98 | if len(options) > 0: 99 | options.append(self._separator) 100 | 101 | options += self._base_actions 102 | options += self._terminate_actions 103 | 104 | return options, header 105 | 106 | def _run_actions_on_entry(self, entry: Any): 107 | options = self.filter_options(entry,self._sub_menu_actions) + [self._cancel_action] 108 | display_value = self.selected_action_display(entry) 109 | 110 | prompt = _("Select an action for '{}'").format(display_value) 111 | 112 | choice = Menu( 113 | prompt, 114 | options, 115 | sort=False, 116 | clear_screen=False, 117 | clear_menu_on_exit=False, 118 | show_search_hint=False 119 | ).run() 120 | 121 | if choice.value and choice.value != self._cancel_action: 122 | self._data = self.handle_action(choice.value, entry, self._data) 123 | 124 | def selected_action_display(self, selection: Any) -> str: 125 | # this will return the value to be displayed in the 126 | # "Select an action for '{}'" string 127 | raise NotImplementedError('Please implement me in the child class') 128 | 129 | def reformat(self, data: List[Any]) -> Dict[str, Any]: 130 | # this should return a dictionary of display string to actual data entry 131 | # mapping; if the value for a given display string is None it will be used 132 | # in the header value (useful when displaying tables) 133 | raise NotImplementedError('Please implement me in the child class') 134 | 135 | def handle_action(self, action: Any, entry: Optional[Any], data: List[Any]) -> List[Any]: 136 | # this function is called when a base action or 137 | # a specific action for an entry is triggered 138 | raise NotImplementedError('Please implement me in the child class') 139 | 140 | def filter_options(self, selection :Any, options :List[str]) -> List[str]: 141 | # filter which actions to show for an specific selection 142 | return options 143 | -------------------------------------------------------------------------------- /archinstall/lib/hardware.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from functools import partial 4 | from pathlib import Path 5 | from typing import Iterator, Optional, Union 6 | 7 | from .general import SysCommand 8 | from .networking import list_interfaces, enrich_iface_types 9 | from .exceptions import SysCallError 10 | from .output import log 11 | 12 | __packages__ = [ 13 | "mesa", 14 | "xf86-video-amdgpu", 15 | "xf86-video-ati", 16 | "xf86-video-nouveau", 17 | "xf86-video-vmware", 18 | "libva-mesa-driver", 19 | "libva-intel-driver", 20 | "intel-media-driver", 21 | "vulkan-radeon", 22 | "vulkan-intel", 23 | "nvidia", 24 | ] 25 | 26 | AVAILABLE_GFX_DRIVERS = { 27 | # Sub-dicts are layer-2 options to be selected 28 | # and lists are a list of packages to be installed 29 | "All open-source (default)": [ 30 | "mesa", 31 | "xf86-video-amdgpu", 32 | "xf86-video-ati", 33 | "xf86-video-nouveau", 34 | "xf86-video-vmware", 35 | "libva-mesa-driver", 36 | "libva-intel-driver", 37 | "intel-media-driver", 38 | "vulkan-radeon", 39 | "vulkan-intel", 40 | ], 41 | "AMD / ATI (open-source)": [ 42 | "mesa", 43 | "xf86-video-amdgpu", 44 | "xf86-video-ati", 45 | "libva-mesa-driver", 46 | "vulkan-radeon", 47 | ], 48 | "Intel (open-source)": [ 49 | "mesa", 50 | "libva-intel-driver", 51 | "intel-media-driver", 52 | "vulkan-intel", 53 | ], 54 | "Nvidia (open kernel module for newer GPUs, Turing+)": ["nvidia-open"], 55 | "Nvidia (open-source nouveau driver)": [ 56 | "mesa", 57 | "xf86-video-nouveau", 58 | "libva-mesa-driver" 59 | ], 60 | "Nvidia (proprietary)": ["nvidia"], 61 | "VMware / VirtualBox (open-source)": ["mesa", "xf86-video-vmware"], 62 | } 63 | 64 | CPUINFO = Path("/proc/cpuinfo") 65 | MEMINFO = Path("/proc/meminfo") 66 | 67 | 68 | def cpuinfo() -> Iterator[dict[str, str]]: 69 | """Yields information about the CPUs of the system.""" 70 | cpu = {} 71 | 72 | with CPUINFO.open() as file: 73 | for line in file: 74 | if not (line := line.strip()): 75 | yield cpu 76 | cpu = {} 77 | continue 78 | 79 | key, value = line.split(":", maxsplit=1) 80 | cpu[key.strip()] = value.strip() 81 | 82 | 83 | def meminfo(key: Optional[str] = None) -> Union[dict[str, int], Optional[int]]: 84 | """Returns a dict with memory info if called with no args 85 | or the value of the given key of said dict. 86 | """ 87 | with MEMINFO.open() as file: 88 | mem_info = { 89 | (columns := line.strip().split())[0].rstrip(':'): int(columns[1]) 90 | for line in file 91 | } 92 | 93 | if key is None: 94 | return mem_info 95 | 96 | return mem_info.get(key) 97 | 98 | 99 | def has_wifi() -> bool: 100 | return 'WIRELESS' in enrich_iface_types(list_interfaces().values()).values() 101 | 102 | 103 | def has_cpu_vendor(vendor_id: str) -> bool: 104 | return any(cpu.get("vendor_id") == vendor_id for cpu in cpuinfo()) 105 | 106 | 107 | has_amd_cpu = partial(has_cpu_vendor, "AuthenticAMD") 108 | 109 | 110 | has_intel_cpu = partial(has_cpu_vendor, "GenuineIntel") 111 | 112 | 113 | def has_uefi() -> bool: 114 | return os.path.isdir('/sys/firmware/efi') 115 | 116 | 117 | def graphics_devices() -> dict: 118 | cards = {} 119 | for line in SysCommand("lspci"): 120 | if b' VGA ' in line or b' 3D ' in line: 121 | _, identifier = line.split(b': ', 1) 122 | cards[identifier.strip().decode('UTF-8')] = line 123 | return cards 124 | 125 | 126 | def has_nvidia_graphics() -> bool: 127 | return any('nvidia' in x.lower() for x in graphics_devices()) 128 | 129 | 130 | def has_amd_graphics() -> bool: 131 | return any('amd' in x.lower() for x in graphics_devices()) 132 | 133 | 134 | def has_intel_graphics() -> bool: 135 | return any('intel' in x.lower() for x in graphics_devices()) 136 | 137 | 138 | def cpu_vendor() -> Optional[str]: 139 | for cpu in cpuinfo(): 140 | return cpu.get("vendor_id") 141 | 142 | return None 143 | 144 | 145 | def cpu_model() -> Optional[str]: 146 | for cpu in cpuinfo(): 147 | return cpu.get("model name") 148 | 149 | return None 150 | 151 | 152 | def sys_vendor() -> Optional[str]: 153 | with open(f"/sys/devices/virtual/dmi/id/sys_vendor") as vendor: 154 | return vendor.read().strip() 155 | 156 | 157 | def product_name() -> Optional[str]: 158 | with open(f"/sys/devices/virtual/dmi/id/product_name") as product: 159 | return product.read().strip() 160 | 161 | 162 | def mem_available() -> Optional[int]: 163 | return meminfo('MemAvailable') 164 | 165 | 166 | def mem_free() -> Optional[int]: 167 | return meminfo('MemFree') 168 | 169 | 170 | def mem_total() -> Optional[int]: 171 | return meminfo('MemTotal') 172 | 173 | 174 | def virtualization() -> Optional[str]: 175 | try: 176 | return str(SysCommand("systemd-detect-virt")).strip('\r\n') 177 | except SysCallError as error: 178 | log(f"Could not detect virtual system: {error}", level=logging.DEBUG) 179 | 180 | return None 181 | 182 | 183 | def is_vm() -> bool: 184 | try: 185 | return b"none" not in b"".join(SysCommand("systemd-detect-virt")).lower() 186 | except SysCallError as error: 187 | log(f"System is not running in a VM: {error}", level=logging.DEBUG) 188 | return None 189 | 190 | # TODO: Add more identifiers 191 | -------------------------------------------------------------------------------- /archinstall/lib/configuration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import stat 4 | import logging 5 | import pathlib 6 | from typing import Optional, Dict 7 | 8 | from .storage import storage 9 | from .general import JSON, UNSAFE_JSON 10 | from .output import log 11 | from .exceptions import RequirementError 12 | from .hsm import get_fido2_devices 13 | 14 | def configuration_sanity_check(): 15 | if storage['arguments'].get('HSM'): 16 | if not get_fido2_devices(): 17 | raise RequirementError( 18 | f"In order to use HSM to pair with the disk encryption," 19 | + f" one needs to be accessible through /dev/hidraw* and support" 20 | + f" the FIDO2 protocol. You can check this by running" 21 | + f" 'systemd-cryptenroll --fido2-device=list'." 22 | ) 23 | 24 | class ConfigurationOutput: 25 | def __init__(self, config: Dict): 26 | """ 27 | Configuration output handler to parse the existing configuration data structure and prepare for output on the 28 | console and for saving it to configuration files 29 | 30 | :param config: A dictionary containing configurations (basically archinstall.arguments) 31 | :type config: Dict 32 | """ 33 | self._config = config 34 | self._user_credentials = {} 35 | self._disk_layout = None 36 | self._user_config = {} 37 | self._default_save_path = pathlib.Path(storage.get('LOG_PATH', '.')) 38 | self._user_config_file = 'user_configuration.json' 39 | self._user_creds_file = "user_credentials.json" 40 | self._disk_layout_file = "user_disk_layout.json" 41 | 42 | self._sensitive = ['!users', '!encryption-password'] 43 | self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run'] 44 | 45 | self._process_config() 46 | 47 | @property 48 | def user_credentials_file(self): 49 | return self._user_creds_file 50 | 51 | @property 52 | def user_configuration_file(self): 53 | return self._user_config_file 54 | 55 | @property 56 | def disk_layout_file(self): 57 | return self._disk_layout_file 58 | 59 | def _process_config(self): 60 | for key in self._config: 61 | if key in self._sensitive: 62 | self._user_credentials[key] = self._config[key] 63 | elif key == 'disk_layouts': 64 | self._disk_layout = self._config[key] 65 | elif key in self._ignore: 66 | pass 67 | else: 68 | self._user_config[key] = self._config[key] 69 | 70 | def user_config_to_json(self) -> str: 71 | return json.dumps({ 72 | 'config_version': storage['__version__'], # Tells us what version was used to generate the config 73 | **self._user_config, # __version__ will be overwritten by old version definition found in config 74 | 'version': storage['__version__'] 75 | }, indent=4, sort_keys=True, cls=JSON) 76 | 77 | def disk_layout_to_json(self) -> Optional[str]: 78 | if self._disk_layout: 79 | return json.dumps(self._disk_layout, indent=4, sort_keys=True, cls=JSON) 80 | return None 81 | 82 | def user_credentials_to_json(self) -> Optional[str]: 83 | if self._user_credentials: 84 | return json.dumps(self._user_credentials, indent=4, sort_keys=True, cls=UNSAFE_JSON) 85 | return None 86 | 87 | def show(self): 88 | print(_('\nThis is your chosen configuration:')) 89 | log(" -- Chosen configuration --", level=logging.DEBUG) 90 | 91 | user_conig = self.user_config_to_json() 92 | disk_layout = self.disk_layout_to_json() 93 | log(user_conig, level=logging.INFO) 94 | 95 | if disk_layout: 96 | log(disk_layout, level=logging.INFO) 97 | 98 | print() 99 | 100 | def _is_valid_path(self, dest_path :pathlib.Path) -> bool: 101 | if (not dest_path.exists()) or not (dest_path.is_dir()): 102 | log( 103 | 'Destination directory {} does not exist or is not a directory,\n Configuration files can not be saved'.format(dest_path.resolve()), 104 | fg="yellow" 105 | ) 106 | return False 107 | return True 108 | 109 | def save_user_config(self, dest_path :pathlib.Path = None): 110 | if self._is_valid_path(dest_path): 111 | target = dest_path / self._user_config_file 112 | 113 | with open(target, 'w') as config_file: 114 | config_file.write(self.user_config_to_json()) 115 | 116 | os.chmod(str(dest_path / self._user_config_file), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) 117 | 118 | def save_user_creds(self, dest_path :pathlib.Path = None): 119 | if self._is_valid_path(dest_path): 120 | if user_creds := self.user_credentials_to_json(): 121 | target = dest_path / self._user_creds_file 122 | 123 | with open(target, 'w') as config_file: 124 | config_file.write(user_creds) 125 | 126 | os.chmod(str(target), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) 127 | 128 | def save_disk_layout(self, dest_path :pathlib.Path = None): 129 | if self._is_valid_path(dest_path): 130 | if disk_layout := self.disk_layout_to_json(): 131 | target = dest_path / self._disk_layout_file 132 | 133 | with target.open('w') as config_file: 134 | config_file.write(disk_layout) 135 | 136 | os.chmod(str(target), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) 137 | 138 | def save(self, dest_path :pathlib.Path = None): 139 | if not dest_path: 140 | dest_path = self._default_save_path 141 | 142 | if self._is_valid_path(dest_path): 143 | self.save_user_config(dest_path) 144 | self.save_user_creds(dest_path) 145 | self.save_disk_layout(dest_path) 146 | -------------------------------------------------------------------------------- /archinstall/lib/user_interaction/system_conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List, Any, Dict, TYPE_CHECKING 4 | 5 | from ..disk import all_blockdevices 6 | from ..exceptions import RequirementError 7 | from ..hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics 8 | from ..menu import Menu 9 | from ..menu.menu import MenuSelectionType 10 | from ..storage import storage 11 | 12 | if TYPE_CHECKING: 13 | _: Any 14 | 15 | 16 | def select_kernel(preset: List[str] = None) -> List[str]: 17 | """ 18 | Asks the user to select a kernel for system. 19 | 20 | :return: The string as a selected kernel 21 | :rtype: string 22 | """ 23 | 24 | kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] 25 | default_kernel = "linux" 26 | 27 | warning = str(_('Are you sure you want to reset this setting?')) 28 | 29 | choice = Menu( 30 | _('Choose which kernels to use or leave blank for default "{}"').format(default_kernel), 31 | kernels, 32 | sort=True, 33 | multi=True, 34 | preset_values=preset, 35 | raise_error_on_interrupt=True, 36 | raise_error_warning_msg=warning 37 | ).run() 38 | 39 | match choice.type_: 40 | case MenuSelectionType.Esc: return preset 41 | case MenuSelectionType.Ctrl_c: return [] 42 | case MenuSelectionType.Selection: return choice.value 43 | 44 | 45 | def select_harddrives(preset: List[str] = []) -> List[str]: 46 | """ 47 | Asks the user to select one or multiple hard drives 48 | 49 | :return: List of selected hard drives 50 | :rtype: list 51 | """ 52 | hard_drives = all_blockdevices(partitions=False).values() 53 | options = {f'{option}': option for option in hard_drives} 54 | 55 | title = str(_('Select one or more hard drives to use and configure\n')) 56 | title += str(_('Any modifications to the existing setting will reset the disk layout!')) 57 | 58 | warning = str(_('If you reset the harddrive selection this will also reset the current disk layout. Are you sure?')) 59 | 60 | selected_harddrive = Menu( 61 | title, 62 | list(options.keys()), 63 | preset_values=preset, 64 | multi=True, 65 | raise_error_on_interrupt=True, 66 | raise_error_warning_msg=warning 67 | ).run() 68 | 69 | match selected_harddrive.type_: 70 | case MenuSelectionType.Ctrl_c: return [] 71 | case MenuSelectionType.Esc: return preset 72 | case MenuSelectionType.Selection: return [options[i] for i in selected_harddrive.value] 73 | 74 | 75 | def select_driver(options: Dict[str, Any] = AVAILABLE_GFX_DRIVERS) -> str: 76 | """ 77 | Some what convoluted function, whose job is simple. 78 | Select a graphics driver from a pre-defined set of popular options. 79 | 80 | (The template xorg is for beginner users, not advanced, and should 81 | there for appeal to the general public first and edge cases later) 82 | """ 83 | 84 | drivers = sorted(list(options)) 85 | 86 | if drivers: 87 | arguments = storage.get('arguments', {}) 88 | title = '' 89 | 90 | if has_amd_graphics(): 91 | title += str(_( 92 | 'For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.' 93 | )) + '\n' 94 | if has_intel_graphics(): 95 | title += str(_( 96 | 'For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n' 97 | )) 98 | if has_nvidia_graphics(): 99 | title += str(_( 100 | 'For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n' 101 | )) 102 | 103 | title += str(_('\n\nSelect a graphics driver or leave blank to install all open-source drivers')) 104 | choice = Menu(title, drivers).run() 105 | 106 | if choice.type_ != MenuSelectionType.Selection: 107 | return arguments.get('gfx_driver') 108 | 109 | arguments['gfx_driver'] = choice.value 110 | return options.get(choice.value) 111 | 112 | raise RequirementError("Selecting drivers require a least one profile to be given as an option.") 113 | 114 | 115 | def ask_for_bootloader(advanced_options: bool = False, preset: str = None) -> str: 116 | if preset == 'systemd-bootctl': 117 | preset_val = 'systemd-boot' if advanced_options else Menu.no() 118 | elif preset == 'grub-install': 119 | preset_val = 'grub' if advanced_options else Menu.yes() 120 | else: 121 | preset_val = preset 122 | 123 | bootloader = "systemd-bootctl" if has_uefi() else "grub-install" 124 | 125 | if has_uefi(): 126 | if not advanced_options: 127 | selection = Menu( 128 | _('Would you like to use GRUB as a bootloader instead of systemd-boot?'), 129 | Menu.yes_no(), 130 | preset_values=preset_val, 131 | default_option=Menu.no() 132 | ).run() 133 | 134 | match selection.type_: 135 | case MenuSelectionType.Esc: return preset 136 | case MenuSelectionType.Selection: bootloader = 'grub-install' if selection.value == Menu.yes() else bootloader 137 | else: 138 | # We use the common names for the bootloader as the selection, and map it back to the expected values. 139 | choices = ['systemd-boot', 'grub', 'efistub'] 140 | selection = Menu(_('Choose a bootloader'), choices, preset_values=preset_val).run() 141 | 142 | value = '' 143 | match selection.type_: 144 | case MenuSelectionType.Esc: value = preset_val 145 | case MenuSelectionType.Selection: value = selection.value 146 | 147 | if value != "": 148 | if value == 'systemd-boot': 149 | bootloader = 'systemd-bootctl' 150 | elif value == 'grub': 151 | bootloader = 'grub-install' 152 | else: 153 | bootloader = value 154 | 155 | return bootloader 156 | 157 | 158 | def ask_for_swap(preset: bool = True) -> bool: 159 | if preset: 160 | preset_val = Menu.yes() 161 | else: 162 | preset_val = Menu.no() 163 | 164 | prompt = _('Would you like to use swap on zram?') 165 | choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run() 166 | 167 | match choice.type_: 168 | case MenuSelectionType.Esc: return preset 169 | case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True 170 | -------------------------------------------------------------------------------- /archinstall/lib/models/network_configuration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from enum import Enum 5 | from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING 6 | 7 | from ..output import log 8 | from ..storage import storage 9 | 10 | if TYPE_CHECKING: 11 | _: Any 12 | 13 | 14 | class NicType(str, Enum): 15 | ISO = "iso" 16 | NM = "nm" 17 | MANUAL = "manual" 18 | 19 | 20 | @dataclass 21 | class NetworkConfiguration: 22 | type: NicType 23 | iface: Optional[str] = None 24 | ip: Optional[str] = None 25 | dhcp: bool = True 26 | gateway: Optional[str] = None 27 | dns: Union[None, List[str]] = None 28 | 29 | def __str__(self): 30 | if self.is_iso(): 31 | return "Copy ISO configuration" 32 | elif self.is_network_manager(): 33 | return "Use NetworkManager" 34 | elif self.is_manual(): 35 | if self.dhcp: 36 | return f'iface={self.iface}, dhcp=auto' 37 | else: 38 | return f'iface={self.iface}, ip={self.ip}, dhcp=staticIp, gateway={self.gateway}, dns={self.dns}' 39 | else: 40 | return 'Unknown type' 41 | 42 | def as_json(self) -> Dict: 43 | exclude_fields = ['type'] 44 | data = {} 45 | for k, v in self.__dict__.items(): 46 | if k not in exclude_fields: 47 | if isinstance(v, list) and len(v) == 0: 48 | v = '' 49 | elif v is None: 50 | v = '' 51 | 52 | data[k] = v 53 | 54 | return data 55 | 56 | def json(self) -> Dict: 57 | # for json serialization when calling json.dumps(...) on this class 58 | return self.__dict__ 59 | 60 | def is_iso(self) -> bool: 61 | return self.type == NicType.ISO 62 | 63 | def is_network_manager(self) -> bool: 64 | return self.type == NicType.NM 65 | 66 | def is_manual(self) -> bool: 67 | return self.type == NicType.MANUAL 68 | 69 | 70 | class NetworkConfigurationHandler: 71 | def __init__(self, config: Union[None, NetworkConfiguration, List[NetworkConfiguration]] = None): 72 | self._configuration = config 73 | 74 | @property 75 | def configuration(self): 76 | return self._configuration 77 | 78 | def config_installer(self, installation: Any): 79 | if self._configuration is None: 80 | return 81 | 82 | if isinstance(self._configuration, list): 83 | for config in self._configuration: 84 | installation.configure_nic(config) 85 | 86 | installation.enable_service('systemd-networkd') 87 | installation.enable_service('systemd-resolved') 88 | else: 89 | # If user selected to copy the current ISO network configuration 90 | # Perform a copy of the config 91 | if self._configuration.is_iso(): 92 | installation.copy_iso_network_config( 93 | enable_services=True) # Sources the ISO network configuration to the install medium. 94 | elif self._configuration.is_network_manager(): 95 | installation.add_additional_packages(["networkmanager"]) 96 | if (profile := storage['arguments'].get('profile')) and profile.is_desktop_profile: 97 | installation.add_additional_packages(["network-manager-applet"]) 98 | installation.enable_service('NetworkManager.service') 99 | 100 | def _backwards_compability_config(self, config: Union[str,Dict[str, str]]) -> Union[List[NetworkConfiguration], NetworkConfiguration, None]: 101 | def get(config: Dict[str, str], key: str) -> List[str]: 102 | if (value := config.get(key, None)) is not None: 103 | return [value] 104 | return [] 105 | 106 | if isinstance(config, str): # is a ISO network 107 | return NetworkConfiguration(NicType.ISO) 108 | elif config.get('NetworkManager'): # is a network manager configuration 109 | return NetworkConfiguration(NicType.NM) 110 | elif 'ip' in config: 111 | return [NetworkConfiguration( 112 | NicType.MANUAL, 113 | iface=config.get('nic', ''), 114 | ip=config.get('ip'), 115 | gateway=config.get('gateway', ''), 116 | dns=get(config, 'dns'), 117 | dhcp=False 118 | )] 119 | elif 'nic' in config: 120 | return [NetworkConfiguration( 121 | NicType.MANUAL, 122 | iface=config.get('nic', ''), 123 | dhcp=True 124 | )] 125 | else: # not recognized 126 | return None 127 | 128 | def _parse_manual_config(self, configs: List[Dict[str, Any]]) -> Optional[List[NetworkConfiguration]]: 129 | configurations = [] 130 | 131 | for manual_config in configs: 132 | iface = manual_config.get('iface', None) 133 | 134 | if iface is None: 135 | log(_('No iface specified for manual configuration')) 136 | exit(1) 137 | 138 | if manual_config.get('dhcp', False) or not any([manual_config.get(v, '') for v in ['ip', 'gateway', 'dns']]): 139 | configurations.append( 140 | NetworkConfiguration(NicType.MANUAL, iface=iface) 141 | ) 142 | else: 143 | ip = manual_config.get('ip', '') 144 | if not ip: 145 | log(_('Manual nic configuration with no auto DHCP requires an IP address'), fg='red') 146 | exit(1) 147 | 148 | configurations.append( 149 | NetworkConfiguration( 150 | NicType.MANUAL, 151 | iface=iface, 152 | ip=ip, 153 | gateway=manual_config.get('gateway', ''), 154 | dns=manual_config.get('dns', []), 155 | dhcp=False 156 | ) 157 | ) 158 | 159 | return configurations 160 | 161 | def _parse_nic_type(self, nic_type: str) -> NicType: 162 | try: 163 | return NicType(nic_type) 164 | except ValueError: 165 | options = [e.value for e in NicType] 166 | log(_('Unknown nic type: {}. Possible values are {}').format(nic_type, options), fg='red') 167 | exit(1) 168 | 169 | def parse_arguments(self, config: Any): 170 | if isinstance(config, list): # new data format 171 | self._configuration = self._parse_manual_config(config) 172 | elif nic_type := config.get('type', None): # new data format 173 | type_ = self._parse_nic_type(nic_type) 174 | 175 | if type_ != NicType.MANUAL: 176 | self._configuration = NetworkConfiguration(type_) 177 | else: # manual configuration settings 178 | self._configuration = self._parse_manual_config([config]) 179 | else: # old style definitions 180 | network_config = self._backwards_compability_config(config) 181 | if network_config: 182 | return network_config 183 | return None 184 | -------------------------------------------------------------------------------- /archinstall/lib/locale_helpers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Iterator, List, Callable 3 | 4 | from .exceptions import ServiceException 5 | from .general import SysCommand 6 | from .output import log 7 | from .storage import storage 8 | 9 | def list_keyboard_languages() -> Iterator[str]: 10 | for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}): 11 | yield line.decode('UTF-8').strip() 12 | 13 | 14 | def list_locales() -> List[str]: 15 | with open('/etc/locale.gen', 'r') as fp: 16 | locales = [] 17 | # before the list of locales begins there's an empty line with a '#' in front 18 | # so we'll collect the localels from bottom up and halt when we're donw 19 | entries = fp.readlines() 20 | entries.reverse() 21 | 22 | for entry in entries: 23 | text = entry.replace('#', '').strip() 24 | if text == '': 25 | break 26 | locales.append(text) 27 | 28 | locales.reverse() 29 | return locales 30 | 31 | def get_locale_mode_text(mode): 32 | if mode == 'LC_ALL': 33 | mode_text = "general (LC_ALL)" 34 | elif mode == "LC_CTYPE": 35 | mode_text = "Character set" 36 | elif mode == "LC_NUMERIC": 37 | mode_text = "Numeric values" 38 | elif mode == "LC_TIME": 39 | mode_text = "Time Values" 40 | elif mode == "LC_COLLATE": 41 | mode_text = "sort order" 42 | elif mode == "LC_MESSAGES": 43 | mode_text = "text messages" 44 | else: 45 | mode_text = "Unassigned" 46 | return mode_text 47 | 48 | def reset_cmd_locale(): 49 | """ sets the cmd_locale to its saved default """ 50 | storage['CMD_LOCALE'] = storage.get('CMD_LOCALE_DEFAULT',{}) 51 | 52 | def unset_cmd_locale(): 53 | """ archinstall will use the execution environment default """ 54 | storage['CMD_LOCALE'] = {} 55 | 56 | def set_cmd_locale(general :str = None, 57 | charset :str = 'C', 58 | numbers :str = 'C', 59 | time :str = 'C', 60 | collate :str = 'C', 61 | messages :str = 'C'): 62 | """ 63 | Set the cmd locale. 64 | If the parameter general is specified, it takes precedence over the rest (might as well not exist) 65 | The rest define some specific settings above the installed default language. If anyone of this parameters is none means the installation default 66 | """ 67 | installed_locales = list_installed_locales() 68 | result = {} 69 | if general: 70 | if general in installed_locales: 71 | storage['CMD_LOCALE'] = {'LC_ALL':general} 72 | else: 73 | log(f"{get_locale_mode_text('LC_ALL')} {general} is not installed. Defaulting to C",fg="yellow",level=logging.WARNING) 74 | return 75 | 76 | if numbers: 77 | if numbers in installed_locales: 78 | result["LC_NUMERIC"] = numbers 79 | else: 80 | log(f"{get_locale_mode_text('LC_NUMERIC')} {numbers} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) 81 | if charset: 82 | if charset in installed_locales: 83 | result["LC_CTYPE"] = charset 84 | else: 85 | log(f"{get_locale_mode_text('LC_CTYPE')} {charset} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) 86 | if time: 87 | if time in installed_locales: 88 | result["LC_TIME"] = time 89 | else: 90 | log(f"{get_locale_mode_text('LC_TIME')} {time} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) 91 | if collate: 92 | if collate in installed_locales: 93 | result["LC_COLLATE"] = collate 94 | else: 95 | log(f"{get_locale_mode_text('LC_COLLATE')} {collate} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) 96 | if messages: 97 | if messages in installed_locales: 98 | result["LC_MESSAGES"] = messages 99 | else: 100 | log(f"{get_locale_mode_text('LC_MESSAGES')} {messages} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) 101 | storage['CMD_LOCALE'] = result 102 | 103 | def host_locale_environ(func :Callable): 104 | """ decorator when we want a function executing in the host's locale environment """ 105 | def wrapper(*args, **kwargs): 106 | unset_cmd_locale() 107 | result = func(*args,**kwargs) 108 | reset_cmd_locale() 109 | return result 110 | return wrapper 111 | 112 | def c_locale_environ(func :Callable): 113 | """ decorator when we want a function executing in the C locale environment """ 114 | def wrapper(*args, **kwargs): 115 | set_cmd_locale(general='C') 116 | result = func(*args,**kwargs) 117 | reset_cmd_locale() 118 | return result 119 | return wrapper 120 | 121 | def list_installed_locales() -> List[str]: 122 | lista = [] 123 | for line in SysCommand('locale -a'): 124 | lista.append(line.decode('UTF-8').strip()) 125 | return lista 126 | 127 | def list_x11_keyboard_languages() -> Iterator[str]: 128 | for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}): 129 | yield line.decode('UTF-8').strip() 130 | 131 | 132 | def verify_keyboard_layout(layout :str) -> bool: 133 | for language in list_keyboard_languages(): 134 | if layout.lower() == language.lower(): 135 | return True 136 | return False 137 | 138 | 139 | def verify_x11_keyboard_layout(layout :str) -> bool: 140 | for language in list_x11_keyboard_languages(): 141 | if layout.lower() == language.lower(): 142 | return True 143 | return False 144 | 145 | 146 | def search_keyboard_layout(layout :str) -> Iterator[str]: 147 | for language in list_keyboard_languages(): 148 | if layout.lower() in language.lower(): 149 | yield language 150 | 151 | 152 | def set_keyboard_language(locale :str) -> bool: 153 | if len(locale.strip()): 154 | if not verify_keyboard_layout(locale): 155 | log(f"Invalid keyboard locale specified: {locale}", fg="red", level=logging.ERROR) 156 | return False 157 | 158 | if (output := SysCommand(f'localectl set-keymap {locale}')).exit_code != 0: 159 | raise ServiceException(f"Unable to set locale '{locale}' for console: {output}") 160 | 161 | return True 162 | 163 | return False 164 | 165 | 166 | def list_timezones() -> Iterator[str]: 167 | for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}): 168 | yield line.decode('UTF-8').strip() 169 | --------------------------------------------------------------------------------