Compare commits

..

No commits in common. "master" and "v2.4.2" have entirely different histories.

415 changed files with 18032 additions and 109577 deletions

View File

@ -1,8 +1,10 @@
[flake8] [flake8]
count = True count = True
ignore = W191,W503,E704,E203 # Several of the following could be autofixed or improved by running the code through psf/black
ignore = E123,E126,E128,E203,E231,E261,E302,E402,E722,F541,W191,W292,W293
max-complexity = 40 max-complexity = 40
max-line-length = 160 max-line-length = 236
show-source = True show-source = True
statistics = True statistics = True
exclude = .git,__pycache__,build,docs,actions-runner builtins = _
per-file-ignores = __init__.py:F401,F403,F405 simple_menu.py:C901,W503 guided.py:C901 network_configuration.py:F821

4
.github/FUNDING.yml vendored
View File

@ -1,4 +0,0 @@
# These are supported funding model platforms
github: [archlinux]
custom: ['https://archlinux.org/donate/']

View File

@ -1,85 +0,0 @@
name: bug report
description: archinstall crashed or could not install properly?
body:
- type: markdown
attributes:
value: >
Please read the ~5 known issues first:
https://archinstall.archlinux.page/help/known_issues.html
- type: markdown
attributes:
value: >
**NOTE: Always try the latest official ISO**
- type: input
id: iso
attributes:
label: Which ISO version are you using?
description: 'Always use the latest ISO version'
placeholder: '"2024-12-01" or "Dec 1:st"'
validations:
required: true
- type: textarea
id: bug-report
attributes:
label: The installation log
description: 'note: located at `/var/log/archinstall/install.log`'
placeholder: |
Hardware model detected: Dell Inc. Precision 7670; UEFI mode: True
Processor model detected: 12th Gen Intel(R) Core(TM) i7-12850HX
Memory statistics: 31111048 available out of 32545396 total installed
Disk states before installing: {'blockdevices': ... }
Testing connectivity to the Arch Linux mirrors ...
...
render: json
validations:
required: true
- type: markdown
attributes:
value: >
**Note**: Assuming you have network connectivity,
you can easily upload the installation log and get a shareable URL by running:
`archinstall share-log`
- type: textarea
id: freeform
attributes:
label: describe the problem
description: >
Please describe your issue as best as you can.
And please consider personal preferences vs what the recommended
steps/values are in https://wiki.archlinux.org/title/Installation_guide
as we try to abide by them as best we can.
value: |
#### Description of the issue
I was installing on X hardware ...
Then X Y Z happened and archinstall crashed ...
#### Virtual machine config:
```xml
<domain type="kvm">
<name>my-arch-machine</name>
...
</devices>
</domain>
```
```console
/usr/bin/qemu-system-x86_64 -name guest=my-arch-machine,debug-threads=on -object ...
```
validations:
required: true
- type: markdown
attributes:
value: >
**Note**: Feel free to modify the textarea above as you wish.
But it will grately help us in testing if we can generate the specific qemu command line,
for instance via:
`sudo virsh domxml-to-native qemu-argv --domain my-arch-machine`

View File

@ -1,17 +0,0 @@
name: feature request
description: a new feature!
body:
- type: markdown
attributes:
value: >
Please read our short mission statement before requesting more features:
https://github.com/archlinux/archinstall?tab=readme-ov-file#mission-statement
- type: textarea
id: freeform
attributes:
label: describe the request
description: >
Feel free to write any feature you think others might benefit from:
validations:
required: true

View File

@ -1,12 +1,12 @@
on: [ push, pull_request ] on: [ push, pull_request ]
name: Bandit security checkup name: Bandit security checkup
jobs: jobs:
bandit: flake8:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: archlinux/archlinux:latest image: archlinux:latest
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@v2
- run: pacman --noconfirm -Syu bandit - run: pacman --noconfirm -Syu bandit
- name: Security checkup with Bandit - name: Security checkup with Bandit
run: bandit -r archinstall || exit 0 run: bandit -r archinstall || exit 0

View File

@ -1,23 +1,14 @@
on: [ push, pull_request ] on: [ push, pull_request ]
name: flake8 linting name: flake8 linting (15 ignores)
jobs: jobs:
flake8: flake8:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: archlinux/archlinux:latest image: archlinux:latest
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@v2
- name: Prepare arch - run: pacman --noconfirm -Syu python python-pip
run: | - run: python -m pip install --upgrade pip
pacman-key --init - run: pip install flake8
pacman --noconfirm -Sy archlinux-keyring
pacman --noconfirm -Syyu
pacman --noconfirm -Sy python-pip python-pyparted pkgconfig gcc
- run: pip install --break-system-packages --upgrade pip
# this will install the exact version of flake8 that is in the pyproject.toml file
- name: Install archinstall dependencies
run: pip install --break-system-packages .[dev]
- run: python --version
- run: flake8 --version
- name: Lint with flake8 - name: Lint with flake8
run: flake8 run: flake8

View File

@ -1,41 +0,0 @@
name: documentation
on:
push:
paths:
- "docs/**"
pull_request:
paths:
- "docs/**"
workflow_dispatch:
permissions:
contents: write
jobs:
docs:
runs-on: ubuntu-latest
container:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
- name: Install pre-dependencies
run: |
pacman -Sy --noconfirm tree git python-pyparted python-setuptools python-sphinx python-sphinx_rtd_theme python-build python-installer python-wheel
- name: Sphinx build
run: |
sphinx-build docs _build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4
if: ${{ github.event_name != 'pull_request' }}
with:
publish_branch: gh-pages
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: _build/
force_orphan: true
enable_jekyll: false # This is required to preserve _static (and thus the theme)
cname: archinstall.archlinux.page

View File

@ -23,17 +23,27 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: archlinux/archlinux:latest image: archlinux:latest
options: --privileged options: --privileged
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@v2
- run: pwd - run: pwd
- run: find . - run: find .
- run: cat /etc/os-release - run: cat /etc/os-release
- run: pacman-key --init - run: pacman-key --init
- run: pacman --noconfirm -Sy archlinux-keyring - run: pacman --noconfirm -Sy archlinux-keyring
- run: ./test_tooling/mkarchiso/build_iso.sh - run: mkdir -p /tmp/archlive/airootfs/root/archinstall-git; cp -r . /tmp/archlive/airootfs/root/archinstall-git
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - run: echo "pip uninstall archinstall -y; cd archinstall-git; rm -rf dist; python -m build -n; pip install dist/archinstall*.whl" > /tmp/archlive/airootfs/root/.zprofile
- run: echo "echo \"This is an unofficial ISO for development and testing of archinstall. No support will be provided.\"" >> /tmp/archlive/airootfs/root/.zprofile
- run: echo "echo \"This ISO was built from Git SHA $GITHUB_SHA\"" >> /tmp/archlive/airootfs/root/.zprofile
- run: echo "echo \"Type archinstall to launch the installer.\"" >> /tmp/archlive/airootfs/root/.zprofile
- run: cat /tmp/archlive/airootfs/root/.zprofile
- run: pacman -Sy; pacman --noconfirm -S git archiso
- run: cp -r /usr/share/archiso/configs/releng/* /tmp/archlive
- run: echo -e "git\npython\npython-pip\npython-build\npython-flit\npython-setuptools\npython-wheel" >> /tmp/archlive/packages.x86_64
- run: find /tmp/archlive
- run: cd /tmp/archlive; mkarchiso -v -w work/ -o out/ ./
- uses: actions/upload-artifact@v2
with: with:
name: Arch Live ISO name: Arch Live ISO
path: /tmp/archlive/out/*.iso path: /tmp/archlive/out/*.iso

View File

@ -4,20 +4,15 @@ jobs:
mypy: mypy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: archlinux/archlinux:latest image: archlinux:latest
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@v2
- name: Prepare arch - run: pacman --noconfirm -Syu python mypy python-pip
run: | - run: python -m pip install --upgrade pip
pacman-key --init - run: pip install fastapi pydantic
pacman --noconfirm -Sy archlinux-keyring
pacman --noconfirm -Syyu
pacman --noconfirm -Sy python-pip python-pyparted pkgconfig gcc
- run: pip install --break-system-packages --upgrade pip
# this will install the exact version of mypy that is in the pyproject.toml file
- name: Install archinstall dependencies
run: pip install --break-system-packages .[dev]
- run: python --version - run: python --version
- run: mypy --version - run: mypy --version
# one day this will be enabled
# run: mypy --strict --module archinstall || exit 0
- name: run mypy - name: run mypy
run: mypy --config-file pyproject.toml run: mypy --follow-imports=skip archinstall/lib/menu/selection_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py

View File

@ -1,22 +0,0 @@
on: [ push, pull_request ]
name: Pylint linting
jobs:
pylint:
runs-on: ubuntu-latest
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init
pacman --noconfirm -Sy archlinux-keyring
pacman --noconfirm -Syyu
pacman --noconfirm -Sy python-pip python-pyparted pkgconfig gcc
- run: pip install --break-system-packages --upgrade pip
- name: Install Pylint
run: pip install --break-system-packages .[dev]
- run: python --version
- run: pylint --version
- name: Lint with Pylint
run: pylint .

View File

@ -4,18 +4,12 @@ jobs:
pytest: pytest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: archlinux/archlinux:latest image: archlinux:latest
options: --privileged options: --privileged
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@v2
- name: Prepare arch - run: pacman --noconfirm -Syu python python-pip qemu gcc
run: | - run: python -m pip install --upgrade pip
pacman-key --init - run: pip install pytest
pacman --noconfirm -Sy archlinux-keyring
pacman --noconfirm -Syyu
pacman --noconfirm -Sy python-pip python-pyparted pkgconfig gcc
- run: pip install --break-system-packages --upgrade pip
- name: Install archinstall dependencies
run: pip install --break-system-packages .[dev]
- name: Test with pytest - name: Test with pytest
run: pytest run: python -m pytest || exit 0

View File

@ -7,33 +7,20 @@ on: [ push, pull_request ]
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container:
image: archlinux/archlinux:latest
options: --privileged
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@v2
- name: Prepare arch - name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: | run: |
pacman-key --init python -m pip install --upgrade pip
pacman --noconfirm -Sy archlinux-keyring pip install build twine
pacman --noconfirm -Syyu
pacman --noconfirm -Sy python-uv python-setuptools python-pip
pacman --noconfirm -Sy python-pyparted python-pydantic python-textual
- name: Remove existing archinstall (if any)
run:
uv pip uninstall archinstall --break-system-packages --system
- name: Build archinstall - name: Build archinstall
run: uv build --no-build-isolation --wheel
- name: Install archinstall
run: | run: |
uv pip install dist/*.whl --break-system-packages --system --no-build --no-deps python -m build
- name: Run archinstall - uses: actions/upload-artifact@v2
run: |
python -V
archinstall --script guided -v
archinstall --script only_hd -v
archinstall --script minimal -v
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: with:
name: archinstall name: archinstall
path: dist/* path: dist/*

View File

@ -1,4 +1,4 @@
# This workflow will upload a Python Package when a release is created # This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Upload archinstall to PyPi name: Upload archinstall to PyPi
@ -11,23 +11,23 @@ jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
# IMPORTANT: this permission is mandatory for Trusted Publishing
id-token: write
container:
image: archlinux/archlinux:latest
options: --privileged
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@v2
- name: Prepare arch - name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: | run: |
pacman-key --init python -m pip install --upgrade pip
pacman --noconfirm -Sy archlinux-keyring pip install build twine
pacman --noconfirm -Syyu
pacman --noconfirm -Sy python python-uv python-setuptools python-pip python-pyparted python-pydantic python-textual
- name: Build archinstall - name: Build archinstall
run: | run: |
uv build --no-build-isolation --wheel python -m build
- name: Publish archinstall to PyPi - name: Publish archinstall to PyPi
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: | run: |
uv publish --trusted-publishing always twine upload dist/*

View File

@ -1,9 +0,0 @@
on: [ push, pull_request ]
name: ruff check formatting
jobs:
ruff_format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
- run: ruff format --diff

View File

@ -1,8 +0,0 @@
on: [ push, pull_request ]
name: ruff check linting
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0

View File

@ -1,22 +0,0 @@
name: Translation validation
on:
push:
paths:
- 'archinstall/**/*.py'
- 'archinstall/locales/**'
- '.github/workflows/translation-check.yaml'
pull_request:
paths:
- 'archinstall/**/*.py'
- 'archinstall/locales/**'
- '.github/workflows/translation-check.yaml'
jobs:
translations:
name: Validate translations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Install gettext
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Run translation checks
run: bash archinstall/locales/locales_generator.sh check

View File

@ -1,40 +0,0 @@
# This workflow will build an Arch Linux UKI file with the commit on it
name: Build Arch UKI with ArchInstall Commit
on:
push:
branches:
- master
- main # In case we adopt this convention in the future
pull_request:
paths-ignore:
- 'docs/**'
- '**.editorconfig'
- '**.gitignore'
- '**.md'
- 'LICENSE'
- 'PKGBUILD'
release:
types:
- created
jobs:
build:
runs-on: ubuntu-latest
container:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- run: pwd
- run: find .
- run: cat /etc/os-release
- run: pacman-key --init
- run: pacman --noconfirm -Sy archlinux-keyring
- run: pacman --noconfirm -Sy mkosi
- run: (cd test_tooling/mkosi/ && mkosi build -B)
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: Arch Live UKI
path: test_tooling/mkosi/mkosi.output/*.efi

16
.gitignore vendored
View File

@ -21,11 +21,10 @@ SAFETY_LOCK
**/**.network **/**.network
**/**.target **/**.target
**/**.qcow2 **/**.qcow2
**/**.log
**/**.fd
/test*.py /test*.py
**/archiso **/archiso
/guided.py /guided.py
/install.log
venv venv
.venv .venv
.idea/** .idea/**
@ -33,15 +32,4 @@ venv
.DS_Store .DS_Store
**/cmd_history.txt **/cmd_history.txt
**/*.*~ **/*.*~
/*.sig /*.sig
/*.json
requirements.txt
/.gitconfig
/actions-runner
/cmd_output.txt
node_modules/
uv.lock
test_tooling/mkosi/mkosi.output/*image*
test_tooling/mkosi/mkosi.cache/**
test_tooling/mkosi/mkosi.tools/**
test_tooling/mkosi/mkosi.tools.manifest

View File

@ -4,7 +4,7 @@
# These jobs should leverage the same tag as that runner. If necessary, change the tag from 'docker' to the one it uses. # These jobs should leverage the same tag as that runner. If necessary, change the tag from 'docker' to the one it uses.
# All jobs will be run in the official archlinux container image, so we will declare that here. # All jobs will be run in the official archlinux container image, so we will declare that here.
image: archlinux/archlinux:latest image: archlinux:latest
# This can be used to handle common actions. In this case, we do a pacman -Sy to make sure repos are ready to use. # This can be used to handle common actions. In this case, we do a pacman -Sy to make sure repos are ready to use.
before_script: before_script:
@ -36,7 +36,7 @@ flake8:
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
# We currently do not have unit tests implemented but this stage is written in anticipation of their future usage. # We currently do not have unit tests implemented but this stage is written in anticipation of their future usage.
# When a stage name is preceded with a '.' it's treated as "disabled" by GitLab and is not executed, so it's fine for it to be declared. # 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.
.pytest: .pytest:
stage: test stage: test
tags: tags:

View File

@ -1,55 +0,0 @@
default_stages: ['pre-commit']
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.0
hooks:
# fix unused imports and sort them
- id: ruff
args: ["--extend-select", "I", "--fix"]
# format the code
- id: ruff-format
# run the linter
- id: ruff
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
# general hooks:
- id: check-added-large-files # Prevent giant files from being committed
args: ['--maxkb=5000']
- id: check-merge-conflict # Check for files that contain merge conflict strings
- id: check-symlinks # Checks for symlinks which do not point to anything
- id: check-yaml # Attempts to load all yaml files to verify syntax
- id: destroyed-symlinks # Detects symlinks which are changed to regular files
- id: detect-private-key # Checks for the existence of private keys
# Python specific hooks:
- id: check-ast # Simply check whether files parse as valid python
- id: check-docstring-first # Checks for a common error of placing code before the docstring
- repo: https://github.com/pycqa/flake8
rev: 7.3.0
hooks:
- id: flake8
args: [--config=.flake8]
fail_fast: true
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.3.0
hooks:
- id: mypy
args: [
'--config-file=pyproject.toml'
]
fail_fast: true
additional_dependencies:
- pydantic
- pytest
- hypothesis
- cryptography
- textual
- repo: local
hooks:
- id: pylint
name: pylint
entry: pylint
language: system
types: [python]
fail_fast: true
require_serial: true

View File

@ -3,4 +3,4 @@ index-servers =
pypi pypi
[pypi] [pypi]
repository = https://upload.pypi.org/legacy/ repository = https://upload.pypi.org/legacy/

View File

@ -1,15 +0,0 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
sphinx:
builder: html
configuration: docs/conf.py
fail_on_warning: true
build:
os: "ubuntu-22.04"
tools:
python: "3.12"

22
.readthedocs.yml Normal file
View File

@ -0,0 +1,22 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Build documentation with MkDocs
#mkdocs:
# configuration: mkdocs.yml
# Optionally build your docs in additional formats such as PDF
formats:
- pdf
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.8

View File

@ -16,7 +16,7 @@ Patch releases will be done against their own branches, branched from stable tag
## Discussions ## Discussions
Currently, questions, bugs and suggestions should be reported through [GitHub issue tracker](https://github.com/archlinux/archinstall/issues).<br> Currently, questions, bugs and suggestions should be reported through [GitHub issue tracker](https://github.com/archlinux/archinstall/issues).<br>
For less formal discussions there is also an [archinstall Discord server](https://discord.gg/aDeMffrxNg). For less formal discussions there is also an [archinstall Discord server](https://discord.gg/cqXU88y).
## Coding convention ## Coding convention
@ -27,7 +27,10 @@ The exceptions to PEP8 are:
* Archinstall uses [tabs instead of spaces](https://www.python.org/dev/peps/pep-0008/#tabs-or-spaces) simply to make it * Archinstall uses [tabs instead of spaces](https://www.python.org/dev/peps/pep-0008/#tabs-or-spaces) simply to make it
easier for non-IDE developers to navigate the code *(Tab display-width should be equal to 4 spaces)*. Exception to the easier for non-IDE developers to navigate the code *(Tab display-width should be equal to 4 spaces)*. Exception to the
rule are comments that need fine-tuned indentation for documentation purposes. rule are comments that need fine-tuned indentation for documentation purposes.
* [Line length](https://www.python.org/dev/peps/pep-0008/#maximum-line-length) a maximum line length is enforced via flake8 with 160 characters * [Line length](https://www.python.org/dev/peps/pep-0008/#maximum-line-length) should aim for no more than 100
characters, but not strictly enforced.
* [Line breaks before/after binary operator](https://www.python.org/dev/peps/pep-0008/#should-a-line-break-before-or-after-a-binary-operator)
is not enforced, as long as the style of line breaks is consistent within the same code block.
* Archinstall should always be saved with **Unix-formatted line endings** and no other platform-specific formats. * Archinstall should always be saved with **Unix-formatted line endings** and no other platform-specific formats.
* [String quotes](https://www.python.org/dev/peps/pep-0008/#string-quotes) follow PEP8, the exception being when * [String quotes](https://www.python.org/dev/peps/pep-0008/#string-quotes) follow PEP8, the exception being when
creating formatted strings, double-quoted strings are *preferred* but not required on the outer edges *( creating formatted strings, double-quoted strings are *preferred* but not required on the outer edges *(
@ -36,16 +39,6 @@ The exceptions to PEP8 are:
Most of these style guidelines have been put into place after the fact *(in an attempt to clean up the code)*.<br> Most of these style guidelines have been put into place after the fact *(in an attempt to clean up the code)*.<br>
There might therefore be older code which does not follow the coding convention and the code is subject to change. There might therefore be older code which does not follow the coding convention and the code is subject to change.
## Git hooks
`archinstall` ships pre-commit hooks that make it easier to run checks such as `mypy`, `ruff check`, and `flake8` locally.
The checks are listed in `.pre-commit-config.yaml` and can be installed via
```
pre-commit install
```
This will install the pre-commit hook and run it every time a `git commit` is executed.
## Documentation ## Documentation
If you'd like to contribute to the documentation, refer to [this guide](docs/README.md) on how to build the documentation locally. If you'd like to contribute to the documentation, refer to [this guide](docs/README.md) on how to build the documentation locally.
@ -65,7 +58,16 @@ documentation or to the implementation of the code you are changing.
Also, do not squash your commits after you have submitted a pull request, as this erases context during review. We will Also, do not squash your commits after you have submitted a pull request, as this erases context during review. We will
squash commits when the pull request is merged. squash commits when the pull request is merged.
Maintainer: At present the current contributors are (alphabetically):
* Anton Hvornum ([@Torxed](https://github.com/Torxed))
[Contributors](https://github.com/archlinux/archinstall/graphs/contributors) * Anton Hvornum ([@Torxed](https://github.com/Torxed))
* Borislav Kosharov ([@nikibobi](https://github.com/nikibobi))
* demostanis ([@demostanis](https://github.com/demostanis))
* Dylan Taylor ([@dylanmtaylor](https://github.com/dylanmtaylor))
* Giancarlo Razzolini (@[grazzolini](https://github.com/grazzolini))
* j-james ([@j-james](https://github.com/j-james))
* Jerker Bengtsson ([@jaybent](https://github.com/jaybent))
* Ninchester ([@ninchester](https://github.com/ninchester))
* Philipp Schaffrath ([@phisch](https://github.com/phisch))
* Varun Madiath ([@vamega](https://github.com/vamega))
* nullrequest ([@advaithm](https://github.com/advaithm))

View File

@ -1,89 +1,47 @@
# Maintainer: David Runge <dvzrv@archlinux.org> # Maintainer: David Runge <dvzrv@archlinux.org>
# Maintainer: Giancarlo Razzolini <grazzolini@archlinux.org> # Maintainer: Giancarlo Razzolini <grazzolini@archlinux.org>
# Maintainer: Anton Hvornum <torxed@archlinux.org>
# Contributor: Anton Hvornum <anton@hvornum.se> # Contributor: Anton Hvornum <anton@hvornum.se>
# Contributor: demostanis worlds <demostanis@protonmail.com> # Contributor: demostanis worlds <demostanis@protonmail.com>
pkgname=archinstall pkgname=archinstall
pkgver=4.4 pkgver=2.4.2
#pkgver=$(git describe --long | sed 's/\([^-]*-g\)/r\1/;s/-/./g')
pkgrel=1 pkgrel=1
pkgdesc="Just another guided/automated Arch Linux installer with a twist" pkgdesc="Just another guided/automated Arch Linux installer with a twist"
arch=(any) arch=(any)
url="https://github.com/archlinux/archinstall" url="https://github.com/archlinux/archinstall"
license=(GPL-3.0-only) license=(GPL3)
depends=( depends=(python)
'arch-install-scripts' makedepends=(python-build python-installer python-flit python-setuptools python-sphinx python-wheel)
'btrfs-progs' provides=(python-archinstall)
'coreutils' conflicts=(python-archinstall)
'cryptsetup' replaces=(python-archinstall)
'dosfstools'
'e2fsprogs'
'glibc'
'kbd'
'libcrypt.so'
'libxcrypt'
'pciutils'
'procps-ng'
'python'
'python-cryptography'
'python-pydantic'
'python-pyparted'
'python-textual'
'python-markdown-it-py'
'python-linkify-it-py'
'systemd'
'util-linux'
'xfsprogs'
'lvm2'
'f2fs-tools'
'libfido2'
)
makedepends=(
'python-build'
'python-installer'
'python-setuptools'
'python-sphinx'
'python-wheel'
'python-sphinx_rtd_theme'
'python-pylint'
'python-pylint-pydantic'
'ruff'
)
optdepends=(
'python-systemd: Adds journald logging'
)
provides=(python-archinstall archinstall)
conflicts=(python-archinstall archinstall-git)
replaces=(python-archinstall archinstall-git)
source=( source=(
$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/$pkgver.tar.gz $pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz
$pkgname-$pkgver.tar.gz.sig::$url/releases/download/$pkgver/$pkgname-$pkgver.tar.gz.sig $pkgname-$pkgver.tar.gz.sig::$url/releases/download/v$pkgver/$pkgname-$pkgver.tar.gz.sig
) )
sha512sums=() sha512sums=('9516719c4e4fe0423224a35b4846cf5c8daeb931cff6fed588957840edc5774e9c6fe18619d2356a6d76681ae3216ba19f5d0f0bd89c6301b4ff9b128d037d13'
b2sums=() 'SKIP')
validpgpkeys=('8AA2213C8464C82D879C8127D4B58E897A929F2E') # torxed@archlinux.org b2sums=('a29ae767756f74ce296d53e31bb8376cfa7db19a53b8c3997b2d8747a60842ba88e8b18c505bc56a36d685f73f7a6d9e53adff17953c8a4ebaabc67c6db8e583'
'SKIP')
validpgpkeys=('256F73CEEFC6705C6BBAB20E5FBBB32941E3740A') # Anton Hvornum (Torxed) <anton@hvornum.se>
check() { prepare() {
cd $pkgname-$pkgver cd $pkgname-$pkgver
ruff check # use real directories for examples and profiles, as symlinks do not work
} # with flit or setuptools PEP517 backends
rm -fv $pkgname/{examples,profiles}
pkgver() { mv -v examples profiles $pkgname/
cd $pkgname-$pkgver
awk '$1 ~ /^__version__/ {gsub("\"", ""); print $3}' archinstall/__init__.py
} }
build() { build() {
cd $pkgname-$pkgver cd $pkgname-$pkgver
python -m build --wheel --no-isolation python -m build --wheel --no-isolation
PYTHONDONTWRITEBYTECODE=1 make man -C docs PYTHONDONTWRITEBYTECODE=1 make man -C docs
} }
package() { package() {
cd "$pkgname-$pkgver" cd "$pkgname-$pkgver"
python -m installer --destdir="$pkgdir" dist/*.whl python -m installer --destdir="$pkgdir" dist/*.whl
install -vDm 644 docs/_build/man/archinstall.1 -t "$pkgdir/usr/share/man/man1/" install -vDm 644 docs/_build/man/archinstall.1 -t "$pkgdir/usr/share/man/man1/"
} }

293
README.md
View File

@ -6,169 +6,143 @@
[![Lint Python and Find Syntax Errors](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml/badge.svg)](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml) [![Lint Python and Find Syntax Errors](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml/badge.svg)](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml)
Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist. Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist.
The installer also doubles as a python library to install Arch Linux and manage services, packages, and other things inside the installed system *(Usually from a live medium or from an existing installation)*. The installer also doubles as a python library to install Arch Linux and manage services, packages and other things inside the installed system *(Usually from a live medium)*.
* archinstall [discord](https://discord.gg/aDeMffrxNg) server * archinstall [discord](https://discord.gg/cqXU88y) server
* archinstall [#archinstall:matrix.org](https://matrix.to/#/#archinstall:matrix.org) Matrix channel * archinstall [matrix.org](https://app.element.io/#/room/#archinstall:matrix.org) channel
* archinstall [#archinstall@irc.libera.chat:6697](https://web.libera.chat/?channel=#archinstall) * archinstall [#archinstall@irc.libera.chat](irc://#archinstall@irc.libera.chat:6697)
* archinstall [documentation](https://archinstall.archlinux.page/) * archinstall [documentation](https://archinstall.readthedocs.io/)
# Installation & Usage # Installation & Usage
> [!TIP]
> In the ISO you are root by default. Use sudo if running from an existing system.
```shell $ sudo pacman -S archinstall
pacman-key --init
pacman -Sy archinstall
archinstall
```
Alternative ways to install are `git clone` the repository (and is better since you get the latest code regardless of [build date](https://archlinux.org/packages/?sort=&q=archinstall)) or `pip install --upgrade archinstall`. Or simply `git clone` the repo as it has no external dependencies *(but there are optional ones)*.<br>
Or use `pip install --upgrade archinstall` to use as a library.
## Upgrade `archinstall` on live Arch ISO image ## Running the [guided](https://github.com/archlinux/archinstall/blob/master/examples/guided.py) installer
Upgrading archinstall on the ISO needs to be done via a full system upgrade using Assuming you are on an Arch Linux live-ISO:
```shell # archinstall
pacman -Syu
```
When booting from a live USB, the space on the ramdisk is limited and may not be sufficient to allow running a re-installation or upgrade of the installer. Some additional options that are not needed by most users are hidden behind the `--advanced` flag.
In case one runs into this issue, any of the following can be used
* Resize the root partition https://wiki.archlinux.org/title/Archiso#Adjusting_the_size_of_the_root_file_system
* Specify the boot parameter copytoram=y (https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio-archiso/-/blob/master/docs/README.bootparams#L26) which will copy the root filesystem to tmpfs
## Running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer
Assuming you are on an Arch Linux live-ISO or installed via `pip`, `archinstall` will use the `guided` script by default
```shell
archinstall
```
similar goes for running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer using `git
```shell
git clone https://github.com/archlinux/archinstall
cd archinstall
python -m archinstall $@
```
To run alternative scripts using the `--script` parameter
```
archinstall --script <name>
```
#### Advanced
Some additional options that most users do not need are hidden behind the `--advanced` flag and all options/args can be consulted through `-h` or `--help`.
## Running from a declarative configuration file or URL ## Running from a declarative configuration file or URL
`archinstall` can be run with a JSON configuration file. There are 2 different configuration files to consider, Prequisites:
the `user_configuration.json` contains all general installation configuration, whereas the `user_credentials.json` 1. Edit the [configuration file](https://github.com/archlinux/archinstall/blob/master/examples/config-sample.json) according to your requirements.
contains the sensitive user configuration such as user password, root password, and encryption password.
An example of the user configuration file can be found here Assuming you are on a Arch Linux live-ISO and booted into EFI mode.
[configuration file](https://github.com/archlinux/archinstall/blob/master/examples/config-sample.json)
and an example of the credentials configuration here
[credentials file](https://github.com/archlinux/archinstall/blob/master/examples/creds-sample.json).
**HINT:** The configuration files can be auto-generated by starting `archinstall`, configuring all desired menu # archinstall --config <path to config file or URL>
points and then going to `Save configuration`.
To load the configuration file into `archinstall` run the following command # Help?
```shell
archinstall --config <path to user config file or URL> --creds <path to user credentials config file or URL>
```
### Credentials configuration file encryption Submit an issue here on GitHub, or submit a post in the discord help channel.<br>
By default, all user account credentials are hashed with `yescrypt` and only the hash is stored in the saved `user_credentials.json` file. When doing so, attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
This is not possible for disk encryption password which needs to be stored in plaintext to be able to apply it.
However, when selecting to save configuration files, `archinstall` will prompt for the option to encrypt the `user_credentials.json` file content. # Mission Statement
A prompt will require to enter a encryption password to encrypt the file. When providing an encrypted `user_configuration.json` as a argument with `--creds <user_credentials.json>`
there are multiple ways to provide the decryption key:
* Provide the decryption key via the command line argument `--creds-decryption-key <password>`
* Store the encryption key in the environment variable `ARCHINSTALL_CREDS_DECRYPTION_KEY` which will be read automatically
* If none of the above is provided a prompt will be shown to enter the decryption key manually
Archinstall promises to ship a [guided installer](https://github.com/archlinux/archinstall/blob/master/examples/guided.py) that follows
the [Arch Principles](https://wiki.archlinux.org/index.php/Arch_Linux#Principles) as well as a library to manage services, packages and other Arch Linux aspects.
# Help or Issues The guided installer will provide user-friendly options along the way, but the keyword here is options, they are optional and will never be forced upon anyone.
The guided installer itself is also optional to use if so desired and not forced upon anyone.
If you come across any issues, kindly submit your issue here on GitHub or post your query in the ---
[discord](https://discord.gg/aDeMffrxNg) help channel.
When submitting an issue, please: Archinstall has one fundamental function which is to be a flexible library to manage services, packages and other aspects inside the installed system.
* Provide the stacktrace of the output if applicable This library is in turn used by the provided guided installer but is also for anyone who wants to script their own installations.
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
* To upload the log from the ISO image and get a shareable URL, run<br>
```shell
archinstall share-log
```
# Available Languages
Archinstall is available in different languages which have been contributed and are maintained by the community.
The language can be switched inside the installer (first menu entry). Bear in mind that not all languages provide
full translations as we rely on contributors to do the translations. Each language has an indicator that shows
how much has been translated.
Any contributions to the translations are more than welcome,
to get started please follow [the guide](https://github.com/archlinux/archinstall/blob/master/archinstall/locales/README.md)
## Fonts
The ISO does not ship with all fonts needed for different languages.
Fonts that use a different character set than Latin will not be displayed correctly. If those languages
want to be selected then a proper font has to be set manually in the console.
All available console fonts can be found in `/usr/share/kbd/consolefonts` and set with `setfont LatGrkCyr-8x16`.
Therefore, Archinstall will try its best to not introduce any breaking changes except for major releases which may break backwards compability after notifying about such changes.
# Scripting your own installation # Scripting your own installation
## Scripting interactive installation You could just copy [guided.py](https://github.com/archlinux/archinstall/blob/master/examples/guided.py) as a starting point.
For an example of a fully scripted, interactive installation please refer to the example However, assuming you're building your own ISO and want to create an automated installation process, or you want to install virtual machines onto local disk images, here is a [minimal example](https://github.com/archlinux/archinstall/blob/master/examples/minimal.py) of how to install using archinstall as a Python library:<br>
[interactive_installation.py](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py)
```python
import archinstall, getpass
> **To create your own ISO with this script in it:** Follow [ArchISO](https://wiki.archlinux.org/index.php/archiso)'s guide on creating your own ISO. # Select a harddrive and a disk password
harddrive = archinstall.select_disk(archinstall.all_blockdevices(partitions=False))
disk_password = getpass.getpass(prompt='Disk password (won\'t echo): ')
## Script non-interactive automated installation # We disable safety precautions in the library that protects the partitions
harddrive.keep_partitions = False
For an example of a fully scripted, automated installation please refer to the example # First, we configure the basic filesystem layout
[full_automated_installation.py](https://github.com/archlinux/archinstall/blob/master/examples/full_automated_installation.py) with archinstall.Filesystem(harddrive, archinstall.GPT) as fs:
# We create a filesystem layout that will use the entire drive
# (this is a helper function, you can partition manually as well)
fs.use_entire_disk(root_filesystem_type='btrfs')
# Profiles boot = fs.find_partition('/boot')
root = fs.find_partition('/')
`archinstall` comes with a set of pre-configured profiles available for selection during the installation process. boot.format('vfat')
- [Desktop](https://github.com/archlinux/archinstall/tree/master/archinstall/default_profiles/desktops) # Set the flag for encrypted to allow for encryption and then encrypt
- [Server](https://github.com/archlinux/archinstall/tree/master/archinstall/default_profiles/servers) root.encrypted = True
root.encrypt(password=disk_password)
The profiles' definitions and the packages they will install can be directly viewed in the menu, or with archinstall.luks2(root, 'luksloop', disk_password) as unlocked_root:
[default profiles](https://github.com/archlinux/archinstall/tree/master/archinstall/default_profiles) unlocked_root.format(root.filesystem)
unlocked_root.mount('/mnt')
boot.mount('/mnt/boot')
with archinstall.Installer('/mnt') as installation:
if installation.minimal_installation():
installation.set_hostname('minimal-arch')
installation.add_bootloader()
installation.add_additional_packages(['nano', 'wget', 'git'])
# Optionally, install a profile of choice.
# In this case, we install a minimal profile that is empty
installation.install_profile('minimal')
installation.user_create('devel', 'devel')
installation.user_set_pw('root', 'airoot')
```
This installer will perform the following:
* Prompt the user to select a disk and disk-password
* Proceed to wipe the selected disk with a `GPT` partition table on a UEFI system and MBR on a BIOS system.
* Sets up a default 100% used disk with encryption.
* Installs a basic instance of Arch Linux *(base base-devel linux linux-firmware btrfs-progs efibootmgr)*
* Installs and configures a bootloader to partition 0 on uefi. On BIOS, it sets the root to partition 0.
* Install additional packages *(nano, wget, git)*
> **Creating your own ISO with this script on it:** Follow [ArchISO](https://wiki.archlinux.org/index.php/archiso)'s guide on how to create your own ISO or use a pre-built [guided ISO](https://hvornum.se/archiso/) to skip the python installation step, or to create auto-installing ISO templates. Further down are examples and cheat sheets on how to create different live ISO's.
## Unattended installation based on MAC address
Archinstall comes with an [unattended](https://github.com/archlinux/archinstall/blob/master/examples/unattended.py) example which will look for a matching profile for the machine it is being run on, based on any local MAC address.
For instance, if the machine that [unattended](https://github.com/archlinux/archinstall/blob/master/examples/unattended.py) is run on has the MAC address `52:54:00:12:34:56` it will look for a profile called [profiles/52-54-00-12-34-56.py](https://github.com/archlinux/archinstall/blob/master/profiles/52-54-00-12-34-56.py).
If it's found, the unattended installation will commence and source that profile as its installation procedure.
# Testing # Testing
## Using a Live ISO Image ## Using a Live ISO Image
If you want to test a commit, branch, or bleeding edge release from the repository using the standard Arch Linux Live ISO image, If you want to test a commit, branch or bleeding edge release from the repository using the vanilla Arch Live ISO image,
replace the archinstall version with a newer one and execute the subsequent steps defined below. you can replace the version of archinstall with a new version and run that with the steps described below:
1. You need a working network connection 1. You need a working network connection
2. Install the build requirements with `pacman -Sy; pacman -S git python-pip gcc pkgconf` 2. Install the build requirements with `pacman -Sy; pacman -S git python-pip`
*(note that this may or may not work depending on your RAM and current state of the squashfs maximum filesystem free space)* *(note that this may or may not work depending on your RAM and current state of the squashfs maximum filesystem free space)*
3. Uninstall the previous version of archinstall with `pip uninstall --break-system-packages archinstall` 3. Uninstall the previous version of archinstall with `pip uninstall archinstall`
4. Now clone the latest repository with `git clone https://github.com/archlinux/archinstall` 4. Now clone the latest repository with `git clone https://github.com/archlinux/archinstall`
5. Enter the repository with `cd archinstall` 5. Enter the repository with `cd archinstall`
*At this stage, you can choose to check out a feature branch for instance with `git checkout v2.3.1-rc1`* *At this stage, you can choose to check out a feature branch for instance with `git checkout v2.3.1-rc1`*
6. To run the source code, there are 2 different options: 6. Build the project and install it using `python setup.py install`
- Run a specific branch version from source directly using `python -m archinstall`, in most cases this will work just fine, the
rare case it will not work is if the source has introduced any new dependencies that are not installed yet After this, running archinstall with `python -m archinstall` will run against whatever branch you chose in step 5.
- Installing the branch version with `pip install --break-system-packages .` and `archinstall`
## Without a Live ISO Image ## Without a Live ISO Image
@ -176,10 +150,11 @@ To test this without a live ISO, the simplest approach is to use a local image a
This can be done by installing `pacman -S arch-install-scripts util-linux` locally and doing the following: This can be done by installing `pacman -S arch-install-scripts util-linux` locally and doing the following:
# truncate -s 20G testimage.img # truncate -s 20G testimage.img
# losetup --partscan --show ./testimage.img # losetup -fP ./testimage.img
# losetup -a | grep "testimage.img" | awk -F ":" '{print $1}'
# pip install --upgrade archinstall # pip install --upgrade archinstall
# python -m archinstall --script guided # python -m archinstall --script guided
# qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd # qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF_CODE.fd -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF_VARS.fd
This will create a *20 GB* `testimage.img` and create a loop device which we can use to format and install to.<br> This will create a *20 GB* `testimage.img` and create a loop device which we can use to format and install to.<br>
`archinstall` is installed and executed in [guided mode](#docs-todo). Once the installation is complete, ~~you can use qemu/kvm to boot the test media.~~<br> `archinstall` is installed and executed in [guided mode](#docs-todo). Once the installation is complete, ~~you can use qemu/kvm to boot the test media.~~<br>
@ -187,83 +162,3 @@ This will create a *20 GB* `testimage.img` and create a loop device which we can
There's also a [Building and Testing](https://github.com/archlinux/archinstall/wiki/Building-and-Testing) guide.<br> There's also a [Building and Testing](https://github.com/archlinux/archinstall/wiki/Building-and-Testing) guide.<br>
It will go through everything from packaging, building and running *(with qemu)* the installer against a dev branch. It will go through everything from packaging, building and running *(with qemu)* the installer against a dev branch.
## Boot an Arch ISO image in a VM
You may want to boot an ISO image in a VM to test `archinstall` in there.
* Download the latest [Arch ISO](https://archlinux.org/download/)
* Use the the below command to boot the ISO in a VM
```
qemu-system-x86_64 -enable-kvm \
-machine q35,accel=kvm -device intel-iommu \
-cpu host -m 4096 -boot order=d \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd \
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw
```
HINT: For espeakup support
```
qemu-system-x86_64 -enable-kvm \
-machine q35,accel=kvm -device intel-iommu \
-cpu host -m 4096 -boot order=d \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd \
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw \
-device intel-hda -device hda-duplex,audiodev=snd0 \
-audiodev pa,id=snd0,server=/run/user/1000/pulse/native
```
# FAQ
## AUR
`archinstall` will not offer or bundle AUR helpers or AUR packages due to a current consensus. This is not any individual developers decision. The reasons and discussions for this stance on the topic can be found on our mailing list thread: [(optional) AUR helper in archinstall](https://lists.archlinux.org/archives/list/arch-dev-public@lists.archlinux.org/thread/VYOULH2GOJLFM2BXOFLWH3D754YXFPSL/).
## Keyring out-of-date
For a description of the problem see https://archinstall.archlinux.page/help/known_issues.html#keyring-is-out-of-date-2213 and discussion in issue https://github.com/archlinux/archinstall/issues/2213.
For a quick fix the below command will install the latest keyrings
```pacman -Sy archlinux-keyring```
## How to dual boot with Windows
To install Arch Linux alongside an existing Windows installation using `archinstall`, follow these steps:
1. Ensure some unallocated space is available for the Linux installation after the Windows installation.
2. Boot into the ISO and run `archinstall`.
3. Choose `Disk configuration` -> `Manual partitioning`.
4. Select the disk on which Windows resides.
5. Select `Create a new partition`.
6. Choose a filesystem type.
7. Determine the start and end sectors for the new partition location (values can be suffixed with various units).
8. Assign the mountpoint `/` to the new partition.
9. Assign the `Boot/ESP` partition the mountpoint `/boot` from the partitioning menu.
10. Confirm your settings and exit to the main menu by choosing `Confirm and exit`.
11. Modify any additional settings for your installation as necessary.
12. Start the installation upon completion of setup.
# Mission Statement
Archinstall promises to ship a [guided installer](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) that follows
the [Arch Linux Principles](https://wiki.archlinux.org/index.php/Arch_Linux#Principles) as well as a library to manage services, packages, and other Arch Linux aspects.
The guided installer ensures a user-friendly experience, offering optional selections throughout the process. Emphasizing its flexible nature, these options are never obligatory.
In addition, the decision to use the guided installer remains entirely with the user, reflecting the Linux philosophy of providing full freedom and flexibility.
---
Archinstall primarily functions as a flexible library for managing services, packages, and other elements within an Arch Linux system.
This core library is the backbone for the guided installer that Archinstall provides. It is also designed to be used by those who wish to script their own custom installations.
Therefore, Archinstall will try its best to not introduce any breaking changes except for major releases which may break backward compatibility after notifying about such changes.
# Contributing
Please see [CONTRIBUTING.md](https://github.com/archlinux/archinstall/blob/master/CONTRIBUTING.md)

View File

@ -1,3 +1,275 @@
from archinstall.lib.plugins import plugin """Arch Linux installer - guided, templates etc."""
import urllib.error
import urllib.parse
import urllib.request
from argparse import ArgumentParser
__all__ = ['plugin'] from .lib.disk import *
from .lib.exceptions import *
from .lib.general import *
from .lib.hardware import *
from .lib.installer import __packages__, Installer, accessibility_tools_in_use
from .lib.locale_helpers import *
from .lib.luks import *
from .lib.mirrors import *
from .lib.models.network_configuration import NetworkConfigurationHandler
from .lib.networking import *
from .lib.output import *
from .lib.models.dataclasses import (
VersionDef,
PackageSearchResult,
PackageSearch,
LocalPackage
)
from .lib.packages.packages import (
group_search,
package_search,
find_package,
find_packages,
installed_package,
validate_package_list,
)
from .lib.profiles import *
from .lib.services import *
from .lib.storage import *
from .lib.systemd import *
from .lib.user_interaction import *
from .lib.menu import Menu
from .lib.menu.list_manager import ListManager
from .lib.menu.text_input import TextInput
from .lib.menu.global_menu import GlobalMenu
from .lib.menu.selection_menu import (
Selector,
GeneralMenu
)
from .lib.translation import Translation, DeferredTranslation
from .lib.plugins import plugins, load_plugin # This initiates the plugin loading ceremony
from .lib.configuration import *
parser = ArgumentParser()
__version__ = "2.4.2"
storage['__version__'] = __version__
# add the custome _ as a builtin, it can now be used anywhere in the
# project to mark strings as translatable with _('translate me')
DeferredTranslation.install()
def define_arguments():
"""
Define which explicit arguments do we allow.
Refer to https://docs.python.org/3/library/argparse.html for documentation and
https://docs.python.org/3/howto/argparse.html for a tutorial
Remember that the property/entry name python assigns to the parameters is the first string defined as argument and
dashes inside it '-' are changed to '_'
"""
parser.add_argument("--config", nargs="?", help="JSON configuration file or URL")
parser.add_argument("--creds", nargs="?", help="JSON credentials configuration file")
parser.add_argument("--disk_layouts","--disk_layout","--disk-layouts","--disk-layout",nargs="?",
help="JSON disk layout file")
parser.add_argument("--silent", action="store_true",
help="WARNING: Disables all prompts for input and confirmation. If no configuration is provided, this is ignored")
parser.add_argument("--dry-run", "--dry_run", action="store_true",
help="Generates a configuration file and then exits instead of performing an installation")
parser.add_argument("--script", default="guided", nargs="?", help="Script to run for installation", type=str)
parser.add_argument("--mount-point","--mount_point", nargs="?", type=str, help="Define an alternate mount point for installation")
parser.add_argument("--debug", action="store_true", default=False, help="Adds debug info into the log")
parser.add_argument("--plugin", nargs="?", type=str)
def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, error :bool = False) -> dict:
"""We accept arguments not defined to the parser. (arguments "ad hoc").
Internally argparse return to us a list of words so we have to parse its contents, manually.
We accept following individual syntax for each argument
--argument value
--argument=value
--argument = value
--argument (boolean as default)
the optional paramters to the function alter a bit its behaviour:
* multiple allows multivalued arguments, each value separated by whitespace. They're returned as a list
* error. If set any non correctly specified argument-value pair to raise an exception. Else, simply notifies the existence of a problem and continues processing.
To a certain extent, multiple and error are incompatible. In fact, the only error this routine can catch, as of now, is the event
argument value value ...
which isn't am error if multiple is specified
"""
tmp_list = unknowns[:] # wastes a few bytes, but avoids any collateral effect of the destructive nature of the pop method()
config = {}
key = None
last_key = None
while tmp_list:
element = tmp_list.pop(0) # retreive an element of the list
if element.startswith('--'): # is an argument ?
if '=' in element: # uses the arg=value syntax ?
key, value = [x.strip() for x in element[2:].split('=', 1)]
config[key] = value
last_key = key # for multiple handling
key = None # we have the kwy value pair we need
else:
key = element[2:]
config[key] = True # every argument starts its lifecycle as boolean
else:
if element == '=':
continue
if key:
config[key] = element
last_key = key # multiple
key = None
else:
if multiple and last_key:
if isinstance(config[last_key],str):
config[last_key] = [config[last_key],element]
else:
config[last_key].append(element)
elif error:
raise ValueError(f"Entry {element} is not related to any argument")
else:
print(f" We ignore the entry {element} as it isn't related to any argument")
return config
def get_arguments() -> Dict[str, Any]:
""" The handling of parameters from the command line
Is done on following steps:
0) we create a dict to store the arguments and their values
1) preprocess.
We take those arguments which use Json files, and read them into the argument dict. So each first level entry becomes a argument un it's own right
2) Load.
We convert the predefined argument list directly into the dict vía the vars() función. Non specified arguments are loaded with value None or false if they are booleans (action="store_true").
The name is chosen according to argparse conventions. See above (the first text is used as argument name, but underscore substitutes dash)
We then load all the undefined arguments. In this case the names are taken as written.
Important. This way explicit command line arguments take precedence over configuración files.
3) Amend
Change whatever is needed on the configuration dictionary (it could be done in post_process_arguments but this ougth to be left to changes anywhere else in the code, not in the arguments dictionary
"""
config = {}
args, unknowns = parser.parse_known_args()
# preprocess the json files.
# TODO Expand the url access to the other JSON file arguments ?
if args.config is not None:
try:
# First, let's check if this is a URL scheme instead of a filename
parsed_url = urllib.parse.urlparse(args.config)
if not parsed_url.scheme: # The Profile was not a direct match on a remote URL, it must be a local file.
if not json_stream_to_structure('--config',args.config,config):
exit(1)
else: # Attempt to load the configuration from the URL.
with urllib.request.urlopen(urllib.request.Request(args.config, headers={'User-Agent': 'ArchInstall'})) as response:
config.update(json.loads(response.read()))
except Exception as e:
raise ValueError(f"Could not load --config because: {e}")
if args.creds is not None:
if not json_stream_to_structure('--creds',args.creds,config):
exit(1)
# load the parameters. first the known, then the unknowns
config.update(vars(args))
config.update(parse_unspecified_argument_list(unknowns))
# amend the parameters (check internal consistency)
# Installation can't be silent if config is not passed
if args.config is not None :
config["silent"] = args.silent
else:
config["silent"] = False
# avoiding a compatibility issue
if 'dry-run' in config:
del config['dry-run']
return config
def load_config():
from .lib.models import NetworkConfiguration
"""
refine and set some arguments. Formerly at the scripts
"""
if arguments.get('harddrives', None) is not None:
if type(arguments['harddrives']) is str:
arguments['harddrives'] = arguments['harddrives'].split(',')
arguments['harddrives'] = [BlockDevice(BlockDev) for BlockDev in arguments['harddrives']]
# Temporarily disabling keep_partitions if config file is loaded
# Temporary workaround to make Desktop Environments work
if arguments.get('profile', None) is not None:
if type(arguments.get('profile', None)) is dict:
arguments['profile'] = Profile(None, arguments.get('profile', None)['path'])
else:
arguments['profile'] = Profile(None, arguments.get('profile', None))
storage['_desktop_profile'] = arguments.get('desktop-environment', None)
if arguments.get('mirror-region', None) is not None:
if type(arguments.get('mirror-region', None)) is dict:
arguments['mirror-region'] = arguments.get('mirror-region', None)
else:
selected_region = arguments.get('mirror-region', None)
arguments['mirror-region'] = {selected_region: list_mirrors()[selected_region]}
if arguments.get('sys-language', None) is not None:
arguments['sys-language'] = arguments.get('sys-language', 'en_US')
if arguments.get('sys-encoding', None) is not None:
arguments['sys-encoding'] = arguments.get('sys-encoding', 'utf-8')
if arguments.get('gfx_driver', None) is not None:
storage['gfx_driver_packages'] = AVAILABLE_GFX_DRIVERS.get(arguments.get('gfx_driver', None), None)
if arguments.get('servers', None) is not None:
storage['_selected_servers'] = arguments.get('servers', None)
if arguments.get('nic', None) is not None:
handler = NetworkConfigurationHandler()
handler.parse_arguments(arguments.get('nic'))
arguments['nic'] = handler.configuration
def post_process_arguments(arguments):
storage['arguments'] = arguments
if arguments.get('mount_point'):
storage['MOUNT_POINT'] = arguments['mount_point']
if arguments.get('debug', False):
log(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!", fg="red", level=logging.WARNING)
if arguments.get('plugin', None):
load_plugin(arguments['plugin'])
if arguments.get('disk_layouts', None) is not None:
# if 'disk_layouts' not in storage:
# storage['disk_layouts'] = {}
layout_storage = {}
if not json_stream_to_structure('--disk_layouts',arguments['disk_layouts'],layout_storage):
exit(1)
else:
if arguments.get('harddrives') is None:
arguments['harddrives'] = [disk for disk in layout_storage]
# backward compatibility. Change partition.format for partition.wipe
for disk in layout_storage:
for i,partition in enumerate(layout_storage[disk].get('partitions',[])):
if 'format' in partition:
partition['wipe'] = partition['format']
del partition['format']
arguments['disk_layouts'] = layout_storage
load_config()
define_arguments()
arguments = get_arguments()
post_process_arguments(arguments)
# @archinstall.plugin decorator hook to programmatically add
# plugins in runtime. Useful in profiles and other things.
def plugin(f, *args, **kwargs):
plugins[f.__name__] = f
def run_as_a_module():
"""
Since we're running this as a 'python -m archinstall' module OR
a nuitka3 compiled version of the project.
This function and the file __main__ acts as a entry point.
"""
# Add another path for finding profiles, so that list_profiles() in Script() can find guided.py, unattended.py etc.
storage['PROFILE_PATH'].append(os.path.abspath(f'{os.path.dirname(__file__)}/examples'))
try:
script = Script(arguments.get('script', None))
except ProfileNotFound as err:
print(f"Couldn't find file: {err}")
sys.exit(1)
os.chdir(os.path.abspath(os.path.dirname(__file__)))
# Remove the example directory from the PROFILE_PATH, to avoid guided.py etc shows up in user input questions.
storage['PROFILE_PATH'].pop()
script.execute()

View File

@ -1,6 +1,15 @@
import importlib
import sys import sys
import pathlib
from archinstall.main import main # Load .git version before the builtin version
if pathlib.Path('./archinstall/__init__.py').absolute().exists():
spec = importlib.util.spec_from_file_location("archinstall", "./archinstall/__init__.py")
archinstall = importlib.util.module_from_spec(spec)
sys.modules["archinstall"] = archinstall
spec.loader.exec_module(sys.modules["archinstall"])
else:
import archinstall
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main()) archinstall.run_as_a_module()

View File

@ -1,80 +0,0 @@
from typing import TYPE_CHECKING
from archinstall.lib.hardware import SysInfo
from archinstall.lib.log import debug
from archinstall.lib.models.application import Audio, AudioConfiguration
from archinstall.lib.models.users import User
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class AudioApp:
@property
def pulseaudio_packages(self) -> list[str]:
return [
'pulseaudio',
]
@property
def pipewire_packages(self) -> list[str]:
return [
'pipewire',
'pipewire-alsa',
'pipewire-jack',
'pipewire-pulse',
'gst-plugin-pipewire',
'libpulse',
'wireplumber',
]
def _enable_pipewire(
self,
install_session: Installer,
users: list[User] | None = None,
) -> None:
if users is None:
return
for user in users:
# Create the full path for enabling the pipewire systemd items
service_dir = install_session.target / 'home' / user.username / '.config' / 'systemd' / 'user' / 'default.target.wants'
service_dir.mkdir(parents=True, exist_ok=True)
# Set ownership of the entire user catalogue
install_session.arch_chroot(f'chown -R {user.username}:{user.username} /home/{user.username}')
# symlink in the correct pipewire systemd items
install_session.arch_chroot(
f'ln -sf /usr/lib/systemd/user/pipewire-pulse.service /home/{user.username}/.config/systemd/user/default.target.wants/pipewire-pulse.service',
run_as=user.username,
)
install_session.arch_chroot(
f'ln -sf /usr/lib/systemd/user/pipewire-pulse.socket /home/{user.username}/.config/systemd/user/default.target.wants/pipewire-pulse.socket',
run_as=user.username,
)
def install(
self,
install_session: Installer,
audio_config: AudioConfiguration,
users: list[User] | None = None,
) -> None:
debug(f'Installing audio server: {audio_config.audio.value}')
if audio_config.audio == Audio.NO_AUDIO:
debug('No audio server selected, skipping installation.')
return
if SysInfo.requires_sof_fw():
install_session.add_additional_packages('sof-firmware')
if SysInfo.requires_alsa_fw():
install_session.add_additional_packages('alsa-firmware')
match audio_config.audio:
case Audio.PIPEWIRE:
install_session.add_additional_packages(self.pipewire_packages)
self._enable_pipewire(install_session, users)
case Audio.PULSEAUDIO:
install_session.add_additional_packages(self.pulseaudio_packages)

View File

@ -1,26 +0,0 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class BluetoothApp:
@property
def packages(self) -> list[str]:
return [
'bluez',
'bluez-utils',
]
@property
def services(self) -> list[str]:
return [
'bluetooth.service',
]
def install(self, install_session: Installer) -> None:
debug('Installing Bluetooth')
install_session.add_additional_packages(self.packages)
install_session.enable_service(self.services)

View File

@ -1,52 +0,0 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.models.application import Firewall, FirewallConfiguration
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class FirewallApp:
@property
def ufw_packages(self) -> list[str]:
return [
'ufw',
]
@property
def fwd_packages(self) -> list[str]:
return [
'firewalld',
]
@property
def ufw_services(self) -> list[str]:
return [
'ufw.service',
]
@property
def fwd_services(self) -> list[str]:
return [
'firewalld.service',
]
def install(
self,
install_session: Installer,
firewall_config: FirewallConfiguration,
) -> None:
debug(f'Installing firewall: {firewall_config.firewall.value}')
match firewall_config.firewall:
case Firewall.UFW:
install_session.add_additional_packages(self.ufw_packages)
install_session.enable_service(self.ufw_services)
# write default conf file to enabled
ufw_conf = install_session.target / 'etc/ufw/ufw.conf'
ufw_conf.write_text(ufw_conf.read_text().replace('ENABLED=no', 'ENABLED=yes'))
case Firewall.FWD:
install_session.add_additional_packages(self.fwd_packages)
install_session.enable_service(self.fwd_services)

View File

@ -1,14 +0,0 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.models.application import FontsConfiguration
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class FontsApp:
def install(self, install_session: Installer, fonts_config: FontsConfiguration) -> None:
packages = [f.value for f in fonts_config.fonts]
debug(f'Installing fonts: {packages}')
install_session.add_additional_packages(packages)

View File

@ -1,49 +0,0 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.models.application import PowerManagement, PowerManagementConfiguration
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class PowerManagementApp:
@property
def ppd_packages(self) -> list[str]:
return [
'power-profiles-daemon',
]
@property
def tuned_packages(self) -> list[str]:
return [
'tuned',
'tuned-ppd',
]
@property
def ppd_services(self) -> list[str]:
return [
'power-profiles-daemon.service',
]
@property
def tuned_services(self) -> list[str]:
return [
'tuned.service',
]
def install(
self,
install_session: Installer,
power_management_config: PowerManagementConfiguration,
) -> None:
debug(f'Installing power management daemon: {power_management_config.power_management.value}')
match power_management_config.power_management:
case PowerManagement.POWER_PROFILES_DAEMON:
install_session.add_additional_packages(self.ppd_packages)
install_session.enable_service(self.ppd_services)
case PowerManagement.TUNED:
install_session.add_additional_packages(self.tuned_packages)
install_session.enable_service(self.tuned_services)

View File

@ -1,23 +0,0 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class PrintServiceApp:
@property
def packages(self) -> list[str]:
return ['cups', 'system-config-printer', 'cups-pk-helper']
@property
def services(self) -> list[str]:
return [
'cups.service',
]
def install(self, install_session: Installer) -> None:
debug('Installing print service')
install_session.add_additional_packages(self.packages)
install_session.enable_service(self.services)

View File

@ -1,118 +0,0 @@
from typing import TYPE_CHECKING, Self, override
from archinstall.default_profiles.desktops.utils import provision_seat_access
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType, SelectResult
from archinstall.lib.log import info
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class DesktopProfile(Profile):
def __init__(self, current_selection: list[Self] = []) -> None:
super().__init__(
'Desktop',
ProfileType.Desktop,
current_selection=current_selection,
support_greeter=True,
)
@property
@override
def packages(self) -> list[str]:
return [
'nano',
'vim',
'openssh',
'htop',
'wget',
'smartmontools',
'xdg-utils',
]
@property
@override
def default_greeter_type(self) -> GreeterType | None:
combined_greeters: dict[GreeterType, int] = {}
for profile in self.current_selection:
if profile.default_greeter_type:
combined_greeters.setdefault(profile.default_greeter_type, 0)
combined_greeters[profile.default_greeter_type] += 1
if len(combined_greeters) >= 1:
return list(combined_greeters)[0]
return None
async def _do_on_select_profiles(self) -> None:
for profile in self.current_selection:
await profile.do_on_select()
@override
async def do_on_select(self) -> SelectResult:
items = [
MenuItem(
p.name,
value=p,
preview_action=lambda x: x.value.preview_text() if x.value else None,
)
for p in profile_handler.get_desktop_profiles()
]
group = MenuItemGroup(items, sort_items=True, sort_case_sensitive=False)
group.set_selected_by_value(self.current_selection)
result = await Selection[Self](
group,
multi=True,
allow_reset=True,
allow_skip=True,
preview_location='right',
).show()
match result.type_:
case ResultType.Selection:
self.current_selection = result.get_values()
await self._do_on_select_profiles()
return SelectResult.NewSelection
case ResultType.Skip:
return SelectResult.SameSelection
case ResultType.Reset:
return SelectResult.ResetCurrent
@override
def post_install(self, install_session: Installer) -> None:
for profile in self.current_selection:
profile.post_install(install_session)
@override
def provision(self, install_session: Installer, users: list[User]) -> None:
for profile in self.current_selection:
profile.provision(install_session, users)
if seat_access := profile.custom_settings.get(CustomSetting.SeatAccess):
provision_seat_access(install_session, users, seat_access)
@override
def install(self, install_session: Installer) -> None:
# Install common packages for all desktop environments
install_session.add_additional_packages(self.packages)
xorg_installed = False
for profile in self.current_selection:
info(f'Installing profile {profile.name}...')
install_session.add_additional_packages(profile.packages)
install_session.enable_service(profile.services)
if not xorg_installed and profile.display_server == DisplayServerType.Xorg:
install_session.add_additional_packages(['xorg-server', 'xorg-xinit'])
xorg_installed = True
profile.install(install_session)

View File

@ -1,67 +0,0 @@
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import DisplayServerType, Profile, ProfileType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class AwesomeProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Awesome',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'awesome',
'alacritty',
'xorg-xrandr',
'xterm',
'feh',
'slock',
'terminus-font',
'gnu-free-fonts',
'ttf-liberation',
'xsel',
]
@override
def install(self, install_session: Installer) -> None:
super().install(install_session)
# TODO: Copy a full configuration to ~/.config/awesome/rc.lua instead.
with open(f'{install_session.target}/etc/xdg/awesome/rc.lua') as fh:
awesome_lua = fh.read()
# Replace xterm with alacritty for a smoother experience.
awesome_lua = awesome_lua.replace('"xterm"', '"alacritty"')
with open(f'{install_session.target}/etc/xdg/awesome/rc.lua', 'w') as fh:
fh.write(awesome_lua)
# TODO: Configure the right-click-menu to contain the above packages that were installed. (as a user config)
# TODO: check if we selected a greeter,
# but for now, awesome is intended to run without one.
with open(f'{install_session.target}/etc/X11/xinit/xinitrc') as xinitrc:
xinitrc_data = xinitrc.read()
for line in xinitrc_data.split('\n'):
if 'twm &' in line:
xinitrc_data = xinitrc_data.replace(line, f'# {line}')
if 'xclock' in line:
xinitrc_data = xinitrc_data.replace(line, f'# {line}')
if 'xterm' in line:
xinitrc_data = xinitrc_data.replace(line, f'# {line}')
xinitrc_data += '\n'
xinitrc_data += 'exec awesome\n'
with open(f'{install_session.target}/etc/X11/xinit/xinitrc', 'w') as xinitrc:
xinitrc.write(xinitrc_data)

View File

@ -1,39 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class BspwmProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Bspwm',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'bspwm',
'sxhkd',
'dmenu',
'xdo',
'rxvt-unicode',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm
@override
def provision(self, install_session: Installer, users: list[User]) -> None:
for user in users:
install_session.arch_chroot('mkdir -p ~/.config/bspwm ~/.config/sxhkd', run_as=user.username)
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/bspwmrc ~/.config/bspwm/', run_as=user.username)
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/sxhkdrc ~/.config/sxhkd/', run_as=user.username)
install_session.arch_chroot('chmod +x ~/.config/bspwm/bspwmrc', run_as=user.username)

View File

@ -1,30 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class BudgieProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Budgie',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
@property
@override
def packages(self) -> list[str]:
return [
'materia-gtk-theme',
'budgie',
'mate-terminal',
'nemo',
'nemo-fileroller',
'papirus-icon-theme',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.LightdmSlick

View File

@ -1,33 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class CinnamonProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Cinnamon',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'cinnamon',
'system-config-printer',
'gnome-keyring',
'gnome-terminal',
'engrampa',
'gnome-screenshot',
'gvfs-smb',
'xed',
'xdg-user-dirs-gtk',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm

View File

@ -1,26 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class CosmicProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Cosmic',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
@property
@override
def packages(self) -> list[str]:
return [
'cosmic',
'xdg-user-dirs',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.CosmicSession

View File

@ -1,27 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class DeepinProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Deepin',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'deepin',
'deepin-terminal',
'deepin-editor',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm

View File

@ -1,26 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class EnlightenmentProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Enlightenment',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'enlightenment',
'terminology',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm

View File

@ -1,26 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class GnomeProfile(Profile):
def __init__(self) -> None:
super().__init__(
'GNOME',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
@property
@override
def packages(self) -> list[str]:
return [
'gnome',
'gnome-tweaks',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Gdm

View File

@ -1,52 +0,0 @@
from typing import override
from archinstall.default_profiles.desktops.utils import select_seat_access
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
class HyprlandProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Hyprland',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
self.custom_settings = {CustomSetting.SeatAccess: None}
@property
@override
def packages(self) -> list[str]:
return [
'hyprland',
'dunst',
'kitty',
'uwsm',
'dolphin',
'wofi',
'xdg-desktop-portal-hyprland',
'qt5-wayland',
'qt6-wayland',
'polkit-kde-agent',
'grim',
'slurp',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Sddm
@property
@override
def services(self) -> list[str]:
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
return [pref]
return []
@override
async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

@ -1,33 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class I3wmProfile(Profile):
def __init__(self) -> None:
super().__init__(
'i3-wm',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'i3-wm',
'i3lock',
'i3status',
'i3blocks',
'xss-lock',
'xterm',
'lightdm-gtk-greeter',
'lightdm',
'dmenu',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm

View File

@ -1,46 +0,0 @@
from typing import override
from archinstall.default_profiles.desktops.utils import select_seat_access
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
class LabwcProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Labwc',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
self.custom_settings = {CustomSetting.SeatAccess: None}
@property
@override
def packages(self) -> list[str]:
additional = []
if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
additional = [seat]
return [
'alacritty',
'labwc',
] + additional
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm
@property
@override
def services(self) -> list[str]:
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
return [pref]
return []
@override
async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

@ -1,34 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class LxqtProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Lxqt',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
# NOTE: SDDM is the only officially supported greeter for LXQt, so unlike other DEs, lightdm is not used here.
# LXQt works with lightdm, but since this is not supported, we will not default to this.
# https://github.com/lxqt/lxqt/issues/795
@property
@override
def packages(self) -> list[str]:
return [
'lxqt',
'breeze-icons',
'oxygen-icons',
'xdg-utils',
'ttf-freefont',
'l3afpad',
'slock',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Sddm

View File

@ -1,26 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class MateProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Mate',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'mate',
'mate-extra',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm

View File

@ -1,54 +0,0 @@
from typing import override
from archinstall.default_profiles.desktops.utils import select_seat_access
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
class NiriProfile(Profile):
def __init__(self) -> None:
super().__init__(
'niri',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
self.custom_settings = {CustomSetting.SeatAccess: None}
@property
@override
def packages(self) -> list[str]:
additional = []
if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
additional = [seat]
return [
'niri',
'alacritty',
'fuzzel',
'mako',
'xorg-xwayland',
'waybar',
'swaybg',
'swayidle',
'swaylock',
'xdg-desktop-portal-gnome',
] + additional
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm
@property
@override
def services(self) -> list[str]:
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
return [pref]
return []
@override
async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

@ -1,66 +0,0 @@
import shutil
from pathlib import Path
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
_TERMINAL = 'alacritty'
_ASSETS_DIR = Path(__file__).parent / 'niri_dms_assets'
class NiriDmsProfile(Profile):
def __init__(self) -> None:
super().__init__(
'niri - DankMaterialShell',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
@property
@override
def packages(self) -> list[str]:
return [
'niri',
'dms-shell-niri',
'polkit',
'xdg-desktop-portal-gnome',
'xorg-xwayland',
'matugen',
'cava',
'kimageformats',
'cups-pk-helper',
'tuned-ppd',
_TERMINAL,
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.GreetdDms
@override
def provision(self, install_session: Installer, users: list[User]) -> None:
binds = (_ASSETS_DIR / 'dms/binds.kdl').read_text().replace('{{TERMINAL_COMMAND}}', _TERMINAL)
for user in users:
home = install_session.target / 'home' / user.username
niri_dir = home / '.config/niri'
dms_dir = niri_dir / 'dms'
dms_dir.mkdir(parents=True, exist_ok=True)
shutil.copy(_ASSETS_DIR / 'niri.kdl', niri_dir / 'config.kdl')
for name in ('colors.kdl', 'layout.kdl', 'alttab.kdl', 'outputs.kdl', 'cursor.kdl'):
shutil.copy(_ASSETS_DIR / 'dms' / name, dms_dir / name)
(dms_dir / 'binds.kdl').write_text(binds)
niri_unit_dropin = home / '.config/systemd/user/niri.service.d'
niri_unit_dropin.mkdir(parents=True, exist_ok=True)
(niri_unit_dropin / 'dms.conf').write_text('[Unit]\nWants=dms.service\n')
install_session.arch_chroot(f'chown -R {user.username}:{user.username} /home/{user.username}/.config')

View File

@ -1,10 +0,0 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
recent-windows {
highlight {
corner-radius 12
}
}

View File

@ -1,221 +0,0 @@
binds {
// === System & Overview ===
Mod+D repeat=false { toggle-overview; }
Mod+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
// === Application Launchers ===
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
Super+X hotkey-overlay-title="Power Menu: Toggle" { spawn "dms" "ipc" "call" "powermenu" "toggle"; }
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
}
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
// === Security ===
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "dms" "ipc" "call" "lock" "lock";
}
Mod+Shift+E { quit; }
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
// === Audio Controls ===
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
XF86AudioPause allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause";
}
XF86AudioPlay allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause";
}
XF86AudioPrev allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "previous";
}
XF86AudioNext allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "next";
}
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "increment" "3";
}
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
}
XF86MonBrightnessDown allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
}
// === Window Management ===
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Shift+T { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+W { toggle-column-tabbed-display; }
Mod+Shift+W hotkey-overlay-title="Create window rule" { spawn "dms" "ipc" "call" "window-rules" "toggle"; }
// === Focus Navigation ===
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
// === Window Movement ===
Mod+Shift+Left { move-column-left; }
Mod+Shift+Down { move-window-down; }
Mod+Shift+Up { move-window-up; }
Mod+Shift+Right { move-column-right; }
Mod+Shift+H { move-column-left; }
Mod+Shift+J { move-window-down; }
Mod+Shift+K { move-window-up; }
Mod+Shift+L { move-column-right; }
// === Column Navigation ===
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
// === Monitor Navigation ===
Mod+Ctrl+Left { focus-monitor-left; }
//Mod+Ctrl+Down { focus-monitor-down; }
//Mod+Ctrl+Up { focus-monitor-up; }
Mod+Ctrl+Right { focus-monitor-right; }
Mod+Ctrl+H { focus-monitor-left; }
Mod+Ctrl+J { focus-monitor-down; }
Mod+Ctrl+K { focus-monitor-up; }
Mod+Ctrl+L { focus-monitor-right; }
// === Move to Monitor ===
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// === Workspace Navigation ===
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Down { move-column-to-workspace-down; }
Mod+Ctrl+Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Workspace Management ===
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
spawn "dms" "ipc" "call" "workspace-rename" "open";
}
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// === Mouse Wheel Navigation ===
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// === Numbered Workspaces ===
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
// === Move to Numbered Workspaces ===
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
// === Column Management ===
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+Period { expel-window-from-column; }
// === Sizing & Layout ===
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
Mod+Ctrl+C { center-visible-columns; }
// === Manual Sizing ===
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// === Screenshots ===
XF86Launch1 { screenshot; }
Ctrl+XF86Launch1 { screenshot-screen; }
Alt+XF86Launch1 { screenshot-window; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// === System Controls ===
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+P { power-off-monitors; }
}

View File

@ -1,39 +0,0 @@
// ! Auto-generated file. Do not edit directly.
// Remove `include "dms/colors.kdl"` from your config to override.
layout {
background-color "transparent"
focus-ring {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
border {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
shadow {
color "#00000070"
}
tab-indicator {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
insert-hint {
color "#d0bcff80"
}
}
recent-windows {
highlight {
active-color "#4f378b"
urgent-color "#f2b8b5"
}
}

View File

@ -1,6 +0,0 @@
// Place cursor configuration here.
// Example:
// cursor {
// xcursor-theme "Adwaita"
// xcursor-size 24
// }

View File

@ -1,22 +0,0 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layout {
gaps 4
border {
width 2
}
focus-ring {
width 2
}
}
window-rule {
geometry-corner-radius 12
clip-to-geometry true
tiled-state true
draw-border-with-background false
}

View File

@ -1,7 +0,0 @@
// Place per-output configuration here.
// Example:
// output "DP-1" {
// mode "2560x1440@165"
// position x=0 y=0
// scale 1
// }

View File

@ -1,279 +0,0 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
config-notification {
disable-failed
}
gestures {
hot-corners {
off
}
}
// Input device configuration.
// Find the full list of options on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
input {
keyboard {
xkb {
// You can set rules, model, layout, variant and options.
// For more information, see xkeyboard-config(7).
// For example:
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
// If this section is empty, niri will fetch xkb settings
// from org.freedesktop.locale1. You can control these using
// localectl set-x11-keymap.
}
// Enable numlock on startup, omitting this setting disables it.
numlock
}
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// scroll-button-lock
// middle-emulation
}
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
}
// You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
// Remember to uncomment the node by removing "/-"!
/-output "eDP-2" {
mode "2560x1600@239.998993"
position x=2560 y=0
variable-refresh-rate
}
// Settings that influence how windows are positioned and sized.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout {
// Set gaps around windows in logical pixels.
background-color "transparent"
// When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left
// or right edge of the screen.
// - "always", the focused column will always be centered.
// - "on-overflow", focusing a column will center it if it doesn't fit
// together with the previously focused column.
center-focused-column "never"
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.33333
proportion 0.5
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
// preset-window-heights { }
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// By default focus ring and border are rendered as a solid background rectangle
// behind windows. That is, they will show up through semitransparent windows.
// This is because windows using client-side decorations can have an arbitrary shape.
//
// If you don't like that, you should uncomment `prefer-no-csd` below.
// Niri will draw focus ring and border *around* windows that agree to omit their
// client-side decorations.
//
// Alternatively, you can override it with a window rule called
// `draw-border-with-background`.
border {
off
width 4
active-color "#707070" // Neutral gray
inactive-color "#d0d0d0" // Light gray
urgent-color "#cc4444" // Softer red
}
shadow {
softness 30
spread 5
offset x=0 y=5
color "#0007"
}
struts {
}
}
layer-rule {
match namespace="^quickshell$"
place-within-backdrop true
}
overview {
workspace-shadow {
off
}
}
// Add lines like this to spawn processes at startup.
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors.
environment {
XDG_CURRENT_DESKTOP "niri"
}
hotkey-overlay {
skip-at-startup
}
prefer-no-csd
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
animations {
workspace-switch {
spring damping-ratio=0.80 stiffness=523 epsilon=0.0001
}
window-open {
duration-ms 150
curve "ease-out-expo"
}
window-close {
duration-ms 150
curve "ease-out-quad"
}
horizontal-view-movement {
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
}
window-movement {
spring damping-ratio=0.75 stiffness=323 epsilon=0.0001
}
window-resize {
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
}
config-notification-open-close {
spring damping-ratio=0.65 stiffness=923 epsilon=0.001
}
screenshot-ui-open {
duration-ms 200
curve "ease-out-quad"
}
overview-open-close {
spring damping-ratio=0.85 stiffness=800 epsilon=0.0001
}
}
// Window rules let you adjust behavior for individual windows.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
// Work around WezTerm's initial configure bug
// by setting an empty default-column-width.
window-rule {
// This regular expression is intentionally made as specific as possible,
// since this is the default config, and we want no false positives.
// You can get away with just app-id="wezterm" if you want.
match app-id=r#"^org\.wezfurlong\.wezterm$"#
default-column-width {}
}
window-rule {
match app-id=r#"^org\.gnome\."#
draw-border-with-background false
geometry-corner-radius 12
clip-to-geometry true
}
window-rule {
match app-id=r#"^gnome-control-center$"#
match app-id=r#"^pavucontrol$"#
match app-id=r#"^nm-connection-editor$"#
default-column-width { proportion 0.5; }
open-floating false
}
window-rule {
match app-id=r#"^org\.gnome\.Calculator$"#
match app-id=r#"^gnome-calculator$"#
match app-id=r#"^galculator$"#
match app-id=r#"^blueman-manager$"#
match app-id=r#"^org\.gnome\.Nautilus$"#
match app-id=r#"^xdg-desktop-portal$"#
open-floating true
}
window-rule {
match app-id=r#"^steam$"# title=r#"^notificationtoasts_\d+_desktop$"#
default-floating-position x=10 y=10 relative-to="bottom-right"
open-focused false
}
window-rule {
match app-id=r#"^org\.wezfurlong\.wezterm$"#
match app-id="Alacritty"
match app-id="zen"
match app-id="com.mitchellh.ghostty"
match app-id="kitty"
draw-border-with-background false
}
window-rule {
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
match app-id="zoom"
open-floating true
}
// Open dms windows as floating by default
window-rule {
match app-id=r#"org.quickshell$"#
match app-id=r#"com.danklinux.dms$"#
open-floating true
}
debug {
honor-xdg-activation-with-invalid-serial
}
// Override to disable super+tab
recent-windows {
binds {
Alt+Tab { next-window scope="output"; }
Alt+Shift+Tab { previous-window scope="output"; }
Alt+grave { next-window filter="app-id"; }
Alt+Shift+grave { previous-window filter="app-id"; }
}
}
// Include dms files
include "dms/colors.kdl"
include "dms/layout.kdl"
include "dms/alttab.kdl"
include "dms/binds.kdl"
include "dms/outputs.kdl"
include "dms/cursor.kdl"

View File

@ -1,120 +0,0 @@
from enum import StrEnum
from typing import override
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.packages.packages import available_package, package_group_info
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class PlasmaFlavor(StrEnum):
Meta = 'plasma-meta'
Plasma = 'plasma'
Desktop = 'plasma-desktop'
def show(self) -> str:
match self:
case PlasmaFlavor.Meta:
return f'{self.value} ({tr("Recommended")})'
case PlasmaFlavor.Plasma | PlasmaFlavor.Desktop:
return self.value
def package_details(self) -> str:
ty = ''
details = ''
desc = ''
match self:
case PlasmaFlavor.Meta:
ty = tr('Package')
desc = tr('Curated selection of KDE Plasma packages')
info = available_package(self.value)
if info is not None:
details = tr('Dependencies') + '\n'
details += '\n'.join(f'- {entry}' for entry in info.get_depends_on)
case PlasmaFlavor.Plasma:
ty = tr('Package group')
desc = tr('Extensive KDE Plasma installation')
group = package_group_info(self.value)
if group is not None:
details = tr('Packages in group') + '\n'
details += '\n'.join(f'- {entry}' for entry in group.packages)
case PlasmaFlavor.Desktop:
ty = tr('Package group')
desc = tr('Minimal KDE Plasma installation')
info = available_package(self.value)
if info is not None:
details = tr('Dependencies') + '\n'
details += '\n'.join(f'- {entry}' for entry in info.get_depends_on)
return f'{tr("Type")}: {ty}\n{tr("Description")}: {desc}\n\n{details}'
def packages(self) -> list[str]:
match self:
case PlasmaFlavor.Meta:
return ['plasma-meta']
case PlasmaFlavor.Plasma:
return ['plasma']
case PlasmaFlavor.Desktop:
return ['plasma-desktop']
class PlasmaProfile(Profile):
def __init__(self) -> None:
super().__init__(
'KDE Plasma',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
@property
@override
def packages(self) -> list[str]:
flavor_str = self.custom_settings.get(CustomSetting.PlasmaFlavor)
if flavor_str is not None:
flavor = PlasmaFlavor(flavor_str)
return flavor.packages()
else:
return PlasmaFlavor.Meta.packages() # use plasma-meta as the recommended default
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.PlasmaLoginManager
async def _select_flavor(self) -> None:
header = tr('Select a flavor of KDE Plasma to install') + '\n'
items = [
MenuItem(
s.show(),
value=s,
preview_action=lambda x: x.value.package_details() if x.value else None,
)
for s in PlasmaFlavor
]
group = MenuItemGroup(items, sort_items=False)
default = self.custom_settings.get(CustomSetting.PlasmaFlavor, None)
group.set_default_by_value(default)
result = await Selection[PlasmaFlavor](
group,
header=header,
allow_skip=False,
preview_location='right',
).show()
if result.type_ == ResultType.Selection:
self.custom_settings[CustomSetting.PlasmaFlavor] = result.get_value().value
@override
async def do_on_select(self) -> None:
await self._select_flavor()

View File

@ -1,26 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class QtileProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Qtile',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'qtile',
'alacritty',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm

View File

@ -1,27 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class RiverProfile(Profile):
def __init__(self) -> None:
super().__init__(
'River',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
@property
@override
def packages(self) -> list[str]:
return [
'foot',
'xdg-desktop-portal-wlr',
'river',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm

View File

@ -1,56 +0,0 @@
from typing import override
from archinstall.default_profiles.desktops.utils import select_seat_access
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
class SwayProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Sway',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
self.custom_settings = {CustomSetting.SeatAccess: None}
@property
@override
def packages(self) -> list[str]:
additional = []
if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
additional = [seat]
return [
'sway',
'swaybg',
'swaylock',
'swayidle',
'waybar',
'wmenu',
'brightnessctl',
'grim',
'slurp',
'pavucontrol',
'foot',
'xorg-xwayland',
] + additional
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm
@property
@override
def services(self) -> list[str]:
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
return [pref]
return []
@override
async def do_on_select(self) -> None:
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

@ -1,45 +0,0 @@
from enum import Enum
from archinstall.lib.installer import Installer
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.models.users import User
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class SeatAccess(Enum):
seatd = 'seatd'
polkit = 'polkit'
def provision_seat_access(
install_session: Installer,
users: list[User],
seat_access: str,
) -> None:
if seat_access == SeatAccess.seatd.value:
for user in users:
install_session.arch_chroot(f'usermod -a -G seat {user.username}')
async def select_seat_access(profile_name: str, default: str | None) -> SeatAccess:
header = tr('{} needs access to your seat').format(profile_name)
header += f' ({tr("collection of hardware devices i.e. keyboard, mouse")})' + '\n'
header += tr('Choose an option how to give {} access to your hardware').format(profile_name)
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(default)
result = await Selection[SeatAccess](
group,
header=header,
allow_skip=False,
).show()
if result.type_ == ResultType.Selection:
return result.get_value()
else:
raise ValueError('Unexpected result type from seat access selection')

View File

@ -1,29 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class Xfce4Profile(Profile):
def __init__(self) -> None:
super().__init__(
'Xfce4',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'xfce4',
'xfce4-goodies',
'pavucontrol',
'gvfs',
'xarchiver',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm

View File

@ -1,29 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class XmonadProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Xmonad',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'xmonad',
'xmonad-contrib',
'xmonad-extras',
'xterm',
'dmenu',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm

View File

@ -1,9 +0,0 @@
from archinstall.default_profiles.profile import Profile, ProfileType
class MinimalProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Minimal',
ProfileType.Minimal,
)

View File

@ -1,218 +0,0 @@
from enum import Enum, StrEnum, auto
from typing import TYPE_CHECKING, Self
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class DisplayServerType(Enum):
Xorg = 'Xorg'
Wayland = 'Wayland'
class ProfileType(Enum):
# top level default_profiles
Server = 'Server'
Desktop = 'Desktop'
Xorg = 'Xorg'
Minimal = 'Minimal'
Custom = 'Custom'
# detailed selection default_profiles
ServerType = 'ServerType'
WindowMgr = 'Window Manager'
DesktopEnv = 'Desktop Environment'
CustomType = 'CustomType'
# special things
Application = 'Application'
class GreeterType(Enum):
Lightdm = 'lightdm-gtk-greeter'
LightdmSlick = 'lightdm-slick-greeter'
Sddm = 'sddm'
Gdm = 'gdm'
Ly = 'ly'
CosmicSession = 'cosmic-greeter'
PlasmaLoginManager = 'plasma-login-manager'
GreetdDms = 'dms-greeter'
class SelectResult(Enum):
NewSelection = auto()
SameSelection = auto()
ResetCurrent = auto()
class CustomSetting(StrEnum):
SeatAccess = 'seat_access'
PlasmaFlavor = 'plasma_flavor'
class Profile:
def __init__(
self,
name: str,
profile_type: ProfileType,
current_selection: list[Self] = [],
packages: list[str] = [],
services: list[str] = [],
support_gfx_driver: bool = False,
support_greeter: bool = False,
display_server: DisplayServerType | None = None,
) -> None:
self.name = name
self.profile_type = profile_type
self.custom_settings: dict[CustomSetting, str | None] = {}
self._support_gfx_driver = support_gfx_driver
self._support_greeter = support_greeter
self._display_server = display_server
# self.gfx_driver: str | None = None
self.current_selection = current_selection
self._packages = packages
self._services = services
# Only used for custom default_profiles
self.custom_enabled = False
@property
def packages(self) -> list[str]:
"""
Returns a list of packages that should be installed when
this profile is among the chosen ones
"""
return self._packages
@property
def services(self) -> list[str]:
"""
Returns a list of services that should be enabled when
this profile is among the chosen ones
"""
return self._services
@property
def default_greeter_type(self) -> GreeterType | None:
"""
Setting a default greeter type for a desktop profile
"""
return None
def install(self, install_session: Installer) -> None:
"""
Performs installation steps when this profile was selected
"""
def post_install(self, install_session: Installer) -> None:
"""
Hook that will be called when the installation process is
finished and custom installation steps for specific default_profiles
are needed
"""
def provision(self, install_session: Installer, users: list[User]) -> None:
"""
Hook that will be called when the installation process is
finished and user configuration for specific default_profiles
is needed
"""
def json(self) -> dict[str, str]:
"""
Returns a json representation of the profile
"""
return {}
async def do_on_select(self) -> SelectResult | None:
"""
Hook that will be called when a profile is selected
"""
return SelectResult.NewSelection
def set_custom_settings(self, settings: dict[CustomSetting, str | None]) -> None:
"""
Set the custom settings for the profile.
This is also called when the settings are parsed from the config
and can be overridden to perform further actions based on the profile
"""
self.custom_settings = settings
def current_selection_names(self) -> list[str]:
if self.current_selection:
return [s.name for s in self.current_selection]
return []
def reset(self) -> None:
self.current_selection = []
def is_top_level_profile(self) -> bool:
top_levels = [ProfileType.Desktop, ProfileType.Server, ProfileType.Xorg, ProfileType.Minimal, ProfileType.Custom]
return self.profile_type in top_levels
def is_desktop_profile(self) -> bool:
return self.profile_type == ProfileType.Desktop
def is_server_type_profile(self) -> bool:
return self.profile_type == ProfileType.ServerType
def is_desktop_type_profile(self) -> bool:
return self.profile_type == ProfileType.DesktopEnv or self.profile_type == ProfileType.WindowMgr
def is_xorg_type_profile(self) -> bool:
return self.profile_type == ProfileType.Xorg
def is_custom_type_profile(self) -> bool:
return self.profile_type == ProfileType.CustomType
def is_graphic_driver_supported(self) -> bool:
if not self.current_selection:
return self._support_gfx_driver
else:
if any([p._support_gfx_driver for p in self.current_selection]):
return True
return False
def is_greeter_supported(self) -> bool:
return self._support_greeter
@property
def display_server(self) -> DisplayServerType | None:
return self._display_server
def preview_text(self) -> str:
"""
Override this method to provide a preview text for the profile
"""
if self.is_desktop_type_profile():
if self._display_server:
text = tr('Environment type: {} {}').format(self._display_server.value, self.profile_type.value)
else:
text = tr('Environment type: {}').format(self.profile_type.value)
if packages := self.packages_text():
text += f'\n{packages}'
return text
return self.packages_text()
def packages_text(self, include_sub_packages: bool = False) -> str:
packages = set()
if self.packages:
packages = set(self.packages)
if include_sub_packages:
for sub_profile in self.current_selection:
if sub_profile.packages:
packages.update(sub_profile.packages)
text = tr('Installed packages') + ':\n'
for pkg in sorted(packages):
text += f' - {pkg}\n'
return text

View File

@ -1,77 +0,0 @@
from typing import TYPE_CHECKING, Self, override
from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult
from archinstall.lib.log import info
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class ServerProfile(Profile):
def __init__(self, current_value: list[Self] = []):
super().__init__(
'Server',
ProfileType.Server,
current_selection=current_value,
)
@override
async def do_on_select(self) -> SelectResult:
items = [
MenuItem(
p.name,
value=p,
preview_action=lambda x: x.value.preview_text() if x.value else None,
)
for p in profile_handler.get_server_profiles()
]
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(self.current_selection)
result = await Selection[Self](
group,
allow_reset=True,
allow_skip=True,
multi=True,
preview_location='right',
).show()
match result.type_:
case ResultType.Selection:
selections = result.get_values()
self.current_selection = selections
return SelectResult.NewSelection
case ResultType.Skip:
return SelectResult.SameSelection
case ResultType.Reset:
return SelectResult.ResetCurrent
@override
def provision(self, install_session: Installer, users: list[User]) -> None:
for profile in self.current_selection:
profile.provision(install_session, users)
@override
def post_install(self, install_session: Installer) -> None:
for profile in self.current_selection:
profile.post_install(install_session)
@override
def install(self, install_session: Installer) -> None:
server_info = self.current_selection_names()
details = ', '.join(server_info)
info(f'Now installing the selected servers: {details}')
for server in self.current_selection:
info(f'Installing {server.name}...')
install_session.add_additional_packages(server.packages)
install_session.enable_service(server.services)
server.install(install_session)
info('If your selections included multiple servers with the same port, you may have to reconfigure them.')

View File

@ -1,21 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import Profile, ProfileType
class CockpitProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Cockpit',
ProfileType.ServerType,
)
@property
@override
def packages(self) -> list[str]:
return ['cockpit', 'udisks2', 'packagekit']
@property
@override
def services(self) -> list[str]:
return ['cockpit.socket']

View File

@ -1,30 +0,0 @@
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import Profile, ProfileType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class DockerProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Docker',
ProfileType.ServerType,
)
@property
@override
def packages(self) -> list[str]:
return ['docker']
@property
@override
def services(self) -> list[str]:
return ['docker']
@override
def provision(self, install_session: Installer, users: list[User]) -> None:
for user in users:
install_session.arch_chroot(f'usermod -a -G docker {user.username}')

View File

@ -1,21 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import Profile, ProfileType
class HttpdProfile(Profile):
def __init__(self) -> None:
super().__init__(
'httpd',
ProfileType.ServerType,
)
@property
@override
def packages(self) -> list[str]:
return ['apache']
@property
@override
def services(self) -> list[str]:
return ['httpd']

View File

@ -1,21 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import Profile, ProfileType
class LighttpdProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Lighttpd',
ProfileType.ServerType,
)
@property
@override
def packages(self) -> list[str]:
return ['lighttpd']
@property
@override
def services(self) -> list[str]:
return ['lighttpd']

View File

@ -1,28 +0,0 @@
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import Profile, ProfileType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class MariadbProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Mariadb',
ProfileType.ServerType,
)
@property
@override
def packages(self) -> list[str]:
return ['mariadb']
@property
@override
def services(self) -> list[str]:
return ['mariadb']
@override
def post_install(self, install_session: Installer) -> None:
install_session.arch_chroot('mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql')

View File

@ -1,21 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import Profile, ProfileType
class NginxProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Nginx',
ProfileType.ServerType,
)
@property
@override
def packages(self) -> list[str]:
return ['nginx']
@property
@override
def services(self) -> list[str]:
return ['nginx']

View File

@ -1,28 +0,0 @@
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import Profile, ProfileType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class PostgresqlProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Postgresql',
ProfileType.ServerType,
)
@property
@override
def packages(self) -> list[str]:
return ['postgresql']
@property
@override
def services(self) -> list[str]:
return ['postgresql']
@override
def post_install(self, install_session: Installer) -> None:
install_session.arch_chroot('initdb -D /var/lib/postgres/data', run_as='postgres')

View File

@ -1,21 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import Profile, ProfileType
class SshdProfile(Profile):
def __init__(self) -> None:
super().__init__(
'sshd',
ProfileType.ServerType,
)
@property
@override
def packages(self) -> list[str]:
return ['openssh']
@property
@override
def services(self) -> list[str]:
return ['sshd']

View File

@ -1,21 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import Profile, ProfileType
class TomcatProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Tomcat',
ProfileType.ServerType,
)
@property
@override
def packages(self) -> list[str]:
return ['tomcat10']
@property
@override
def services(self) -> list[str]:
return ['tomcat10']

View File

@ -1,32 +0,0 @@
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import DisplayServerType, Profile, ProfileType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class XorgProfile(Profile):
def __init__(
self,
name: str = 'Xorg',
profile_type: ProfileType = ProfileType.Xorg,
):
super().__init__(
name,
profile_type,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'xorg-server',
'xorg-xinit',
]
@override
def install(self, install_session: Installer) -> None:
install_session.add_additional_packages(self.packages)

View File

@ -1,51 +0,0 @@
from typing import TYPE_CHECKING
from archinstall.applications.audio import AudioApp
from archinstall.applications.bluetooth import BluetoothApp
from archinstall.applications.firewall import FirewallApp
from archinstall.applications.fonts import FontsApp
from archinstall.applications.power_management import PowerManagementApp
from archinstall.applications.print_service import PrintServiceApp
from archinstall.lib.models import Audio
from archinstall.lib.models.application import ApplicationConfiguration
from archinstall.lib.models.users import User
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class ApplicationHandler:
def __init__(self) -> None:
pass
def install_applications(self, install_session: Installer, app_config: ApplicationConfiguration, users: list[User] | None = None) -> None:
if app_config.bluetooth_config and app_config.bluetooth_config.enabled:
BluetoothApp().install(install_session)
if app_config.audio_config and app_config.audio_config.audio != Audio.NO_AUDIO:
AudioApp().install(
install_session,
app_config.audio_config,
users,
)
if app_config.power_management_config:
PowerManagementApp().install(
install_session,
app_config.power_management_config,
)
if app_config.print_service_config and app_config.print_service_config.enabled:
PrintServiceApp().install(install_session)
if app_config.firewall_config:
FirewallApp().install(
install_session,
app_config.firewall_config,
)
if app_config.fonts_config:
FontsApp().install(
install_session,
app_config.fonts_config,
)

View File

@ -1,263 +0,0 @@
from typing import override
from archinstall.lib.hardware import SysInfo
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.application import (
ApplicationConfiguration,
Audio,
AudioConfiguration,
BluetoothConfiguration,
Firewall,
FirewallConfiguration,
FontPackage,
FontsConfiguration,
PowerManagement,
PowerManagementConfiguration,
PrintServiceConfiguration,
)
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
def __init__(
self,
preset: ApplicationConfiguration | None = None,
):
if preset:
self._app_config = preset
else:
self._app_config = ApplicationConfiguration()
menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_options, checkmarks=True)
super().__init__(
self._item_group,
config=self._app_config,
allow_reset=True,
)
@override
async def show(self) -> ApplicationConfiguration | None:
_ = await super().show()
return self._app_config
def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=tr('Bluetooth'),
action=select_bluetooth,
value=self._app_config.bluetooth_config,
preview_action=self._prev_bluetooth,
key='bluetooth_config',
),
MenuItem(
text=tr('Audio'),
action=select_audio,
preview_action=self._prev_audio,
key='audio_config',
),
MenuItem(
text=tr('Print service'),
action=select_print_service,
preview_action=self._prev_print_service,
key='print_service_config',
),
MenuItem(
text=tr('Power management'),
action=select_power_management,
preview_action=self._prev_power_management,
enabled=SysInfo.has_battery(),
key='power_management_config',
),
MenuItem(
text=tr('Firewall'),
action=select_firewall,
preview_action=self._prev_firewall,
key='firewall_config',
),
MenuItem(
text=tr('Additional fonts'),
action=select_fonts,
value=self._app_config.fonts_config,
preview_action=self._prev_fonts,
key='fonts_config',
),
]
def _prev_power_management(self, item: MenuItem) -> str | None:
if item.value is not None:
config: PowerManagementConfiguration = item.value
return f'{tr("Power management")}: {config.power_management.value}'
return None
def _prev_bluetooth(self, item: MenuItem) -> str | None:
if item.value is not None:
bluetooth_config: BluetoothConfiguration = item.value
output = f'{tr("Bluetooth")}: '
output += tr('Enabled') if bluetooth_config.enabled else tr('Disabled')
return output
return None
def _prev_audio(self, item: MenuItem) -> str | None:
if item.value is not None:
config: AudioConfiguration = item.value
return f'{tr("Audio")}: {config.audio.value}'
return None
def _prev_print_service(self, item: MenuItem) -> str | None:
if item.value is not None:
print_service_config: PrintServiceConfiguration = item.value
output = f'{tr("Print service")}: '
output += tr('Enabled') if print_service_config.enabled else tr('Disabled')
return output
return None
def _prev_firewall(self, item: MenuItem) -> str | None:
if item.value is not None:
config: FirewallConfiguration = item.value
return f'{tr("Firewall")}: {config.firewall.value}'
return None
def _prev_fonts(self, item: MenuItem) -> str | None:
if item.value is not None:
config: FontsConfiguration = item.value
packages = ', '.join(f.value for f in config.fonts)
return f'{tr("Additional fonts")}: {packages}'
return None
async def select_power_management(preset: PowerManagementConfiguration | None = None) -> PowerManagementConfiguration | None:
group = MenuItemGroup.from_enum(PowerManagement)
if preset:
group.set_focus_by_value(preset.power_management)
result = await Selection[PowerManagement](
group,
allow_skip=True,
allow_reset=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return PowerManagementConfiguration(power_management=result.get_value())
case ResultType.Reset:
return None
async def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None:
header = tr('Would you like to configure Bluetooth?') + '\n'
preset_val = preset.enabled if preset else False
result = await Confirmation(
header=header,
allow_skip=True,
preset=preset_val,
).show()
match result.type_:
case ResultType.Selection:
return BluetoothConfiguration(result.get_value())
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled result type')
async def select_print_service(preset: PrintServiceConfiguration | None) -> PrintServiceConfiguration | None:
header = tr('Would you like to configure the print service?') + '\n'
preset_val = preset.enabled if preset else False
result = await Confirmation(
header=header,
allow_skip=True,
preset=preset_val,
).show()
match result.type_:
case ResultType.Selection:
result.get_value()
return PrintServiceConfiguration(result.get_value())
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled result type')
async def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration | None:
items = [MenuItem(a.value, value=a) for a in Audio]
group = MenuItemGroup(items)
if preset:
group.set_focus_by_value(preset.audio)
result = await Selection[Audio](
group,
header=tr('Select audio configuration'),
allow_skip=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return AudioConfiguration(audio=result.get_value())
case ResultType.Reset:
raise ValueError('Unhandled result type')
async def select_firewall(preset: FirewallConfiguration | None = None) -> FirewallConfiguration | None:
group = MenuItemGroup.from_enum(Firewall)
if preset:
group.set_focus_by_value(preset.firewall)
result = await Selection[Firewall](
group,
allow_skip=True,
allow_reset=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return FirewallConfiguration(firewall=result.get_value())
case ResultType.Reset:
return None
async def select_fonts(preset: FontsConfiguration | None = None) -> FontsConfiguration | None:
items = [MenuItem(f'{f.value} ({f.description()})', value=f) for f in FontPackage]
group = MenuItemGroup(items)
if preset:
for f in preset.fonts:
group.set_selected_by_value(f)
result = await Selection[FontPackage](
group,
header=tr('Select font packages to install'),
allow_skip=True,
allow_reset=True,
multi=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
selected = result.get_values()
if selected:
return FontsConfiguration(fonts=selected)
return None
case ResultType.Reset:
return None

View File

@ -1,801 +0,0 @@
import argparse
import json
import os
import stat
import sys
import tempfile
import urllib.error
import urllib.parse
from argparse import ArgumentParser, Namespace
from dataclasses import dataclass, field
from enum import Enum, StrEnum
from pathlib import Path
from typing import Any, Self
from urllib.request import Request, urlopen
from pydantic import TypeAdapter
from pydantic.dataclasses import dataclass as p_dataclass
from archinstall.lib.crypt import decrypt, encrypt
from archinstall.lib.log import debug, error, logger, warn
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration
from archinstall.lib.models.network import NetworkConfiguration
from archinstall.lib.models.package_types import DEFAULT_KERNEL
from archinstall.lib.models.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.models.users import Password, User, UserSerialization
from archinstall.lib.plugins import load_plugin
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_key_value_pair
from archinstall.lib.version import get_version
from archinstall.tui.components import tui
class SubCommand(Enum):
SHARE_LOG = 'share-log'
@p_dataclass
class Arguments:
config: Path | None = None
config_url: str | None = None
creds: Path | None = None
creds_url: str | None = None
creds_decryption_key: str | None = None
silent: bool = False
dry_run: bool = False
script: str | None = None
mountpoint: Path = Path('/mnt')
skip_ntp: bool = False
skip_wkd: bool = False
skip_boot: bool = False
debug: bool = False
offline: bool = False
no_pkg_lookups: bool = False
plugin: Path | None = None
plugin_url: str | None = None
skip_version_check: bool = False
skip_wifi_check: bool = False
advanced: bool = False
verbose: bool = False
command: SubCommand | None = None
class ArchConfigType(StrEnum):
VERSION = 'version'
SCRIPT = 'script'
LOCALE_CONFIG = 'locale_config'
ARCHINSTALL_LANGUAGE = 'archinstall_language'
DISK_CONFIG = 'disk_config'
PROFILE_CONFIG = 'profile_config'
MIRROR_CONFIG = 'mirror_config'
NETWORK_CONFIG = 'network_config'
BOOTLOADER_CONFIG = 'bootloader_config'
APP_CONFIG = 'app_config'
AUTH_CONFIG = 'auth_config'
SWAP = 'swap'
USERS = 'users'
ROOT_ENC_PASSWORD = 'root_enc_password'
ENCRYPTION_PASSWORD = 'encryption_password'
HOSTNAME = 'hostname'
KERNELS = 'kernels'
NTP = 'ntp'
TIMEZONE = 'timezone'
SERVICES = 'services'
PACKAGES = 'packages'
PACMAN_CONFIG = 'pacman_config'
CUSTOM_COMMANDS = 'custom_commands'
def text(self) -> str:
match self:
case ArchConfigType.ARCHINSTALL_LANGUAGE:
return tr('ArchInstall Language')
case ArchConfigType.VERSION:
return tr('Version')
case ArchConfigType.SCRIPT:
return tr('Installation Script')
case ArchConfigType.LOCALE_CONFIG:
return tr('Locales')
case ArchConfigType.DISK_CONFIG:
return tr('Disk configuration')
case ArchConfigType.PROFILE_CONFIG:
return tr('Profile')
case ArchConfigType.MIRROR_CONFIG:
return tr('Mirrors and repositories')
case ArchConfigType.NETWORK_CONFIG:
return tr('Network')
case ArchConfigType.BOOTLOADER_CONFIG:
return tr('Bootloader')
case ArchConfigType.APP_CONFIG:
return tr('Application')
case ArchConfigType.AUTH_CONFIG:
return tr('Authentication')
case ArchConfigType.SWAP:
return tr('Swap')
case ArchConfigType.HOSTNAME:
return tr('Hostname')
case ArchConfigType.KERNELS:
return tr('Kernels')
case ArchConfigType.NTP:
return tr('Automatic time sync (NTP)')
case ArchConfigType.TIMEZONE:
return tr('Timezone')
case ArchConfigType.SERVICES:
return tr('Services')
case ArchConfigType.PACKAGES:
return tr('Additional packages')
case ArchConfigType.PACMAN_CONFIG:
return tr('Pacman')
case ArchConfigType.CUSTOM_COMMANDS:
return tr('Custom commands')
case ArchConfigType.USERS:
return tr('Users')
case ArchConfigType.ROOT_ENC_PASSWORD:
return tr('Root encrypted password')
case ArchConfigType.ENCRYPTION_PASSWORD:
return tr('Disk encryption password')
USER_CONFIG_FILE: Path = Path('user_configuration.json')
USER_CREDS_FILE: Path = Path('user_credentials.json')
DEFAULT_SAVE_PATH = logger.directory
@dataclass
class ArchConfig:
version: str | None = None
script: str | None = None
locale_config: LocaleConfiguration | None = None
archinstall_language: Language = field(default_factory=lambda: translation_handler.get_language_by_abbr('en'))
disk_config: DiskLayoutConfiguration | None = None
profile_config: ProfileConfiguration | None = None
mirror_config: MirrorConfiguration | None = None
network_config: NetworkConfiguration | None = None
bootloader_config: BootloaderConfiguration | None = None
app_config: ApplicationConfiguration | None = None
auth_config: AuthenticationConfiguration | None = None
swap: ZramConfiguration | None = None
hostname: str = 'archlinux'
kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
ntp: bool = True
packages: list[str] = field(default_factory=list)
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
timezone: str = 'UTC'
services: list[str] = field(default_factory=list)
custom_commands: list[str] = field(default_factory=list)
def unsafe_config(self) -> dict[ArchConfigType, Any]:
config: dict[ArchConfigType, list[UserSerialization] | str | None] = {}
if self.auth_config:
if self.auth_config.users:
config[ArchConfigType.USERS] = [user.json() for user in self.auth_config.users]
if self.auth_config.root_enc_password:
config[ArchConfigType.ROOT_ENC_PASSWORD] = self.auth_config.root_enc_password.enc_password
if self.disk_config:
disk_encryption = self.disk_config.disk_encryption
if disk_encryption and disk_encryption.encryption_password:
config[ArchConfigType.ENCRYPTION_PASSWORD] = disk_encryption.encryption_password.plaintext
return config
def safe_config(self) -> dict[ArchConfigType, Any]:
base_config: dict[ArchConfigType, Any] = {
ArchConfigType.VERSION: self.version,
ArchConfigType.SCRIPT: self.script,
ArchConfigType.ARCHINSTALL_LANGUAGE: self.archinstall_language.json(),
}
base_config.update(self.plain_cfg())
sub_config = self.sub_cfg()
for config_type, value in sub_config.items():
if not hasattr(value, 'json'):
raise ValueError(f'Config value for {config_type} must implement json() method')
base_config[config_type] = value.json()
return base_config
def plain_cfg(self) -> dict[ArchConfigType, str | list[str] | bool]:
return {
ArchConfigType.HOSTNAME: self.hostname,
ArchConfigType.KERNELS: self.kernels,
ArchConfigType.NTP: self.ntp,
ArchConfigType.TIMEZONE: self.timezone,
ArchConfigType.SERVICES: self.services,
ArchConfigType.PACKAGES: self.packages,
ArchConfigType.CUSTOM_COMMANDS: self.custom_commands,
}
def sub_cfg(self) -> dict[ArchConfigType, SubConfig]:
cfg: dict[ArchConfigType, SubConfig] = {
ArchConfigType.PACMAN_CONFIG: self.pacman_config,
}
if self.mirror_config:
cfg[ArchConfigType.MIRROR_CONFIG] = self.mirror_config
if self.bootloader_config:
cfg[ArchConfigType.BOOTLOADER_CONFIG] = self.bootloader_config
if self.disk_config:
cfg[ArchConfigType.DISK_CONFIG] = self.disk_config
if self.swap:
cfg[ArchConfigType.SWAP] = self.swap
if self.auth_config:
cfg[ArchConfigType.AUTH_CONFIG] = self.auth_config
if self.locale_config:
cfg[ArchConfigType.LOCALE_CONFIG] = self.locale_config
if self.profile_config:
cfg[ArchConfigType.PROFILE_CONFIG] = self.profile_config
if self.network_config:
cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config
if self.app_config:
cfg[ArchConfigType.APP_CONFIG] = self.app_config
return cfg
@classmethod
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
arch_config = cls()
arch_config.locale_config = LocaleConfiguration.parse_arg(args_config)
if script := args_config.get('script', None):
arch_config.script = script
if archinstall_lang := args_config.get('archinstall-language', None):
arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang)
translation_handler.activate(arch_config.archinstall_language, set_font=False)
if disk_config := args_config.get('disk_config', {}):
enc_password = args_config.get('encryption_password', '')
password = Password(plaintext=enc_password) if enc_password else None
arch_config.disk_config = DiskLayoutConfiguration.parse_arg(disk_config, password)
# DEPRECATED
# backwards compatibility for main level disk_encryption entry
disk_encryption: DiskEncryption | None = None
if args_config.get('disk_encryption', None) is not None and arch_config.disk_config is not None:
disk_encryption = DiskEncryption.parse_arg(
arch_config.disk_config,
args_config['disk_encryption'],
Password(plaintext=args_config.get('encryption_password', '')),
)
if disk_encryption:
arch_config.disk_config.disk_encryption = disk_encryption
if profile_config := args_config.get('profile_config', None):
arch_config.profile_config = ProfileConfiguration.parse_arg(profile_config)
if mirror_config := args_config.get('mirror_config', None):
backwards_compatible_repo = []
if additional_repositories := args_config.get('additional-repositories', []):
backwards_compatible_repo = [Repository(r) for r in additional_repositories]
arch_config.mirror_config = MirrorConfiguration.parse_args(
mirror_config,
backwards_compatible_repo,
)
if net_config := args_config.get('network_config', None):
arch_config.network_config = NetworkConfiguration.parse_arg(net_config)
if bootloader_config_dict := args_config.get('bootloader_config', None):
arch_config.bootloader_config = BootloaderConfiguration.parse_arg(bootloader_config_dict, args.skip_boot)
# DEPRECATED: separate bootloader and uki fields (backward compatibility)
elif bootloader_str := args_config.get('bootloader', None):
bootloader = Bootloader.from_arg(bootloader_str, args.skip_boot)
uki = args_config.get('uki', False)
if uki and not bootloader.has_uki_support():
uki = False
arch_config.bootloader_config = BootloaderConfiguration(bootloader=bootloader, uki=uki, removable=True)
# deprecated: backwards compatibility
audio_config_args = args_config.get('audio_config', None)
app_config_args = args_config.get('app_config', None)
if audio_config_args is not None or app_config_args is not None:
arch_config.app_config = ApplicationConfiguration.parse_arg(app_config_args, audio_config_args)
if auth_config_args := args_config.get('auth_config', None):
arch_config.auth_config = AuthenticationConfiguration.parse_arg(auth_config_args)
if hostname := args_config.get('hostname', ''):
arch_config.hostname = hostname
if kernels := args_config.get('kernels', []):
arch_config.kernels = kernels
arch_config.ntp = args_config.get('ntp', True)
if packages := args_config.get('packages', []):
arch_config.packages = packages
if pacman_config := args_config.get('pacman_config', None):
arch_config.pacman_config = PacmanConfiguration.parse_arg(pacman_config)
elif parallel_downloads := args_config.get('parallel_downloads', 0):
arch_config.pacman_config = PacmanConfiguration(parallel_downloads=int(parallel_downloads))
swap_arg = args_config.get('swap')
if swap_arg is not None:
arch_config.swap = ZramConfiguration.parse_arg(swap_arg)
if timezone := args_config.get('timezone', 'UTC'):
arch_config.timezone = timezone
if services := args_config.get('services', []):
arch_config.services = services
# DEPRECATED: backwards compatibility
root_password = None
if root_password := args_config.get('!root-password', None):
root_password = Password(plaintext=root_password)
if enc_password := args_config.get('root_enc_password', None):
root_password = Password(enc_password=enc_password)
if root_password is not None:
if arch_config.auth_config is None:
arch_config.auth_config = AuthenticationConfiguration()
arch_config.auth_config.root_enc_password = root_password
# DEPRECATED: backwards compatibility
users: list[User] = []
if args_users := args_config.get('!users', None):
users = User.parse_arguments(args_users)
if args_users := args_config.get('users', None):
users = User.parse_arguments(args_users)
if users:
if arch_config.auth_config is None:
arch_config.auth_config = AuthenticationConfiguration()
arch_config.auth_config.users = users
if custom_commands := args_config.get('custom_commands', []):
arch_config.custom_commands = custom_commands
return arch_config
def user_config_to_json(self) -> str:
config = self.safe_config()
adapter = TypeAdapter(dict[ArchConfigType, Any])
python_dict = adapter.dump_python(config)
return json.dumps(python_dict, indent=4, sort_keys=True)
def user_credentials_to_json(self) -> str:
cfg = self.unsafe_config()
adapter = TypeAdapter(dict[ArchConfigType, Any])
python_dict = adapter.dump_python(cfg)
return json.dumps(python_dict, indent=4, sort_keys=True)
def write_debug(self) -> None:
debug(' -- Chosen configuration --')
debug(self.user_config_to_json())
def save(
self,
dest_path: Path | None = None,
creds: bool = False,
password: str | None = None,
) -> None:
save_path = dest_path or DEFAULT_SAVE_PATH
if not save_path.is_dir():
warn(
f'Destination directory {save_path} does not exist or is not a directory\n.',
'Configuration files can not be saved',
)
return
self.save_user_config(save_path)
if creds:
self.save_user_creds(save_path, password=password)
def save_user_config(self, dest_path: Path) -> None:
if not dest_path.is_dir():
error(f'Invalid path {dest_path}. User configuration could not be saved.')
return
target = dest_path / USER_CONFIG_FILE
data = self.user_config_to_json()
target.write_text(data)
target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
def save_user_creds(
self,
dest_path: Path,
password: str | None = None,
) -> None:
if not dest_path.is_dir():
error(f'Invalid path {dest_path}. User credentials could not be saved.')
return
data = self.user_credentials_to_json()
if password:
data = encrypt(password, data)
target = dest_path / USER_CREDS_FILE
target.write_text(data)
target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
def as_summary(self) -> str:
"""
Render a concise two-column summary of the current configuration.
Returns an empty string if nothing meaningful to show.
"""
cfg: dict[str, str | list[str] | bool] = {}
for key, value in self.plain_cfg().items():
cfg[key.text()] = value
for config_type, obj in self.sub_cfg().items():
if not hasattr(obj, 'summary'):
continue
summary = obj.summary()
if summary:
cfg[config_type.text()] = summary
simple_summary = as_key_value_pair(cfg, ignore_empty=True)
return simple_summary
class ArchConfigHandler:
def __init__(self) -> None:
self._parser: ArgumentParser = self._define_arguments()
self._add_sub_parsers()
self._args: Arguments = self._parse_args()
config = self._parse_config()
try:
self._config = ArchConfig.from_config(config, self._args)
self._config.version = get_version()
except ValueError as err:
warn(str(err))
sys.exit(1)
@property
def config(self) -> ArchConfig:
return self._config
@property
def args(self) -> Arguments:
return self._args
def get_script(self) -> str:
if script := self.args.script:
return script
if script := self.config.script:
return script
return 'guided'
def print_help(self) -> None:
self._parser.print_help()
def _add_sub_parsers(self) -> None:
subparsers = self._parser.add_subparsers(dest='command', help='Available subcommands')
_ = subparsers.add_parser(SubCommand.SHARE_LOG.value, help='Upload log file to public server')
def _define_arguments(self) -> ArgumentParser:
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
'-v',
'--version',
action='version',
default=False,
version='%(prog)s ' + get_version(),
)
parser.add_argument(
'--config',
type=Path,
nargs='?',
default=None,
help='JSON configuration file',
)
parser.add_argument(
'--config-url',
type=str,
nargs='?',
default=None,
help='Url to a JSON configuration file',
)
parser.add_argument(
'--creds',
type=Path,
nargs='?',
default=None,
help='JSON credentials configuration file',
)
parser.add_argument(
'--creds-url',
type=str,
nargs='?',
default=None,
help='Url to a JSON credentials configuration file',
)
parser.add_argument(
'--creds-decryption-key',
type=str,
nargs='?',
default=None,
help='Decryption key for credentials file',
)
parser.add_argument(
'--silent',
action='store_true',
default=False,
help='WARNING: Disables all prompts for input and confirmation. If no configuration is provided, this is ignored',
)
parser.add_argument(
'--dry-run',
'--dry_run',
action='store_true',
default=False,
help='Generates a configuration file and then exits instead of performing an installation',
)
parser.add_argument(
'--script',
nargs='?',
help='Script to run for installation',
type=str,
)
parser.add_argument(
'--mountpoint',
type=Path,
nargs='?',
default=Path('/mnt'),
help='Define an alternate mount point for installation',
)
parser.add_argument(
'--skip-ntp',
action='store_true',
help='Disables NTP checks during installation',
default=False,
)
parser.add_argument(
'--skip-wkd',
action='store_true',
help='Disables checking if archlinux keyring wkd sync is complete.',
default=False,
)
parser.add_argument(
'--skip-boot',
action='store_true',
help='Disables installation of a boot loader (note: only use this when problems arise with the boot loader step).',
default=False,
)
parser.add_argument(
'--debug',
action='store_true',
default=False,
help='Adds debug info into the log',
)
parser.add_argument(
'--offline',
action='store_true',
default=False,
help='Disabled online upstream services such as package search and key-ring auto update.',
)
parser.add_argument(
'--no-pkg-lookups',
action='store_true',
default=False,
help='Disabled package validation specifically prior to starting installation.',
)
parser.add_argument(
'--plugin',
nargs='?',
type=Path,
default=None,
help='File path to a plugin to load',
)
parser.add_argument(
'--plugin-url',
type=str,
nargs='?',
default=None,
help='Url to a plugin file to load',
)
parser.add_argument(
'--skip-version-check',
action='store_true',
default=False,
help='Skip the version check when running archinstall',
)
parser.add_argument(
'--skip-wifi-check',
action='store_true',
default=False,
help='Skip wifi check when running archinstall',
)
parser.add_argument(
'--advanced',
action='store_true',
default=False,
help='Enabled advanced options',
)
parser.add_argument(
'--verbose',
action='store_true',
default=False,
help='Enabled verbose options',
)
return parser
def _parse_args(self) -> Arguments:
argparse_args = vars(self._parser.parse_args())
args: Arguments = Arguments(**argparse_args)
# amend the parameters (check internal consistency)
# Installation can't be silent if config is not passed
if args.config is None and args.config_url is None:
args.silent = False
if args.debug:
warn(f'Warning: --debug mode will write certain credentials to {logger.path}!')
if args.plugin:
load_plugin(args.plugin)
if args.plugin_url:
plugin_data = self._fetch_from_url(args.plugin_url)
plugin_path = self._write_plugin_to_temp_file(plugin_data)
load_plugin(plugin_path)
if args.creds_decryption_key is None:
if os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY'):
args.creds_decryption_key = os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY')
return args
def _parse_config(self) -> dict[str, Any]:
config: dict[str, Any] = {}
config_data: str | None = None
creds_data: str | None = None
if self._args.config is not None:
config_data = self._read_file(self._args.config)
elif self._args.config_url is not None:
config_data = self._fetch_from_url(self._args.config_url)
if config_data is not None:
config.update(json.loads(config_data))
if self._args.creds is not None:
creds_data = self._read_file(self._args.creds)
elif self._args.creds_url is not None:
creds_data = self._fetch_from_url(self._args.creds_url)
if creds_data is not None:
json_data = self._process_creds_data(creds_data)
if json_data is not None:
config.update(json_data)
config = self._cleanup_config(config)
return config
def _process_creds_data(self, creds_data: str) -> dict[str, Any] | None:
if creds_data.startswith('$'): # encrypted data
if self._args.creds_decryption_key is not None:
try:
creds_data = decrypt(creds_data, self._args.creds_decryption_key)
return json.loads(creds_data)
except ValueError as err:
if 'Invalid password' in str(err):
error(tr('Incorrect credentials file decryption password'))
sys.exit(1)
else:
debug(f'Error decrypting credentials file: {err}')
raise err from err
else:
header = tr('Enter credentials file decryption password')
wrong_pwd_text = tr('Incorrect password')
prompt = header
while True:
decryption_pwd: Password | None = tui.run(
lambda p=prompt: get_password( # type: ignore[misc]
header=p,
allow_skip=False,
no_confirmation=True,
)
)
if not decryption_pwd:
return None
try:
creds_data = decrypt(creds_data, decryption_pwd.plaintext)
break
except ValueError as err:
if 'Invalid password' in str(err):
debug('Incorrect credentials file decryption password')
prompt = f'{header}' + f'\n\n{wrong_pwd_text}'
else:
debug(f'Error decrypting credentials file: {err}')
raise err from err
return json.loads(creds_data)
def _fetch_from_url(self, url: str) -> str:
if urllib.parse.urlparse(url).scheme:
try:
req = Request(url, headers={'User-Agent': 'ArchInstall'})
with urlopen(req) as resp:
return resp.read().decode('utf-8')
except urllib.error.HTTPError as err:
error(f'Could not fetch JSON from {url}: {err}')
else:
error('Not a valid url')
sys.exit(1)
def _write_plugin_to_temp_file(self, plugin_data: str) -> Path:
if not plugin_data.strip():
error('The downloaded plugin is empty')
sys.exit(1)
tmp_file = tempfile.NamedTemporaryFile(
mode='w',
suffix='.py',
prefix='archinstall_plugin_',
delete=False,
)
try:
with tmp_file as f:
f.write(plugin_data)
except OSError as err:
error(f'Could not write the downloaded plugin to a temporary file: {err}')
sys.exit(1)
return Path(tmp_file.name)
def _read_file(self, path: Path) -> str:
if not path.exists():
error(f'Could not find file {path}')
sys.exit(1)
return path.read_text()
def _cleanup_config(self, config: Namespace | dict[str, Any]) -> dict[str, Any]:
clean_args = {}
for key, val in config.items():
if isinstance(val, dict):
val = self._cleanup_config(val)
if val is not None:
clean_args[key] = val
return clean_args

View File

@ -1,126 +0,0 @@
import getpass
from pathlib import Path
from typing import TYPE_CHECKING
from archinstall.lib.command import SysCommandWorker
from archinstall.lib.log import debug, info
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import User
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class AuthenticationHandler:
def setup_auth(
self,
install_session: Installer,
auth_config: AuthenticationConfiguration,
hostname: str,
) -> None:
if auth_config.u2f_config and auth_config.users is not None:
self._setup_u2f_login(install_session, auth_config.u2f_config, auth_config.users, hostname)
def _setup_u2f_login(self, install_session: Installer, u2f_config: U2FLoginConfiguration, users: list[User], hostname: str) -> None:
self._configure_u2f_mapping(install_session, u2f_config, users, hostname)
self._update_pam_config(install_session, u2f_config)
def _update_pam_config(
self,
install_session: Installer,
u2f_config: U2FLoginConfiguration,
) -> None:
match u2f_config.u2f_login_method:
case U2FLoginMethod.Passwordless:
config_entry = 'auth sufficient pam_u2f.so authfile=/etc/u2f_mappings cue'
case U2FLoginMethod.SecondFactor:
config_entry = 'auth required pam_u2f.so authfile=/etc/u2f_mappings cue'
case _:
raise ValueError(f'Unknown U2F login method: {u2f_config.u2f_login_method}')
debug(f'U2F PAM configuration: {config_entry}')
debug(f'Passwordless sudo enabled: {u2f_config.passwordless_sudo}')
sudo_config = install_session.target / 'etc/pam.d/sudo'
sys_login = install_session.target / 'etc/pam.d/system-login'
if u2f_config.passwordless_sudo:
self._add_u2f_entry(sudo_config, config_entry)
self._add_u2f_entry(sys_login, config_entry)
def _add_u2f_entry(self, file: Path, entry: str) -> None:
if not file.exists():
debug(f'File does not exist: {file}')
return
content = file.read_text().splitlines()
# remove any existing u2f auth entry
content = [line for line in content if 'pam_u2f.so' not in line]
# add the u2f auth entry as the first one after comments
for i, line in enumerate(content):
if not line.startswith('#'):
content.insert(i, entry)
break
else:
content.append(entry)
file.write_text('\n'.join(content) + '\n')
def _configure_u2f_mapping(
self,
install_session: Installer,
u2f_config: U2FLoginConfiguration,
users: list[User],
hostname: str,
) -> None:
debug(f'Setting up U2F login: {u2f_config.u2f_login_method.value}')
install_session.pacman.strap('pam-u2f')
print(tr('Setting up U2F login: {}').format(u2f_config.u2f_login_method.value))
# https://developers.yubico.com/pam-u2f/
u2f_auth_file = install_session.target / 'etc/u2f_mappings'
u2f_auth_file.touch()
existing_keys = u2f_auth_file.read_text()
registered_keys: list[str] = []
for user in users:
print('')
info(tr('Setting up U2F device for user: {}').format(user.username))
info(tr('You may need to enter the PIN and then touch your U2F device to register it'))
cmd = ' '.join(
['arch-chroot', '-S', str(install_session.target), 'pamu2fcfg', '-u', user.username, '-o', f'pam://{hostname}', '-i', f'pam://{hostname}']
)
debug(f'Enrolling U2F device: {cmd}')
worker = SysCommandWorker(cmd, peek_output=True)
pin_inputted = False
while worker.is_alive():
if pin_inputted is False:
if bytes('enter pin for', 'UTF-8') in worker._trace_log.lower():
worker.write(bytes(getpass.getpass(''), 'UTF-8'))
pin_inputted = True
output = worker.decode().strip().splitlines()
debug(f'Output from pamu2fcfg: {output}')
key = output[-1].strip()
registered_keys.append(key)
all_keys = '\n'.join(registered_keys)
if existing_keys:
existing_keys += f'\n{all_keys}'
else:
existing_keys = all_keys
u2f_auth_file.write_text(existing_keys)

View File

@ -1,147 +0,0 @@
from typing import override
from archinstall.lib.disk.fido import Fido2
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import Password, User
from archinstall.lib.translationhandler import tr
from archinstall.lib.user.user_menu import select_users
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
def __init__(self, preset: AuthenticationConfiguration | None = None):
if preset:
self._auth_config = preset
else:
self._auth_config = AuthenticationConfiguration()
menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_options, checkmarks=True)
super().__init__(
self._item_group,
config=self._auth_config,
allow_reset=True,
)
@override
async def show(self) -> AuthenticationConfiguration | None:
return await super().show()
def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=tr('Root password'),
action=lambda x: select_root_password(),
preview_action=self._prev_root_pwd,
key='root_enc_password',
),
MenuItem(
text=tr('User account'),
action=self._create_user_account,
preview_action=self._prev_users,
key='users',
),
MenuItem(
text=tr('U2F login setup'),
action=select_u2f_login,
value=self._auth_config.u2f_config,
preview_action=self._prev_u2f_login,
key='u2f_config',
),
]
async def _create_user_account(self, preset: list[User] | None = None) -> list[User]:
preset = [] if preset is None else preset
users = await select_users(preset=preset)
return users
def _prev_users(self, item: MenuItem) -> str | None:
users: list[User] | None = item.value
if users:
return as_table(users)
return None
def _prev_root_pwd(self, item: MenuItem) -> str | None:
if item.value is not None:
password: Password = item.value
return f'{tr("Root password")}: {password.hidden()}'
return None
def _depends_on_u2f(self) -> bool:
devices = Fido2.get_fido2_devices()
if not devices:
return False
return True
def _prev_u2f_login(self, item: MenuItem) -> str | None:
if item.value is not None:
u2f_config: U2FLoginConfiguration = item.value
login_method = u2f_config.u2f_login_method.display_value()
output = tr('U2F login method: ') + login_method
output += '\n'
output += tr('Passwordless sudo: ') + (tr('Enabled') if u2f_config.passwordless_sudo else tr('Disabled'))
return output
devices = Fido2.get_fido2_devices()
if not devices:
return tr('No U2F devices found')
return None
async def select_root_password() -> Password | None:
password = await get_password(header=tr('Enter root password'), allow_skip=True)
return password
async def select_u2f_login(preset: U2FLoginConfiguration | None) -> U2FLoginConfiguration | None:
devices = Fido2.get_fido2_devices()
if not devices:
return None
items = []
for method in U2FLoginMethod:
items.append(MenuItem(method.display_value(), value=method))
group = MenuItemGroup(items)
if preset is not None:
group.set_selected_by_value(preset.u2f_login_method)
result = await Selection[U2FLoginMethod](
group,
allow_skip=True,
allow_reset=True,
).show()
match result.type_:
case ResultType.Selection:
u2f_method = result.get_value()
header = tr('Enable passwordless sudo?')
result_sudo = await Confirmation(
header=header,
allow_skip=True,
preset=False,
).show()
passwordless_sudo = result_sudo.item() == MenuItem.yes()
return U2FLoginConfiguration(
u2f_login_method=u2f_method,
passwordless_sudo=passwordless_sudo,
)
case ResultType.Skip:
return preset
case ResultType.Reset:
return None

View File

@ -1,111 +0,0 @@
import time
from collections.abc import Iterator
from pathlib import Path
from types import TracebackType
from typing import ClassVar, Self
from archinstall.lib.command import SysCommand, SysCommandWorker
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import error
class Boot:
_active_boot: ClassVar[Self | None] = None
def __init__(self, path: Path | str):
if isinstance(path, Path):
path = str(path)
self.path = path
self.container_name = 'archinstall'
self.session: SysCommandWorker | None = None
self.ready = False
def __enter__(self) -> Self:
if Boot._active_boot and Boot._active_boot.path != self.path:
raise KeyError('Archinstall only supports booting up one instance and another session is already active.')
if Boot._active_boot:
self.session = Boot._active_boot.session
self.ready = Boot._active_boot.ready
else:
# '-P' or --console=pipe could help us not having to do a bunch
# of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual.
self.session = SysCommandWorker(
[
'systemd-nspawn',
'-D',
self.path,
'--timezone=off',
'-b',
'--no-pager',
'--machine',
self.container_name,
]
)
if not self.ready and self.session:
while self.session.is_alive():
if b' login:' in self.session:
self.ready = True
break
Boot._active_boot = self
return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
# b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync.
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if exc_type is not None:
error(
str(exc_value),
f'The error above occurred in a temporary boot-up of the installation {self.path!r}',
)
shutdown = None
shutdown_exit_code: int | None = -1
try:
shutdown = SysCommand(f'systemd-run --machine={self.container_name} --pty shutdown now')
except SysCallError as err:
shutdown_exit_code = err.exit_code
if self.session:
while self.session.is_alive():
time.sleep(0.25)
if shutdown and shutdown.exit_code:
shutdown_exit_code = shutdown.exit_code
if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0):
Boot._active_boot = None
else:
session_exit_code = self.session.exit_code if self.session else -1
raise SysCallError(
f'Could not shut down temporary boot of {self.path!r}: {session_exit_code}/{shutdown_exit_code}',
exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])),
)
def __iter__(self) -> Iterator[bytes]:
if self.session:
yield from self.session
def __contains__(self, key: bytes) -> bool:
if self.session is None:
return False
return key in self.session
def is_alive(self) -> bool:
if self.session is None:
return False
return self.session.is_alive()
def SysCommand(self, cmd: list[str], *args, **kwargs) -> SysCommand: # type: ignore[no-untyped-def]
return SysCommand(['systemd-run', f'--machine={self.container_name}', '--pty', *cmd], *args, **kwargs)
def SysCommandWorker(self, cmd: list[str], *args, **kwargs) -> SysCommandWorker: # type: ignore[no-untyped-def]
return SysCommandWorker(['systemd-run', f'--machine={self.container_name}', '--pty', *cmd], *args, **kwargs)

View File

@ -1,281 +0,0 @@
import textwrap
from typing import override
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration, PlymouthTheme
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
def __init__(
self,
bootloader_conf: BootloaderConfiguration,
uefi: bool,
skip_boot: bool = False,
):
self._bootloader_conf = bootloader_conf
self._skip_boot = skip_boot
self._uefi = uefi
menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True)
super().__init__(
self._item_group,
config=self._bootloader_conf,
allow_reset=False,
)
def _define_menu_options(self) -> list[MenuItem]:
bootloader = self._bootloader_conf.bootloader
# UKI availability
uki_enabled = self._uefi and bootloader.has_uki_support()
if not uki_enabled:
self._bootloader_conf.uki = False
# Removable availability
removable_enabled = self._uefi and bootloader.has_removable_support()
if not removable_enabled:
self._bootloader_conf.removable = False
return [
MenuItem(
text=tr('Bootloader'),
action=self._select_bootloader,
value=self._bootloader_conf.bootloader,
preview_action=self._prev_bootloader,
mandatory=True,
key='bootloader',
),
MenuItem(
text=tr('Unified kernel images'),
action=self._select_uki,
value=self._bootloader_conf.uki,
preview_action=self._prev_uki,
key='uki',
enabled=uki_enabled,
),
MenuItem(
text=tr('Install to removable location'),
action=self._select_removable,
value=self._bootloader_conf.removable,
preview_action=self._prev_removable,
key='removable',
enabled=removable_enabled,
),
MenuItem(
text=tr('Plymouth'),
action=self._select_plymouth,
value=self._bootloader_conf.plymouth,
preview_action=self._prev_plymouth,
key='plymouth',
),
]
def _prev_bootloader(self, item: MenuItem) -> str | None:
if item.value:
return f'{tr("Bootloader")}: {item.value.value}'
return None
def _prev_uki(self, item: MenuItem) -> str | None:
uki_text = f'{tr("Unified kernel images")}'
if item.value:
return f'{uki_text}: {tr("Enabled")}'
else:
return f'{uki_text}: {tr("Disabled")}'
def _prev_removable(self, item: MenuItem) -> str | None:
if item.value:
return tr('Will install to /EFI/BOOT/ (removable location, safe default)')
return tr('Will install to custom location with NVRAM entry')
def _prev_plymouth(self, item: MenuItem) -> str | None:
if item.value:
return f'{tr("Plymouth")}: {item.value.value}'
return None
@override
async def show(self) -> BootloaderConfiguration:
_ = await super().show()
return self._bootloader_conf
async def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None:
bootloader = await select_bootloader(preset, self._uefi, self._skip_boot)
if bootloader:
# Update UKI option based on bootloader
uki_item = self._menu_item_group.find_by_key('uki')
if not self._uefi or not bootloader.has_uki_support():
uki_item.enabled = False
uki_item.value = False
self._bootloader_conf.uki = False
else:
uki_item.enabled = True
# Update removable option based on bootloader
removable_item = self._menu_item_group.find_by_key('removable')
if not self._uefi or not bootloader.has_removable_support():
removable_item.enabled = False
removable_item.value = False
self._bootloader_conf.removable = False
else:
if not removable_item.enabled:
removable_item.value = True
self._bootloader_conf.removable = True
removable_item.enabled = True
return bootloader
async def _select_plymouth(self, preset: PlymouthTheme | None) -> PlymouthTheme | None:
# Plymouth is purely cosmetic and a frequent source of boot breakage
# (notably with the NVIDIA driver and disk encryption), so confirm before
# enabling it. When it is already enabled the user is only changing the
# theme, so the warning is skipped.
if preset is None:
prompt = (
'[ansi_bright_yellow]'
+ tr('Plymouth adds a cosmetic boot splash but can cause boot problems on some setups:')
+ '\n\n'
+ tr('black screen with the NVIDIA driver')
+ '\n'
+ tr('hidden password prompt with disk encryption (LUKS)')
+ '\n\n'
+ tr('Would you like to enable it?')
+ '[/]\n'
)
result = await Confirmation(header=prompt, allow_skip=True, preset=False).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
if not result.get_value():
return preset
case ResultType.Reset:
raise ValueError('Unhandled result type')
return await select_plymouth_theme(preset)
async def _select_uki(self, preset: bool) -> bool:
prompt = tr('Would you like to use unified kernel images?') + '\n'
result = await Confirmation(header=prompt, allow_skip=True, preset=preset).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.item() == MenuItem.yes()
case ResultType.Reset:
raise ValueError('Unhandled result type')
async def _select_removable(self, preset: bool) -> bool:
prompt = (
tr('Would you like to install the bootloader to the default removable media search location?')
+ '\n\n'
+ tr('This installs the bootloader to /EFI/BOOT/BOOTX64.EFI (or similar) which is useful for:')
+ '\n\n'
+ tr('Firmware that does not properly support NVRAM boot entries like most MSI motherboards,')
+ '\n '
+ tr('most Apple Macs, many laptops...')
+ '\n'
+ tr('USB drives or other portable external media.')
+ '\n'
+ tr('Systems where you want the disk to be bootable on any computer.')
+ '\n\n'
+ tr(
textwrap.dedent(
"""\
If you do not know what this means, LEAVE THIS OPTION ENABLED, as it is the safe default.
It is suggested to disable this if none of the above apply, as it makes installing multiple
EFI bootloaders on the same disk easier, and it will not overwrite whatever bootloader
was previously installed at the default removable media search location, if any.
It may also make the installation more resilient in case of dual-booting with Windows,
as Windows is known to sometimes erase or replace the bootloader installed at the removable
location.
"""
)
)
+ '\n'
)
result = await Confirmation(
header=prompt,
allow_skip=True,
preset=preset,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
async def select_bootloader(
preset: Bootloader | None,
uefi: bool,
skip_boot: bool = False,
) -> Bootloader | None:
options = []
hidden_options = []
header = tr('Select bootloader to install')
default = Bootloader.get_default(uefi, skip_boot)
if not skip_boot:
hidden_options += [Bootloader.NO_BOOTLOADER]
if not uefi:
options += [Bootloader.Grub, Bootloader.Limine]
header += '\n' + tr('UEFI is not detected and some options are disabled')
else:
options += [b for b in Bootloader if b not in hidden_options]
items = [MenuItem(o.value, value=o) for o in options]
group = MenuItemGroup(items)
group.set_default_by_value(default)
group.set_focus_by_value(preset)
result = await Selection[Bootloader](
group,
header=header,
allow_skip=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
async def select_plymouth_theme(preset: PlymouthTheme | None = None) -> PlymouthTheme | None:
items = [MenuItem(t.value, value=t) for t in PlymouthTheme]
group = MenuItemGroup(items, sort_items=False)
group.set_focus_by_value(preset)
result = await Selection[PlymouthTheme](
group,
header=tr('Select Plymouth theme'),
allow_reset=True,
allow_skip=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return None
case ResultType.Selection:
return PlymouthTheme(result.get_value())

View File

@ -1,86 +0,0 @@
from dataclasses import dataclass
from enum import Enum, auto
from pathlib import Path
from archinstall.lib.hardware import SysInfo
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration
class BootloaderValidationFailureKind(Enum):
LimineNonFatBoot = auto()
LimineLayout = auto()
BootloaderRequiresUefi = auto()
EfistubNonFatBoot = auto()
@dataclass(frozen=True)
class BootloaderValidationFailure:
kind: BootloaderValidationFailureKind
description: str
def validate_bootloader_layout(
bootloader_config: BootloaderConfiguration | None,
disk_config: DiskLayoutConfiguration | None,
) -> BootloaderValidationFailure | None:
"""Validate bootloader configuration against disk layout.
Returns a failure with a human-readable description if the configuration
would produce an unbootable system, or None if it is valid.
"""
if not (bootloader_config and disk_config):
return None
bootloader = bootloader_config.bootloader
if bootloader == Bootloader.NO_BOOTLOADER:
return None
if bootloader.is_uefi_only() and not SysInfo.has_uefi():
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.BootloaderRequiresUefi,
description=f'{bootloader.value} requires a UEFI system.',
)
boot_part = next(
(p for m in disk_config.device_modifications if (p := m.get_boot_partition())),
None,
)
if bootloader == Bootloader.Efistub:
# The UEFI firmware reads the kernel directly from the boot partition,
# which must be FAT.
if boot_part and (boot_part.fs_type is None or not boot_part.fs_type.is_fat()):
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.EfistubNonFatBoot,
description='Efistub does not support booting with a non-FAT boot partition.',
)
if bootloader == Bootloader.Limine:
# Limine reads its config and kernels from the boot partition, which
# must be FAT.
if boot_part and (boot_part.fs_type is None or not boot_part.fs_type.is_fat()):
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.LimineNonFatBoot,
description='Limine does not support booting with a non-FAT boot partition.',
)
# When the ESP is the boot partition but mounted outside /boot and
# UKI is disabled, kernels end up on the root filesystem which
# Limine cannot access.
if not bootloader_config.uki:
efi_part = next(
(p for m in disk_config.device_modifications if (p := m.get_efi_partition())),
None,
)
if efi_part and efi_part == boot_part and efi_part.mountpoint != Path('/boot'):
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.LimineLayout,
description=(
f'Limine requires kernels on a FAT partition. The ESP is mounted at {efi_part.mountpoint}, '
'enable UKI or add a separate /boot partition to install Limine.'
),
)
return None

View File

@ -1,384 +0,0 @@
import os
import shlex
import stat
import subprocess
import sys
import time
from collections.abc import Iterator
from select import EPOLLHUP, EPOLLIN, epoll
from shutil import which
from types import TracebackType
from typing import Any, Self, override
from archinstall.lib.exceptions import RequirementError, SysCallError
from archinstall.lib.log import debug, error, logger
from archinstall.lib.utils.encoding import clear_vt100_escape_codes
class SysCommandWorker:
def __init__(
self,
cmd: str | list[str],
peek_output: bool | None = False,
environment_vars: dict[str, str] | None = None,
working_directory: str = './',
remove_vt100_escape_codes_from_lines: bool = True,
):
if isinstance(cmd, str):
cmd = shlex.split(cmd)
if cmd and not cmd[0].startswith(('/', './')): # Path() does not work well
cmd[0] = locate_binary(cmd[0])
self.cmd = cmd
self.peek_output = peek_output
# define the standard locale for command outputs. For now the C ascii one. Can be overridden
self.environment_vars = {'LC_ALL': 'C'}
if environment_vars:
self.environment_vars.update(environment_vars)
self.working_directory = working_directory
self.exit_code: int | None = None
self._trace_log = b''
self._trace_log_pos = 0
self.poll_object = epoll()
self.child_fd: int | None = None
self.started = False
self.ended = False
self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines
def __contains__(self, key: bytes) -> bool:
"""
Contains will also move the current buffert position forward.
This is to avoid re-checking the same data when looking for output.
"""
assert isinstance(key, bytes)
index = self._trace_log.find(key, self._trace_log_pos)
if index >= 0:
self._trace_log_pos += index + len(key)
return True
return False
def __iter__(self, *args: str, **kwargs: dict[str, Any]) -> Iterator[bytes]:
last_line = self._trace_log.rfind(b'\n')
lines = filter(None, self._trace_log[self._trace_log_pos : last_line].splitlines())
for line in lines:
if self.remove_vt100_escape_codes_from_lines:
line = clear_vt100_escape_codes(line)
yield line + b'\n'
self._trace_log_pos = last_line
@override
def __repr__(self) -> str:
self.make_sure_we_are_executing()
return str(self._trace_log)
@override
def __str__(self) -> str:
try:
return self._trace_log.decode('utf-8')
except UnicodeDecodeError:
return str(self._trace_log)
def __enter__(self) -> Self:
return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
# b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync.
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if self.child_fd:
try:
os.close(self.child_fd)
except Exception:
pass
if self.peek_output:
# To make sure any peaked output didn't leave us hanging
# on the same line we were on.
sys.stdout.write('\n')
sys.stdout.flush()
if exc_type is not None:
debug(str(exc_value))
if self.exit_code != 0:
raise SysCallError(
f'{self.cmd} exited with abnormal exit code [{self.exit_code}]: {str(self)[-500:]}',
self.exit_code,
worker_log=self._trace_log,
)
def is_alive(self) -> bool:
self.poll()
if self.started and not self.ended:
return True
return False
def write(self, data: bytes, line_ending: bool = True) -> int:
assert isinstance(data, bytes) # TODO: Maybe we can support str as well and encode it
self.make_sure_we_are_executing()
if self.child_fd:
return os.write(self.child_fd, data + (b'\n' if line_ending else b''))
return 0
def make_sure_we_are_executing(self) -> bool:
if not self.started:
return self.execute()
return True
def tell(self) -> int:
self.make_sure_we_are_executing()
return self._trace_log_pos
def seek(self, pos: int) -> None:
self.make_sure_we_are_executing()
# Safety check to ensure 0 < pos < len(tracelog)
self._trace_log_pos = min(max(0, pos), len(self._trace_log))
def peak(self, output: str | bytes) -> bool:
if self.peek_output:
if isinstance(output, bytes):
try:
output = output.decode('UTF-8')
except UnicodeDecodeError:
return False
_cmd_output(output)
sys.stdout.write(output)
sys.stdout.flush()
return True
def poll(self) -> None:
self.make_sure_we_are_executing()
if self.child_fd:
got_output = False
for _fileno, _event in self.poll_object.poll(0.1):
try:
output = os.read(self.child_fd, 8192)
got_output = True
self.peak(output)
self._trace_log += output
except OSError:
self.ended = True
break
if self.ended or (not got_output and not _pid_exists(self.pid)):
self.ended = True
try:
wait_status = os.waitpid(self.pid, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status)
except ChildProcessError:
try:
wait_status = os.waitpid(self.child_fd, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status)
except ChildProcessError:
self.exit_code = 1
def execute(self) -> bool:
import pty
if (old_dir := os.getcwd()) != self.working_directory:
os.chdir(str(self.working_directory))
# Note: If for any reason, we get a Python exception between here
# and until os.close(), the traceback will get locked inside
# stdout of the child_fd object. `os.read(self.child_fd, 8192)` is the
# only way to get the traceback without losing it.
self.pid, self.child_fd = pty.fork()
# https://stackoverflow.com/questions/4022600/python-pty-fork-how-does-it-work
if not self.pid:
_cmd_history(self.cmd)
try:
os.execve(self.cmd[0], list(self.cmd), {**os.environ, **self.environment_vars})
except FileNotFoundError:
error(f'{self.cmd[0]} does not exist.')
self.exit_code = 1
return False
else:
# Only parent process moves back to the original working directory
os.chdir(old_dir)
self.started = True
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
return True
def decode(self, encoding: str = 'UTF-8') -> str:
return self._trace_log.decode(encoding)
class SysCommand:
def __init__(
self,
cmd: str | list[str],
peek_output: bool | None = False,
environment_vars: dict[str, str] | None = None,
working_directory: str = './',
remove_vt100_escape_codes_from_lines: bool = True,
):
self.cmd = cmd
self.peek_output = peek_output
self.environment_vars = environment_vars
self.working_directory = working_directory
self.remove_vt100_escape_codes_from_lines = remove_vt100_escape_codes_from_lines
self.session: SysCommandWorker | None = None
self.create_session()
def __enter__(self) -> SysCommandWorker | None:
return self.session
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
# b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync.
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if exc_type is not None:
error(str(exc_value))
def __iter__(self, *args: list[Any], **kwargs: dict[str, Any]) -> Iterator[bytes]:
if self.session:
yield from self.session
def __getitem__(self, key: slice) -> bytes:
if not self.session:
raise KeyError('SysCommand() does not have an active session.')
elif type(key) is slice:
start = key.start or 0
end = key.stop or len(self.session._trace_log)
return self.session._trace_log[start:end]
else:
raise ValueError("SysCommand() doesn't have key & value pairs, only slices, SysCommand('ls')[:10] as an example.")
@override
def __repr__(self, *args: list[Any], **kwargs: dict[str, Any]) -> str:
return self.decode('UTF-8', errors='backslashreplace') or ''
def create_session(self) -> bool:
"""
Initiates a :ref:`SysCommandWorker` session in this class ``.session``.
It then proceeds to poll the process until it ends, after which it also
clears any printed output if ``.peek_output=True``.
"""
if self.session:
return True
with SysCommandWorker(
self.cmd,
peek_output=self.peek_output,
environment_vars=self.environment_vars,
remove_vt100_escape_codes_from_lines=self.remove_vt100_escape_codes_from_lines,
working_directory=self.working_directory,
) as session:
self.session = session
while not self.session.ended:
self.session.poll()
if self.peek_output:
sys.stdout.write('\n')
sys.stdout.flush()
return True
def decode(self, encoding: str = 'utf-8', errors: str = 'backslashreplace', strip: bool = True) -> str:
if not self.session:
raise ValueError('No session available to decode')
val = self.session._trace_log.decode(encoding, errors=errors)
if strip:
return val.strip()
return val
def output(self, remove_cr: bool = True) -> bytes:
if not self.session:
raise ValueError('No session available')
if remove_cr:
return self.session._trace_log.replace(b'\r\n', b'\n')
return self.session._trace_log
@property
def exit_code(self) -> int | None:
if self.session:
return self.session.exit_code
else:
return None
@property
def trace_log(self) -> bytes | None:
if self.session:
return self.session._trace_log
return None
def run(
cmd: list[str],
input_data: bytes | None = None,
) -> subprocess.CompletedProcess[bytes]:
_cmd_history(cmd)
return subprocess.run(
cmd,
input=input_data,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=True,
)
def locate_binary(name: str) -> str:
if path := which(name):
return path
raise RequirementError(f'Binary {name} does not exist.')
def _pid_exists(pid: int) -> bool:
try:
return any(subprocess.check_output(['ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip())
except subprocess.CalledProcessError:
return False
def _cmd_history(cmd: list[str]) -> None:
content = f'{time.time()} {cmd}\n'
_append_log('cmd_history.txt', content)
def _cmd_output(output: str) -> None:
_append_log('cmd_output.txt', output)
def _append_log(file: str, content: str) -> None:
path = logger.directory / file
change_perm = not path.exists()
try:
with path.open('a') as f:
f.write(content)
if change_perm:
path.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
except PermissionError, FileNotFoundError:
# If the file does not exist, ignore the error
pass

View File

@ -1,137 +1,122 @@
import readline import json
import logging
from pathlib import Path
from typing import Optional, Dict
from archinstall.lib.args import USER_CONFIG_FILE, USER_CREDS_FILE, ArchConfig from .storage import storage
from archinstall.lib.log import debug from .general import JSON, UNSAFE_JSON
from archinstall.lib.menu.helpers import Confirmation, Selection from .output import log
from archinstall.lib.menu.util import get_password, prompt_dir
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
async def confirm_config(config: ArchConfig) -> bool: class ConfigurationOutput:
header = f'{tr("The specified configuration will be applied")}. ' def __init__(self, config: Dict):
header += tr('Would you like to continue?') + '\n' """
Configuration output handler to parse the existing configuration data structure and prepare for output on the
console and for saving it to configuration files
group = MenuItemGroup.yes_no() :param config: A dictionary containing configurations (basically archinstall.arguments)
group.set_preview_for_all(lambda x: config.user_config_to_json()) :type config: Dict
"""
self._config = config
self._user_credentials = {}
self._disk_layout = None
self._user_config = {}
self._default_save_path = Path(storage.get('LOG_PATH', '.'))
self._user_config_file = 'user_configuration.json'
self._user_creds_file = "user_credentials.json"
self._disk_layout_file = "user_disk_layout.json"
result = await Confirmation( self._sensitive = ['!users', '!superusers', '!encryption-password']
group=group, self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run']
header=header,
allow_skip=False,
preset=True,
preview_location='bottom',
preview_header=tr('Configuration preview'),
).show()
if not result.get_value(): self._process_config()
return False
return True @property
def user_credentials_file(self):
return self._user_creds_file
@property
def user_configuration_file(self):
return self._user_config_file
async def save_config(config: ArchConfig) -> None: @property
def preview(item: MenuItem) -> str | None: def disk_layout_file(self):
match item.value: return self._disk_layout_file
case 'user_config':
serialized = config.user_config_to_json() def _process_config(self):
return f'{USER_CONFIG_FILE}\n{serialized}' for key in self._config:
case 'user_creds': if key in self._sensitive:
if maybe_serial := config.user_credentials_to_json(): self._user_credentials[key] = self._config[key]
return f'{USER_CREDS_FILE}\n{maybe_serial}' elif key == 'disk_layouts':
return tr('No configuration') self._disk_layout = self._config[key]
case 'all': elif key in self._ignore:
output = [str(USER_CONFIG_FILE)] pass
config.user_credentials_to_json() else:
output.append(str(USER_CREDS_FILE)) self._user_config[key] = self._config[key]
return '\n'.join(output)
def user_config_to_json(self) -> str:
return json.dumps({
'config_version': storage['__version__'], # Tells us what version was used to generate the config
**self._user_config, # __version__ will be overwritten by old version definition found in config
'version': storage['__version__']
}, indent=4, sort_keys=True, cls=JSON)
def disk_layout_to_json(self) -> Optional[str]:
if self._disk_layout:
return json.dumps(self._disk_layout, indent=4, sort_keys=True, cls=JSON)
return None return None
items = [ def user_credentials_to_json(self) -> Optional[str]:
MenuItem( if self._user_credentials:
tr('Save user configuration (including disk layout)'), return json.dumps(self._user_credentials, indent=4, sort_keys=True, cls=UNSAFE_JSON)
value='user_config', return None
preview_action=preview,
),
MenuItem(
tr('Save user credentials'),
value='user_creds',
preview_action=preview,
),
MenuItem(
tr('Save all'),
value='all',
preview_action=preview,
),
]
group = MenuItemGroup(items) def show(self):
result = await Selection[str]( print(_('\nThis is your chosen configuration:'))
group, log(" -- Chosen configuration --", level=logging.DEBUG)
allow_skip=True,
preview_location='right',
).show()
match result.type_: user_conig = self.user_config_to_json()
case ResultType.Skip: disk_layout = self.disk_layout_to_json()
return log(user_conig, level=logging.INFO)
case ResultType.Selection:
save_option = result.get_value()
case _:
raise ValueError('Unhandled return type')
readline.set_completer_delims('\t\n=') if disk_layout:
readline.parse_and_bind('tab: complete') log(disk_layout, level=logging.INFO)
dest_path = await prompt_dir( print()
tr('Enter a directory for the configuration(s) to be saved') + '\n',
allow_skip=True,
)
if not dest_path: def _is_valid_path(self, dest_path :Path) -> bool:
return if (not dest_path.exists()) or not (dest_path.is_dir()):
log(
header = tr('Do you want to save the configuration file(s) to {}?').format(dest_path) 'Destination directory {} does not exist or is not a directory,\n Configuration files can not be saved'.format(dest_path.resolve()),
fg="yellow"
save_result = await Confirmation(
header=header,
allow_skip=False,
preset=True,
).show()
match save_result.type_:
case ResultType.Selection:
if not save_result.get_value():
return
case _:
return
debug(f'Saving configuration files to {dest_path.absolute()}')
header = tr('Do you want to encrypt the user_credentials.json file?')
enc_result = await Confirmation(
header=header,
allow_skip=False,
preset=False,
).show()
enc_password: str | None = None
if enc_result.type_ == ResultType.Selection:
if enc_result.get_value():
password = await get_password(
header=tr('Credentials file encryption password'),
allow_skip=True,
) )
return False
return True
if password: def save_user_config(self, dest_path :Path = None):
enc_password = password.plaintext if self._is_valid_path(dest_path):
with open(dest_path / self._user_config_file, 'w') as config_file:
config_file.write(self.user_config_to_json())
match save_option: def save_user_creds(self, dest_path :Path = None):
case 'user_config': if self._is_valid_path(dest_path):
config.save_user_config(dest_path) if user_creds := self.user_credentials_to_json():
case 'user_creds': target = dest_path / self._user_creds_file
config.save_user_creds(dest_path, password=enc_password) with open(target, 'w') as config_file:
case 'all': config_file.write(user_creds)
config.save(dest_path, creds=True, password=enc_password)
def save_disk_layout(self, dest_path :Path = None):
if self._is_valid_path(dest_path):
if disk_layout := self.disk_layout_to_json():
target = dest_path / self._disk_layout_file
with target.open('w') as config_file:
config_file.write(disk_layout)
def save(self, dest_path :Path = None):
if not dest_path:
dest_path = self._default_save_path
if self._is_valid_path(dest_path):
self.save_user_config(dest_path)
self.save_user_creds(dest_path)
self.save_disk_layout(dest_path)

View File

@ -1,125 +0,0 @@
import base64
import ctypes
import os
from pathlib import Path
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
from archinstall.lib.log import debug
libcrypt = ctypes.CDLL('libcrypt.so')
libcrypt.crypt.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
libcrypt.crypt.restype = ctypes.c_char_p
libcrypt.crypt_gensalt.argtypes = [ctypes.c_char_p, ctypes.c_ulong, ctypes.c_char_p, ctypes.c_int]
libcrypt.crypt_gensalt.restype = ctypes.c_char_p
LOGIN_DEFS = Path('/etc/login.defs')
def _search_login_defs(key: str) -> str | None:
defs = LOGIN_DEFS.read_text()
for line in defs.split('\n'):
line = line.strip()
if line.startswith('#'):
continue
if line.startswith(key):
value = line.split(' ')[1]
return value
return None
def crypt_gen_salt(prefix: str | bytes, rounds: int) -> bytes:
if isinstance(prefix, str):
prefix = prefix.encode('utf-8')
setting = libcrypt.crypt_gensalt(prefix, rounds, None, 0)
if setting is None:
raise ValueError(f'crypt_gensalt() returned NULL for prefix {prefix!r} and rounds {rounds}')
return setting
def crypt_yescrypt(plaintext: str) -> str:
"""
By default chpasswd in Arch uses PAM to hash the password with crypt_yescrypt
the PAM code https://github.com/linux-pam/linux-pam/blob/master/modules/pam_unix/support.c
shows that the hashing rounds are determined from YESCRYPT_COST_FACTOR in /etc/login.defs
If no value was specified (or commented out) a default of 5 is chosen
"""
value = _search_login_defs('YESCRYPT_COST_FACTOR')
if value is not None:
rounds = int(value)
if rounds < 3:
rounds = 3
elif rounds > 11:
rounds = 11
else:
rounds = 5
debug(f'Creating yescrypt hash with rounds {rounds}')
enc_plaintext = plaintext.encode('utf-8')
salt = crypt_gen_salt('$y$', rounds)
crypt_hash = libcrypt.crypt(enc_plaintext, salt)
if crypt_hash is None:
raise ValueError('crypt() returned NULL')
return crypt_hash.decode('utf-8')
def _get_fernet(salt: bytes, password: str) -> Fernet:
# https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#argon2id
kdf = Argon2id(
salt=salt,
length=32,
iterations=1,
lanes=4,
memory_cost=64 * 1024,
ad=None,
secret=None,
)
key = base64.urlsafe_b64encode(
kdf.derive(
password.encode('utf-8'),
),
)
return Fernet(key)
def encrypt(password: str, data: str) -> str:
salt = os.urandom(16)
f = _get_fernet(salt, password)
token = f.encrypt(data.encode('utf-8'))
encoded_token = base64.urlsafe_b64encode(token).decode('utf-8')
encoded_salt = base64.urlsafe_b64encode(salt).decode('utf-8')
return f'$argon2id${encoded_salt}${encoded_token}'
def decrypt(data: str, password: str) -> str:
_, algo, encoded_salt, encoded_token = data.split('$')
salt = base64.urlsafe_b64decode(encoded_salt)
token = base64.urlsafe_b64decode(encoded_token)
if algo != 'argon2id':
raise ValueError(f'Unsupported algorithm {algo!r}')
f = _get_fernet(salt, password)
try:
decrypted = f.decrypt(token)
except InvalidToken:
raise ValueError('Invalid password')
return decrypted.decode('utf-8')

View File

@ -0,0 +1,7 @@
from .btrfs import *
from .helpers import *
from .blockdevice import BlockDevice
from .filesystem import Filesystem, MBR, GPT
from .partition import *
from .user_guides import *
from .validators import *

View File

@ -0,0 +1,265 @@
from __future__ import annotations
import os
import json
import logging
import time
from typing import Optional, Dict, Any, Iterator, Tuple, List, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from .partition import Partition
from ..exceptions import DiskError, SysCallError
from ..output import log
from ..general import SysCommand
from ..storage import storage
class BlockDevice:
def __init__(self, path :str, info :Optional[Dict[str, Any]] = None):
if not info:
from .helpers import all_blockdevices
# If we don't give any information, we need to auto-fill it.
# Otherwise any subsequent usage will break.
info = all_blockdevices(partitions=False)[path].info
self.path = path
self.info = info
self.keep_partitions = True
self.part_cache = {}
# TODO: Currently disk encryption is a BIT misleading.
# It's actually partition-encryption, but for future-proofing this
# I'm placing the encryption password on a BlockDevice level.
def __repr__(self, *args :str, **kwargs :str) -> str:
return f"BlockDevice({self.device_or_backfile}, size={self.size}GB, free_space={'+'.join(part[2] for part in self.free_space)}, bus_type={self.bus_type})"
def __iter__(self) -> Iterator[Partition]:
for partition in self.partitions:
yield self.partitions[partition]
def __getitem__(self, key :str, *args :str, **kwargs :str) -> Any:
if hasattr(self, key):
return getattr(self, key)
elif key not in self.info:
raise KeyError(f'{self} does not contain information: "{key}"')
return self.info[key]
def __len__(self) -> int:
return len(self.partitions)
def __lt__(self, left_comparitor :'BlockDevice') -> bool:
return self.path < left_comparitor.path
def json(self) -> str:
"""
json() has precedence over __dump__, so this is a way
to give less/partial information for user readability.
"""
return self.path
def __dump__(self) -> Dict[str, Dict[str, Any]]:
return {
self.path : {
'partuuid' : self.uuid,
'wipe' : self.info.get('wipe', None),
'partitions' : [part.__dump__() for part in self.partitions.values()]
}
}
@property
def partition_type(self) -> str:
output = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.path}").decode('UTF-8'))
for device in output['blockdevices']:
return device['pttype']
@property
def device_or_backfile(self) -> str:
"""
Returns the actual device-endpoint of the BlockDevice.
If it's a loop-back-device it returns the back-file,
For other types it return self.device
"""
if self.info.get('type') == 'loop':
return self.info['back-file']
else:
return self.device
@property
def mountpoint(self) -> None:
"""
A dummy function to enable transparent comparisons of mountpoints.
As blockdevices can't be mounted directly, this will always be None
"""
return None
@property
def device(self) -> str:
"""
Returns the device file of the BlockDevice.
If it's a loop-back-device it returns the /dev/X device,
If it's a ATA-drive it returns the /dev/X device
And if it's a crypto-device it returns the parent device
"""
if "DEVTYPE" not in self.info:
raise DiskError(f'Could not locate backplane info for "{self.path}"')
if self.info['DEVTYPE'] in ['disk','loop']:
return self.path
elif self.info['DEVTYPE'][:4] == 'raid':
# This should catch /dev/md## raid devices
return self.path
elif self.info['DEVTYPE'] == 'crypt':
if 'pkname' not in self.info:
raise DiskError(f'A crypt device ({self.path}) without a parent kernel device name.')
return f"/dev/{self.info['pkname']}"
else:
log(f"Unknown blockdevice type for {self.path}: {self.info['DEVTYPE']}", level=logging.DEBUG)
# if not stat.S_ISBLK(os.stat(full_path).st_mode):
# raise DiskError(f'Selected disk "{full_path}" is not a block device.')
@property
def partitions(self) -> Dict[str, Partition]:
from .filesystem import Partition
self.partprobe()
result = SysCommand(['/usr/bin/lsblk', '-J', self.path])
if b'not a block device' in result:
raise DiskError(f'Can not read partitions off something that isn\'t a block device: {self.path}')
if not result[:1] == b'{':
raise DiskError('Error getting JSON output from:', f'/usr/bin/lsblk -J {self.path}')
r = json.loads(result.decode('UTF-8'))
if len(r['blockdevices']) and 'children' in r['blockdevices'][0]:
root_path = f"/dev/{r['blockdevices'][0]['name']}"
for part in r['blockdevices'][0]['children']:
part_id = part['name'][len(os.path.basename(self.path)):]
if part_id not in self.part_cache:
# TODO: Force over-write even if in cache?
if part_id not in self.part_cache or self.part_cache[part_id].size != part['size']:
self.part_cache[part_id] = Partition(root_path + part_id, block_device=self, part_id=part_id)
return {k: self.part_cache[k] for k in sorted(self.part_cache)}
@property
def partition(self) -> Partition:
all_partitions = self.partitions
return [all_partitions[k] for k in all_partitions]
@property
def partition_table_type(self) -> int:
# TODO: Don't hardcode :)
# Remove if we don't use this function anywhere
from .filesystem import GPT
return GPT
@property
def uuid(self) -> str:
log('BlockDevice().uuid is untested!', level=logging.WARNING, fg='yellow')
"""
Returns the disk UUID as returned by lsblk.
This is more reliable than relying on /dev/disk/by-partuuid as
it doesn't seam to be able to detect md raid partitions.
"""
return SysCommand(f'blkid -s PTUUID -o value {self.path}').decode('UTF-8')
@property
def size(self) -> float:
from .helpers import convert_size_to_gb
output = json.loads(SysCommand(f"lsblk --json -b -o+SIZE {self.path}").decode('UTF-8'))
for device in output['blockdevices']:
return convert_size_to_gb(device['size'])
@property
def bus_type(self) -> str:
output = json.loads(SysCommand(f"lsblk --json -o+ROTA,TRAN {self.path}").decode('UTF-8'))
for device in output['blockdevices']:
return device['tran']
@property
def spinning(self) -> bool:
output = json.loads(SysCommand(f"lsblk --json -o+ROTA,TRAN {self.path}").decode('UTF-8'))
for device in output['blockdevices']:
return device['rota'] is True
@property
def free_space(self) -> Tuple[str, str, str]:
# NOTE: parted -s will default to `cancel` on prompt, skipping any partition
# that is "outside" the disk. in /dev/sr0 this is usually the case with Archiso,
# so the free will ignore the ESP partition and just give the "free" space.
# Doesn't harm us, but worth noting in case something weird happens.
try:
for line in SysCommand(f"parted -s --machine {self.path} print free"):
if 'free' in (free_space := line.decode('UTF-8')):
_, start, end, size, *_ = free_space.strip('\r\n;').split(':')
yield (start, end, size)
except SysCallError as error:
log(f"Could not get free space on {self.path}: {error}", level=logging.DEBUG)
@property
def largest_free_space(self) -> List[str]:
info = []
for space_info in self.free_space:
if not info:
info = space_info
else:
# [-1] = size
if space_info[-1] > info[-1]:
info = space_info
return info
@property
def first_free_sector(self) -> str:
if info := self.largest_free_space:
start = info[0]
else:
start = '512MB'
return start
@property
def first_end_sector(self) -> str:
if info := self.largest_free_space:
end = info[1]
else:
end = f"{self.size}GB"
return end
def partprobe(self) -> bool:
return SysCommand(['partprobe', self.path]).exit_code == 0
def has_partitions(self) -> int:
return len(self.partitions)
def has_mount_point(self, mountpoint :str) -> bool:
for partition in self.partitions:
if self.partitions[partition].mountpoint == mountpoint:
return True
return False
def flush_cache(self) -> None:
self.part_cache = {}
def get_partition(self, uuid :str) -> Partition:
count = 0
while count < 5:
for partition_uuid, partition in self.partitions.items():
if partition.uuid.lower() == uuid.lower():
return partition
else:
log(f"uuid {uuid} not found. Waiting for {count +1} time",level=logging.DEBUG)
time.sleep(float(storage['arguments'].get('disk-sleep', 0.2)))
count += 1
else:
log(f"Could not find {uuid} in disk after 5 retries",level=logging.INFO)
print(f"Cache: {self.part_cache}")
print(f"Partitions: {self.partitions.items()}")
print(f"UUID: {[uuid]}")
raise DiskError(f"New partition {uuid} never showed up after adding new partition on {self}")

View File

@ -0,0 +1,232 @@
from __future__ import annotations
import pathlib
import glob
import logging
import re
from typing import Union, Dict, TYPE_CHECKING, Any, Iterator
from dataclasses import dataclass
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from ..installer import Installer
from .helpers import get_mount_info
from ..exceptions import DiskError
from ..general import SysCommand
from ..output import log
from ..exceptions import SysCallError
@dataclass
class BtrfsSubvolume:
target :str
source :str
fstype :str
name :str
options :str
root :bool = False
def get_subvolumes_from_findmnt(struct :Dict[str, Any], index=0) -> Iterator[BtrfsSubvolume]:
if '[' in struct['source']:
subvolume = re.findall(r'\[.*?\]', struct['source'])[0][1:-1]
struct['source'] = struct['source'].replace(f"[{subvolume}]", "")
yield BtrfsSubvolume(
target=struct['target'],
source=struct['source'],
fstype=struct['fstype'],
name=subvolume,
options=struct['options'],
root=index == 0
)
index += 1
for child in struct.get('children', []):
for item in get_subvolumes_from_findmnt(child, index=index):
yield item
index += 1
def get_subvolume_info(path :pathlib.Path) -> Dict[str, Any]:
try:
output = SysCommand(f"btrfs subvol show {path}").decode()
except SysCallError as error:
print('Error:', error)
result = {}
for line in output.replace('\r\n', '\n').split('\n'):
if ':' in line:
key, val = line.replace('\t', '').split(':', 1)
result[key.strip().lower().replace(' ', '_')] = val.strip()
return result
def mount_subvolume(installation :Installer, subvolume_location :Union[pathlib.Path, str], force=False) -> bool:
"""
This function uses mount to mount a subvolume on a given device, at a given location with a given subvolume name.
@installation: archinstall.Installer instance
@subvolume_location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot
@force: overrides the check for weither or not the subvolume mountpoint is empty or not
This function is DEPRECATED. you can get the same result creating a partition dict like any other partition, and using the standard mount procedure.
Only change partition['device_instance'].path with the apropriate bind name: real_partition_path[/subvolume_name]
"""
log("[Deprecated] function btrfs.mount_subvolume is deprecated. See code for alternatives",fg="yellow",level=logging.WARNING)
installation_mountpoint = installation.target
if type(installation_mountpoint) == str:
installation_mountpoint = pathlib.Path(installation_mountpoint)
# Set up the required physical structure
if type(subvolume_location) == str:
subvolume_location = pathlib.Path(subvolume_location)
target = installation_mountpoint / subvolume_location.relative_to(subvolume_location.anchor)
if not target.exists():
target.mkdir(parents=True)
if glob.glob(str(target / '*')) and force is False:
raise DiskError(f"Cannot mount subvolume to {target} because it contains data (non-empty folder target)")
log(f"Mounting {target} as a subvolume", level=logging.INFO)
# Mount the logical volume to the physical structure
mount_information, mountpoint_device_real_path = get_mount_info(target, traverse=True, return_real_path=True)
if mountpoint_device_real_path == str(target):
log(f"Unmounting non-subvolume {mount_information['source']} previously mounted at {target}")
SysCommand(f"umount {mount_information['source']}")
return SysCommand(f"mount {mount_information['source']} {target} -o subvol=@{subvolume_location}").exit_code == 0
def create_subvolume(installation :Installer, subvolume_location :Union[pathlib.Path, str]) -> bool:
"""
This function uses btrfs to create a subvolume.
@installation: archinstall.Installer instance
@subvolume_location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot
"""
installation_mountpoint = installation.target
if type(installation_mountpoint) == str:
installation_mountpoint = pathlib.Path(installation_mountpoint)
# Set up the required physical structure
if type(subvolume_location) == str:
subvolume_location = pathlib.Path(subvolume_location)
target = installation_mountpoint / subvolume_location.relative_to(subvolume_location.anchor)
# Difference from mount_subvolume:
# We only check if the parent exists, since we'll run in to "target path already exists" otherwise
if not target.parent.exists():
target.parent.mkdir(parents=True)
if glob.glob(str(target / '*')):
raise DiskError(f"Cannot create subvolume at {target} because it contains data (non-empty folder target)")
# Remove the target if it exists
if target.exists():
target.rmdir()
log(f"Creating a subvolume on {target}", level=logging.INFO)
if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0:
raise DiskError(f"Could not create a subvolume at {target}: {cmd}")
def _has_option(option :str,options :list) -> bool:
""" auxiliary routine to check if an option is present in a list.
we check if the string appears in one of the options, 'cause it can appear in severl forms (option, option=val,...)
"""
if not options:
return False
for item in options:
if option in item:
return True
return False
def manage_btrfs_subvolumes(installation :Installer,
partition :Dict[str, str],) -> list:
from copy import deepcopy
""" we do the magic with subvolumes in a centralized place
parameters:
* the installation object
* the partition dictionary entry which represents the physical partition
returns
* mountpoinst, the list which contains all the "new" partititon to be mounted
We expect the partition has been mounted as / , and it to be unmounted after the processing
Then we create all the subvolumes inside btrfs as demand
We clone then, both the partition dictionary and the object inside it and adapt it to the subvolume needs
Then we return a list of "new" partitions to be processed as "normal" partitions
# TODO For encrypted devices we need some special processing prior to it
"""
# We process each of the pairs <subvolume name: mount point | None | mount info dict>
# th mount info dict has an entry for the path of the mountpoint (named 'mountpoint') and 'options' which is a list
# of mount options (or similar used by brtfs)
mountpoints = []
subvolumes = partition['btrfs']['subvolumes']
for name, right_hand in subvolumes.items():
try:
# we normalize the subvolume name (getting rid of slash at the start if exists. In our implemenation has no semantic load - every subvolume is created from the top of the hierarchy- and simplifies its further use
if name.startswith('/'):
name = name[1:]
# renormalize the right hand.
location = None
subvol_options = []
# no contents, so it is not to be mounted
if not right_hand:
location = None
# just a string. per backward compatibility the mount point
elif isinstance(right_hand,str):
location = right_hand
# a dict. two elements 'mountpoint' (obvious) and and a mount options list ¿?
elif isinstance(right_hand,dict):
location = right_hand.get('mountpoint',None)
subvol_options = right_hand.get('options',[])
# we create the subvolume
create_subvolume(installation,name)
# Make the nodatacow processing now
# It will be the main cause of creation of subvolumes which are not to be mounted
# it is not an options which can be established by subvolume (but for whole file systems), and can be
# set up via a simple attribute change in a directory (if empty). And here the directories are brand new
if 'nodatacow' in subvol_options:
if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0:
raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}")
# entry is deleted so nodatacow doesn't propagate to the mount options
del subvol_options[subvol_options.index('nodatacow')]
# Make the compress processing now
# it is not an options which can be established by subvolume (but for whole file systems), and can be
# set up via a simple attribute change in a directory (if empty). And here the directories are brand new
# in this way only zstd compression is activaded
# TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated
if 'compress' in subvol_options:
if not _has_option('compress',partition.get('filesystem',{}).get('mount_options',[])):
if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0:
raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}")
# entry is deleted so compress doesn't propagate to the mount options
del subvol_options[subvol_options.index('compress')]
# END compress processing.
# we do not mount if THE basic partition will be mounted or if we exclude explicitly this subvolume
if not partition['mountpoint'] and location is not None:
# we begin to create a fake partition entry. First we copy the original -the one that corresponds to
# the primary partition. We make a deepcopy to avoid altering the original content in any case
fake_partition = deepcopy(partition)
# we start to modify entries in the "fake partition" to match the needs of the subvolumes
# to avoid any chance of entering in a loop (not expected) we delete the list of subvolumes in the copy
del fake_partition['btrfs']
fake_partition['encrypted'] = False
fake_partition['generate-encryption-key-file'] = False
# Mount destination. As of now the right hand part
fake_partition['mountpoint'] = location
# we load the name in an attribute called subvolume, but i think it is not needed anymore, 'cause the mount logic uses a different path.
fake_partition['subvolume'] = name
# here we add the special mount options for the subvolume, if any.
# if the original partition['options'] is not a list might give trouble
if fake_partition.get('filesystem',{}).get('mount_options',[]):
fake_partition['filesystem']['mount_options'].extend(subvol_options)
else:
fake_partition['filesystem']['mount_options'] = subvol_options
# Here comes the most exotic part. The dictionary attribute 'device_instance' contains an instance of Partition. This instance will be queried along the mount process at the installer.
# As the rest will query there the path of the "partition" to be mounted, we feed it with the bind name needed to mount subvolumes
# As we made a deepcopy we have a fresh instance of this object we can manipulate problemless
fake_partition['device_instance'].path = f"{partition['device_instance'].path}[/{name}]"
# Well, now that this "fake partition" is ready, we add it to the list of the ones which are to be mounted,
# as "normal" ones
mountpoints.append(fake_partition)
except Exception as e:
raise e
return mountpoints

View File

@ -1,636 +0,0 @@
import logging
import os
import platform
from pathlib import Path
from parted import Device, Disk, DiskException, FileSystem, Geometry, IOException, Partition, PartitionException, freshDisk, getAllDevices, getDevice, newDisk
from archinstall.lib.command import SysCommand
from archinstall.lib.disk.luks import Luks2, unlock_luks2_dev
from archinstall.lib.disk.utils import (
find_lsblk_info,
get_all_lsblk_info,
get_lsblk_info,
linux_root_guid,
mount,
udev_sync,
umount,
)
from archinstall.lib.exceptions import DiskError, SysCallError, UnknownFilesystemFormat
from archinstall.lib.log import debug, error, info, log
from archinstall.lib.models.device import (
DEFAULT_ITER_TIME,
BDevice,
BtrfsMountOption,
DeviceModification,
DiskEncryption,
FilesystemType,
LsblkInfo,
ModificationStatus,
PartitionFlag,
PartitionModification,
PartitionTable,
SubvolumeModification,
Unit,
_BtrfsSubvolumeInfo,
_DeviceInfo,
_PartitionInfo,
)
from archinstall.lib.models.users import Password
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
class DeviceHandler:
_TMP_BTRFS_MOUNT = Path('/mnt/arch_btrfs')
def __init__(self) -> None:
self._devices: dict[Path, BDevice] = {}
self._partition_table = PartitionTable.default()
self.load_devices()
@property
def devices(self) -> list[BDevice]:
return list(self._devices.values())
@property
def partition_table(self) -> PartitionTable:
return self._partition_table
def load_devices(self) -> None:
block_devices = {}
udev_sync()
all_lsblk_info = get_all_lsblk_info()
devices = getAllDevices()
devices.extend(self.get_loop_devices())
for device in devices:
dev_lsblk_info = find_lsblk_info(device.path, all_lsblk_info)
if not dev_lsblk_info:
debug(f'Device lsblk info not found: {device.path}')
continue
if dev_lsblk_info.type == 'rom':
continue
# exclude archiso loop device
if dev_lsblk_info.mountpoint == ARCHISO_MOUNTPOINT:
continue
try:
if dev_lsblk_info.pttype:
disk = newDisk(device)
else:
disk = freshDisk(device, self.partition_table.value)
except DiskException as err:
debug(f'Unable to get disk from {device.path}: {err}')
continue
device_info = _DeviceInfo.from_disk(disk)
partition_infos = []
for partition in disk.partitions:
lsblk_info = find_lsblk_info(partition.path, dev_lsblk_info.children)
if not lsblk_info:
debug(f'Partition lsblk info not found: {partition.path}')
continue
fs_type = self._determine_fs_type(partition, lsblk_info)
subvol_infos = []
if fs_type == FilesystemType.BTRFS:
subvol_infos = self.get_btrfs_info(partition.path, lsblk_info)
partition_infos.append(
_PartitionInfo.from_partition(
partition,
lsblk_info,
fs_type,
subvol_infos,
),
)
block_device = BDevice(disk, device_info, partition_infos)
block_devices[block_device.device_info.path] = block_device
self._devices = block_devices
@staticmethod
def get_loop_devices() -> list[Device]:
devices = []
try:
loop_devices = SysCommand(['losetup', '-a'])
except SysCallError as err:
debug(f'Failed to get loop devices: {err}')
else:
for ld_info in str(loop_devices).splitlines():
try:
loop_device_path, _ = ld_info.split(':', maxsplit=1)
except ValueError:
continue
try:
loop_device = getDevice(loop_device_path)
except IOException as err:
debug(f'Failed to get loop device: {err}')
else:
devices.append(loop_device)
return devices
def _determine_fs_type(
self,
partition: Partition,
lsblk_info: LsblkInfo | None = None,
) -> FilesystemType | None:
try:
if partition.fileSystem:
if partition.fileSystem.type == FilesystemType.LINUX_SWAP.parted_value:
return FilesystemType.LINUX_SWAP
return FilesystemType(partition.fileSystem.type)
elif lsblk_info is not None:
return FilesystemType(lsblk_info.fstype) if lsblk_info.fstype else None
return None
except ValueError:
debug(f'Could not determine the filesystem: {partition.fileSystem}')
return None
def get_device(self, path: Path) -> BDevice | None:
return self._devices.get(path, None)
def get_device_by_partition_path(self, partition_path: Path) -> BDevice | None:
partition = self.find_partition(partition_path)
if partition:
device: Device = partition.disk.device
return self.get_device(Path(device.path))
return None
def find_partition(self, path: Path) -> _PartitionInfo | None:
for device in self._devices.values():
part = next(filter(lambda x: str(x.path) == str(path), device.partition_infos), None)
if part is not None:
return part
return None
def get_uuid_for_path(self, path: Path) -> str | None:
partition = self.find_partition(path)
return partition.partuuid if partition else None
def get_btrfs_info(
self,
dev_path: Path,
lsblk_info: LsblkInfo | None = None,
) -> list[_BtrfsSubvolumeInfo]:
if not lsblk_info:
lsblk_info = get_lsblk_info(dev_path)
subvol_infos: list[_BtrfsSubvolumeInfo] = []
if not lsblk_info.mountpoint:
mount(dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
mountpoint = self._TMP_BTRFS_MOUNT
else:
# when multiple subvolumes are mounted then the lsblk output may look like
# "mountpoint": "/mnt/archinstall/var/log"
# "mountpoints": ["/mnt/archinstall/var/log", "/mnt/archinstall/home", ..]
# so we'll determine the minimum common path and assume that's the root
try:
common_path = os.path.commonpath(lsblk_info.mountpoints)
except ValueError:
return subvol_infos
mountpoint = Path(common_path)
try:
result = SysCommand(f'btrfs subvolume list {mountpoint}').decode()
except SysCallError as err:
debug(f'Failed to read btrfs subvolume information: {err}')
return subvol_infos
# It is assumed that lsblk will contain the fields as
# "mountpoints": ["/mnt/archinstall/log", "/mnt/archinstall/home", "/mnt/archinstall", ...]
# "fsroots": ["/@log", "/@home", "/@"...]
# we'll thereby map the fsroot, which are the mounted filesystem roots
# to the corresponding mountpoints
btrfs_subvol_info = dict(zip(lsblk_info.fsroots, lsblk_info.mountpoints))
# ID 256 gen 16 top level 5 path @
for line in result.splitlines():
# expected output format:
# ID 257 gen 8 top level 5 path @home
name = Path(line.split(' ')[-1])
sub_vol_mountpoint = btrfs_subvol_info.get('/' / name, None)
subvol_infos.append(_BtrfsSubvolumeInfo(name, sub_vol_mountpoint))
if not lsblk_info.mountpoint:
umount(dev_path)
return subvol_infos
def format(
self,
fs_type: FilesystemType,
path: Path,
additional_parted_options: list[str] = [],
) -> None:
mkfs_type = fs_type.value
command = None
options = []
match fs_type:
case FilesystemType.BTRFS | FilesystemType.XFS:
# Force overwrite
options.append('-f')
case FilesystemType.F2FS:
options.append('-f')
options.extend(('-O', 'extra_attr'))
case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4:
# Force create
options.append('-F')
case _ if fs_type.is_fat():
mkfs_type = 'fat'
# Set FAT size
options.extend(('-F', fs_type.value.removeprefix(mkfs_type)))
case FilesystemType.LINUX_SWAP:
command = 'mkswap'
case _:
raise UnknownFilesystemFormat(f'Filetype "{fs_type.value}" is not supported')
if not command:
command = f'mkfs.{mkfs_type}'
cmd = [command, *options, *additional_parted_options, str(path)]
debug('Formatting filesystem:', ' '.join(cmd))
try:
SysCommand(cmd)
except SysCallError as err:
msg = f'Could not format {path} with {fs_type.value}: {err.message}'
error(msg)
raise DiskError(msg) from err
def encrypt(
self,
dev_path: Path,
mapper_name: str | None,
enc_password: Password | None,
lock_after_create: bool = True,
iter_time: int = DEFAULT_ITER_TIME,
) -> Luks2:
luks_handler = Luks2(
dev_path,
mapper_name=mapper_name,
password=enc_password,
)
key_file = luks_handler.encrypt(iter_time=iter_time)
udev_sync()
luks_handler.unlock(key_file=key_file)
if not luks_handler.mapper_dev:
raise DiskError('Failed to unlock luks device')
if lock_after_create:
debug(f'luks2 locking device: {dev_path}')
luks_handler.lock()
return luks_handler
def format_encrypted(
self,
dev_path: Path,
mapper_name: str | None,
fs_type: FilesystemType,
enc_conf: DiskEncryption,
) -> None:
if not enc_conf.encryption_password:
raise ValueError('No encryption password provided')
luks_handler = Luks2(
dev_path,
mapper_name=mapper_name,
password=enc_conf.encryption_password,
)
key_file = luks_handler.encrypt(iter_time=enc_conf.iter_time)
udev_sync()
luks_handler.unlock(key_file=key_file)
if not luks_handler.mapper_dev:
raise DiskError('Failed to unlock luks device')
info(f'luks2 formatting mapper dev: {luks_handler.mapper_dev}')
self.format(fs_type, luks_handler.mapper_dev)
info(f'luks2 locking device: {dev_path}')
luks_handler.lock()
def _setup_partition(
self,
part_mod: PartitionModification,
block_device: BDevice,
disk: Disk,
requires_delete: bool,
arch: str | None = None,
) -> None:
# when we require a delete and the partition to be (re)created
# already exists then we have to delete it first
if requires_delete and part_mod.status in [ModificationStatus.MODIFY, ModificationStatus.DELETE]:
info(f'Delete existing partition: {part_mod.safe_dev_path}')
part_info = self.find_partition(part_mod.safe_dev_path)
if not part_info:
raise DiskError(f'No partition for dev path found: {part_mod.safe_dev_path}')
disk.deletePartition(part_info.partition)
if part_mod.status == ModificationStatus.DELETE:
return
start_sector = part_mod.start.convert(
Unit.sectors,
block_device.device_info.sector_size,
)
length_sector = part_mod.length.convert(
Unit.sectors,
block_device.device_info.sector_size,
)
geometry = Geometry(
device=block_device.disk.device,
start=start_sector.value,
length=length_sector.value,
)
fs_value = part_mod.safe_fs_type.parted_value
filesystem = FileSystem(type=fs_value, geometry=geometry)
partition = Partition(
disk=disk,
type=part_mod.type.get_partition_code(),
fs=filesystem,
geometry=geometry,
)
for flag in part_mod.flags:
partition.setFlag(flag.flag_id)
debug(f'\tType: {part_mod.type.value}')
debug(f'\tFilesystem: {fs_value}')
debug(f'\tGeometry: {start_sector.value} start sector, {length_sector.value} length')
try:
disk.addPartition(partition=partition, constraint=disk.device.optimalAlignedConstraint)
except PartitionException as ex:
raise DiskError(f'Unable to add partition, most likely due to overlapping sectors: {ex}') from ex
if disk.type == PartitionTable.GPT.value:
if part_mod.is_root():
partition.type_uuid = linux_root_guid(arch).bytes
elif PartitionFlag.LINUX_HOME not in part_mod.flags and part_mod.is_home():
partition.setFlag(PartitionFlag.LINUX_HOME.flag_id)
# the partition has a path now that it has been added
part_mod.dev_path = Path(partition.path)
def fetch_part_info(self, path: Path) -> LsblkInfo:
lsblk_info = get_lsblk_info(path)
if not lsblk_info.partn:
debug(f'Unable to determine new partition number: {path}\n{lsblk_info}')
raise DiskError(f'Unable to determine new partition number: {path}')
if not lsblk_info.partuuid:
debug(f'Unable to determine new partition uuid: {path}\n{lsblk_info}')
raise DiskError(f'Unable to determine new partition uuid: {path}')
if not lsblk_info.uuid:
debug(f'Unable to determine new uuid: {path}\n{lsblk_info}')
raise DiskError(f'Unable to determine new uuid: {path}')
debug(f'partition information found: {lsblk_info.model_dump_json()}')
return lsblk_info
def create_lvm_btrfs_subvolumes(
self,
path: Path,
btrfs_subvols: list[SubvolumeModification],
mount_options: list[str],
) -> None:
info(f'Creating subvolumes: {path}')
mount(path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
for sub_vol in sorted(btrfs_subvols, key=lambda x: x.name):
debug(f'Creating subvolume: {sub_vol.name}')
subvol_path = self._TMP_BTRFS_MOUNT / sub_vol.name
SysCommand(f'btrfs subvolume create -p {subvol_path}')
if BtrfsMountOption.nodatacow.value in mount_options:
try:
SysCommand(f'chattr +C {subvol_path}')
except SysCallError as err:
raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {err}')
if BtrfsMountOption.compress.value in mount_options:
try:
SysCommand(f'chattr +c {subvol_path}')
except SysCallError as err:
raise DiskError(f'Could not set compress attribute at {subvol_path}: {err}')
umount(path)
def create_btrfs_volumes(
self,
part_mod: PartitionModification,
enc_conf: DiskEncryption | None = None,
) -> None:
info(f'Creating subvolumes: {part_mod.safe_dev_path}')
# unlock the partition first if it's encrypted
if enc_conf is not None and part_mod in enc_conf.partitions:
if not part_mod.mapper_name:
raise ValueError('No device path specified for modification')
luks_handler = unlock_luks2_dev(
part_mod.safe_dev_path,
part_mod.mapper_name,
enc_conf.encryption_password,
)
if not luks_handler.mapper_dev:
raise DiskError('Failed to unlock luks device')
dev_path = luks_handler.mapper_dev
else:
luks_handler = None
dev_path = part_mod.safe_dev_path
mount(
dev_path,
self._TMP_BTRFS_MOUNT,
create_target_mountpoint=True,
options=part_mod.mount_options,
)
for sub_vol in sorted(part_mod.btrfs_subvols, key=lambda x: x.name):
debug(f'Creating subvolume: {sub_vol.name}')
subvol_path = self._TMP_BTRFS_MOUNT / sub_vol.name
SysCommand(f'btrfs subvolume create -p {subvol_path}')
umount(dev_path)
if luks_handler is not None and luks_handler.mapper_dev is not None:
luks_handler.lock()
def umount_all_existing(self, device_path: Path) -> None:
debug(f'Unmounting all existing partitions: {device_path}')
existing_partitions = self._devices[device_path].partition_infos
for partition in existing_partitions:
debug(f'Unmounting: {partition.path}')
# un-mount for existing encrypted partitions
if partition.fs_type == FilesystemType.CRYPTO_LUKS:
Luks2(partition.path).lock()
else:
umount(partition.path, recursive=True)
def partition(
self,
modification: DeviceModification,
partition_table: PartitionTable | None = None,
) -> None:
"""
Create a partition table on the block device and create all partitions.
"""
partition_table = partition_table or self.partition_table
# WARNING: the entire device will be wiped and all data lost
if modification.wipe:
if partition_table.is_mbr() and len(modification.partitions) > 3:
raise DiskError('Too many partitions on disk, MBR disks can only have 3 primary partitions')
self.wipe_dev(modification.device)
disk = freshDisk(modification.device.disk.device, partition_table.value)
else:
info(f'Use existing device: {modification.device_path}')
disk = modification.device.disk
info(f'Creating partitions: {modification.device_path}')
# don't touch existing partitions
filtered_part = [p for p in modification.partitions if not p.exists()]
arch = platform.machine()
for part_mod in filtered_part:
# if the entire disk got nuked then we don't have to delete
# any existing partitions anymore because they're all gone already
requires_delete = modification.wipe is False
self._setup_partition(
part_mod,
modification.device,
disk,
requires_delete=requires_delete,
arch=arch,
)
disk.commit()
# Wipe filesystem/LVM signatures from newly created partitions
# to prevent "signature detected" errors
for part_mod in filtered_part:
if part_mod.dev_path:
debug(f'Wiping signatures from: {part_mod.dev_path}')
SysCommand(f'wipefs --all {part_mod.dev_path}')
# Sync with udev after wiping signatures
if filtered_part:
udev_sync()
def detect_pre_mounted_mods(self, base_mountpoint: Path) -> list[DeviceModification]:
part_mods: dict[Path, list[PartitionModification]] = {}
for device in self.devices:
for part_info in device.partition_infos:
for mountpoint in part_info.mountpoints:
if mountpoint.is_relative_to(base_mountpoint):
path = Path(part_info.disk.device.path)
part_mods.setdefault(path, [])
part_mod = PartitionModification.from_existing_partition(part_info)
if part_mod.mountpoint:
part_mod.mountpoint = mountpoint.root / mountpoint.relative_to(base_mountpoint)
else:
for subvol in part_mod.btrfs_subvols:
if sm := subvol.mountpoint:
subvol.mountpoint = sm.root / sm.relative_to(base_mountpoint)
part_mods[path].append(part_mod)
break
device_mods: list[DeviceModification] = []
for device_path, mods in part_mods.items():
device_mod = DeviceModification(self._devices[device_path], False, mods)
device_mods.append(device_mod)
return device_mods
def partprobe(self, path: Path | None = None) -> None:
if path is not None:
command = f'partprobe {path}'
else:
command = 'partprobe'
try:
debug(f'Calling partprobe: {command}')
SysCommand(command)
except SysCallError as err:
if 'have been written, but we have been unable to inform the kernel of the change' in str(err):
log(f'Partprobe was not able to inform the kernel of the new disk state (ignoring error): {err}', fg='gray', level=logging.INFO)
else:
error(f'"{command}" failed to run (continuing anyway): {err}')
def _wipe(self, dev_path: Path) -> None:
"""
Wipe a device (partition or otherwise) of meta-data, be it file system, LVM, etc.
@param dev_path: Device path of the partition to be wiped.
@type dev_path: str
"""
with open(dev_path, 'wb') as p:
p.write(bytearray(1024))
def wipe_dev(self, block_device: BDevice) -> None:
"""
Wipe the block device of meta-data, be it file system, LVM, etc.
This is not intended to be secure, but rather to ensure that
auto-discovery tools don't recognize anything here.
"""
info(f'Wiping partitions and metadata: {block_device.device_info.path}')
for partition in block_device.partition_infos:
luks = Luks2(partition.path)
if luks.isLuks():
luks.erase()
self._wipe(partition.path)
self._wipe(block_device.device_info.path)
device_handler = DeviceHandler()

View File

@ -1,880 +0,0 @@
from dataclasses import dataclass
from pathlib import Path
from typing import override
from archinstall.lib.disk.device_handler import device_handler
from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu
from archinstall.lib.disk.partitioning_menu import manual_partitioning
from archinstall.lib.log import debug
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Notify, Selection, Table
from archinstall.lib.menu.util import prompt_dir
from archinstall.lib.models.device import (
DEFAULT_ITER_TIME,
BDevice,
BtrfsMountOption,
BtrfsOptions,
DeviceModification,
DiskEncryption,
DiskLayoutConfiguration,
DiskLayoutType,
EncryptionType,
FilesystemType,
LvmConfiguration,
LvmLayoutType,
LvmVolume,
LvmVolumeGroup,
ModificationStatus,
PartitionFlag,
PartitionModification,
PartitionType,
SectorSize,
Size,
SnapshotConfig,
SnapshotType,
SubvolumeModification,
Unit,
_DeviceInfo,
)
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@dataclass
class DiskMenuConfig:
disk_config: DiskLayoutConfiguration | None
lvm_config: LvmConfiguration | None
btrfs_snapshot_config: SnapshotConfig | None
disk_encryption: DiskEncryption | None
class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
def __init__(self, disk_layout_config: DiskLayoutConfiguration | None):
if not disk_layout_config:
self._disk_menu_config = DiskMenuConfig(
disk_config=None,
lvm_config=None,
btrfs_snapshot_config=None,
disk_encryption=None,
)
else:
snapshot_config = disk_layout_config.btrfs_options.snapshot_config if disk_layout_config.btrfs_options else None
self._disk_menu_config = DiskMenuConfig(
disk_config=disk_layout_config,
lvm_config=disk_layout_config.lvm_config,
disk_encryption=disk_layout_config.disk_encryption,
btrfs_snapshot_config=snapshot_config,
)
menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True)
super().__init__(
self._item_group,
self._disk_menu_config,
allow_reset=True,
)
def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=tr('Partitioning'),
action=self._select_disk_layout_config,
value=self._disk_menu_config.disk_config,
preview_action=self._prev_disk_layouts,
key='disk_config',
),
MenuItem(
text='LVM',
action=self._select_lvm_config,
value=self._disk_menu_config.lvm_config,
preview_action=self._prev_lvm_config,
dependencies=[self._check_dep_lvm],
key='lvm_config',
),
MenuItem(
text=tr('Disk encryption'),
action=self._select_disk_encryption,
preview_action=self._prev_disk_encryption,
dependencies=['disk_config'],
key='disk_encryption',
),
MenuItem(
text='Btrfs snapshots',
action=self._select_btrfs_snapshots,
value=self._disk_menu_config.btrfs_snapshot_config,
preview_action=self._prev_btrfs_snapshots,
dependencies=[self._check_dep_btrfs],
key='btrfs_snapshot_config',
),
]
@override
async def show(self) -> DiskLayoutConfiguration | None: # type: ignore[override]
config: DiskMenuConfig | None = await super().show()
if config is None:
return None
if config.disk_config:
config.disk_config.lvm_config = self._disk_menu_config.lvm_config
config.disk_config.btrfs_options = BtrfsOptions(snapshot_config=self._disk_menu_config.btrfs_snapshot_config)
config.disk_config.disk_encryption = self._disk_menu_config.disk_encryption
return config.disk_config
return None
def _check_dep_lvm(self) -> bool:
disk_layout_conf: DiskLayoutConfiguration | None = self._menu_item_group.find_by_key('disk_config').value
if disk_layout_conf and disk_layout_conf.config_type == DiskLayoutType.Default:
return True
return False
def _check_dep_btrfs(self) -> bool:
disk_layout_conf: DiskLayoutConfiguration | None = self._menu_item_group.find_by_key('disk_config').value
if disk_layout_conf:
return disk_layout_conf.has_default_btrfs_vols()
return False
async def _select_disk_encryption(self, preset: DiskEncryption | None) -> DiskEncryption | None:
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
lvm_config: LvmConfiguration | None = self._item_group.find_by_key('lvm_config').value
if not disk_config:
return preset
modifications = disk_config.device_modifications
if not DiskEncryption.validate_enc(modifications, lvm_config):
return None
disk_encryption = await DiskEncryptionMenu(modifications, lvm_config=lvm_config, preset=preset).show()
return disk_encryption
async def _select_disk_layout_config(self, preset: DiskLayoutConfiguration | None) -> DiskLayoutConfiguration | None:
disk_config = await select_disk_config(preset)
if disk_config != preset:
self._menu_item_group.find_by_key('lvm_config').value = None
self._menu_item_group.find_by_key('disk_encryption').value = None
return disk_config
async def _select_lvm_config(self, preset: LvmConfiguration | None) -> LvmConfiguration | None:
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
if not disk_config:
return preset
lvm_config = await select_lvm_config(disk_config, preset=preset)
if lvm_config != preset:
self._menu_item_group.find_by_key('disk_encryption').value = None
return lvm_config
async def _select_btrfs_snapshots(self, preset: SnapshotConfig | None) -> SnapshotConfig | None:
preset_type = preset.snapshot_type if preset else None
group = MenuItemGroup.from_enum(
SnapshotType,
sort_items=True,
preset=preset_type,
)
result = await Selection[SnapshotType](
group,
allow_reset=True,
allow_skip=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return None
case ResultType.Selection:
return SnapshotConfig(snapshot_type=result.get_value())
def _prev_disk_layouts(self, item: MenuItem) -> str | None:
if not item.value:
return None
disk_layout_conf = item.get_value()
if disk_layout_conf.config_type == DiskLayoutType.Pre_mount:
msg = tr('Configuration type: {}').format(disk_layout_conf.config_type.display_msg()) + '\n'
msg += tr('Mountpoint') + ': ' + str(disk_layout_conf.mountpoint)
return msg
device_mods = [d for d in disk_layout_conf.device_modifications if d.partitions]
if device_mods:
output_partition = '{}: {}\n'.format(tr('Configuration'), disk_layout_conf.config_type.display_msg())
output_btrfs = ''
for mod in device_mods:
# create partition table
partition_table = as_table(mod.partitions)
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe)
output_partition += partition_table + '\n'
# create btrfs table
btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols]
for partition in btrfs_partitions:
output_btrfs += as_table(partition.btrfs_subvols) + '\n'
output = output_partition + output_btrfs
return output.rstrip()
return None
def _prev_lvm_config(self, item: MenuItem) -> str | None:
if not item.value:
return None
lvm_config: LvmConfiguration = item.value
output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg())
for vol_gp in lvm_config.vol_groups:
pv_table = as_table(vol_gp.pvs)
output += '{}:\n{}'.format(tr('Physical volumes'), pv_table)
output += f'\nVolume Group: {vol_gp.name}'
lvm_volumes = as_table(vol_gp.volumes)
output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes)
return output
return None
def _prev_btrfs_snapshots(self, item: MenuItem) -> str | None:
if not item.value:
return None
snapshot_config: SnapshotConfig = item.value
return tr('Snapshot type: {}').format(snapshot_config.snapshot_type.value)
def _prev_disk_encryption(self, item: MenuItem) -> str | None:
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
lvm_config: LvmConfiguration | None = self._item_group.find_by_key('lvm_config').value
enc_config: DiskEncryption | None = item.value
if disk_config and not DiskEncryption.validate_enc(disk_config.device_modifications, lvm_config):
return tr('LVM disk encryption with more than 2 partitions is currently not supported')
if enc_config:
enc_type = enc_config.encryption_type
output = tr('Encryption type') + f': {enc_type.type_to_text()}\n'
if enc_config.encryption_password:
output += tr('Password') + f': {enc_config.encryption_password.hidden()}\n'
if enc_type != EncryptionType.NO_ENCRYPTION:
output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n'
if enc_config.partitions:
output += f'Partitions: {len(enc_config.partitions)} selected\n'
elif enc_config.lvm_volumes:
output += f'LVM volumes: {len(enc_config.lvm_volumes)} selected\n'
if enc_config.hsm_device:
output += f'HSM: {enc_config.hsm_device.manufacturer}'
return output
return None
async def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | None:
def _preview_device_selection(item: MenuItem) -> str | None:
device: _DeviceInfo = item.value # type: ignore[assignment]
dev = device_handler.get_device(device.path)
if dev and dev.partition_infos:
return as_table(dev.partition_infos)
return None
if preset is None:
preset = []
devices = device_handler.devices
if len(devices) < 1:
await Notify(tr('No disks were detected. A disk is required to be able to install Arch Linux')).show()
return None
items = [
MenuItem(
str(d.device_info.path),
d.device_info,
preview_action=_preview_device_selection,
)
for d in devices
]
presets = [p.device_info for p in preset]
group = MenuItemGroup(items)
group.set_selected_by_value(presets)
result = await Table[_DeviceInfo](
header=tr('Select disks for the installation'),
group=group,
presets=presets,
allow_skip=True,
multi=True,
preview_location='bottom',
preview_header=tr('Partitions'),
).show()
match result.type_:
case ResultType.Reset:
return None
case ResultType.Skip:
return None
case ResultType.Selection:
selected_device_info = result.get_values()
selected_devices = []
for device in devices:
if device.device_info in selected_device_info:
selected_devices.append(device)
return selected_devices
async def get_default_partition_layout(
devices: list[BDevice],
filesystem_type: FilesystemType | None = None,
) -> list[DeviceModification]:
if len(devices) == 1:
device_modification = await suggest_single_disk_layout(
devices[0],
filesystem_type=filesystem_type,
)
return [device_modification]
else:
return await suggest_multi_disk_layout(
devices,
filesystem_type=filesystem_type,
)
async def _manual_partitioning(
preset: list[DeviceModification],
devices: list[BDevice],
) -> list[DeviceModification] | None:
modifications: list[DeviceModification] = []
for device in devices:
mod = next(filter(lambda x: x.device == device, preset), None)
if not mod:
mod = DeviceModification(device, wipe=False)
device_mod = await manual_partitioning(mod, device_handler.partition_table)
if not device_mod:
return None
modifications.append(device_mod)
return modifications
async def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLayoutConfiguration | None:
default_layout = DiskLayoutType.Default.display_msg()
manual_mode = DiskLayoutType.Manual.display_msg()
pre_mount_mode = DiskLayoutType.Pre_mount.display_msg()
items = [
MenuItem(default_layout, value=default_layout),
MenuItem(manual_mode, value=manual_mode),
MenuItem(pre_mount_mode, value=pre_mount_mode),
]
group = MenuItemGroup(items, sort_items=False)
if preset:
group.set_selected_by_value(preset.config_type.display_msg())
result = await Selection[str](
group,
header=tr('Select a disk configuration'),
allow_skip=True,
allow_reset=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return None
case ResultType.Selection:
selection = result.get_value()
if selection == pre_mount_mode:
output = tr('Enter root mount directory') + '\n\n'
output += tr('You will use whatever drive-setup is mounted at the specified directory') + '\n'
output += tr("WARNING: Archinstall won't check the suitability of this setup")
path = await prompt_dir(output, allow_skip=True)
if path is None:
return None
mods = device_handler.detect_pre_mounted_mods(path)
return DiskLayoutConfiguration(
config_type=DiskLayoutType.Pre_mount,
device_modifications=mods,
mountpoint=path,
)
preset_devices = [mod.device for mod in preset.device_modifications] if preset else []
devices = await select_devices(preset_devices)
if devices is None:
return preset
if result.get_value() == default_layout:
modifications = await get_default_partition_layout(devices)
if modifications:
return DiskLayoutConfiguration(
config_type=DiskLayoutType.Default,
device_modifications=modifications,
)
elif result.get_value() == manual_mode:
preset_mods = preset.device_modifications if preset else []
partitions = await _manual_partitioning(preset_mods, devices)
if not partitions:
return preset
return DiskLayoutConfiguration(
config_type=DiskLayoutType.Manual,
device_modifications=partitions,
)
return None
async def select_lvm_config(
disk_config: DiskLayoutConfiguration,
preset: LvmConfiguration | None = None,
) -> LvmConfiguration | None:
preset_value = preset.config_type.display_msg() if preset else None
default_mode = LvmLayoutType.Default.display_msg()
items = [MenuItem(default_mode, value=default_mode)]
group = MenuItemGroup(items)
group.set_focus_by_value(preset_value)
result = await Selection[str](
group,
allow_reset=True,
allow_skip=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return None
case ResultType.Selection:
if result.get_value() == default_mode:
return await suggest_lvm_layout(disk_config)
return None
def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModification:
flags = [PartitionFlag.BOOT]
size = Size(1, Unit.GiB, sector_size)
start = Size(1, Unit.MiB, sector_size)
if using_gpt:
flags.append(PartitionFlag.ESP)
# boot partition
return PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
start=start,
length=size,
mountpoint=Path('/boot'),
fs_type=FilesystemType.FAT32,
flags=flags,
)
async def select_main_filesystem_format() -> FilesystemType:
items = [
MenuItem(FilesystemType.BTRFS.value, value=FilesystemType.BTRFS),
MenuItem(FilesystemType.EXT4.value, value=FilesystemType.EXT4),
MenuItem(FilesystemType.XFS.value, value=FilesystemType.XFS),
MenuItem(FilesystemType.F2FS.value, value=FilesystemType.F2FS),
]
group = MenuItemGroup(items, sort_items=False)
result = await Selection[FilesystemType](
group,
header=tr('Select main filesystem'),
allow_skip=False,
).show()
match result.type_:
case ResultType.Selection:
return result.get_value()
case _:
raise ValueError('Unhandled result type')
async def select_mount_options() -> list[str]:
prompt = tr('Would you like to use compression or disable CoW?') + '\n'
compression = tr('Use compression')
disable_cow = tr('Disable Copy-on-Write')
items = [
MenuItem(compression, value=BtrfsMountOption.compress.value),
MenuItem(disable_cow, value=BtrfsMountOption.nodatacow.value),
]
group = MenuItemGroup(items, sort_items=False)
result = await Selection[str](
group,
header=prompt,
allow_skip=True,
).show()
match result.type_:
case ResultType.Skip:
return []
case ResultType.Selection:
return [result.get_value()]
case _:
raise ValueError('Unhandled result type')
def process_root_partition_size(total_size: Size, sector_size: SectorSize) -> Size:
# root partition size processing
total_device_size = total_size.convert(Unit.GiB)
if total_device_size.value > 500:
# maximum size
return Size(value=50, unit=Unit.GiB, sector_size=sector_size)
elif total_device_size.value < 320:
# minimum size
return Size(value=32, unit=Unit.GiB, sector_size=sector_size)
else:
# 10% of total size
length = total_device_size.value // 10
return Size(value=length, unit=Unit.GiB, sector_size=sector_size)
def get_default_btrfs_subvols() -> list[SubvolumeModification]:
# https://btrfs.wiki.kernel.org/index.php/FAQ
# https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash
# https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh
return [
SubvolumeModification(Path('@'), Path('/')),
SubvolumeModification(Path('@home'), Path('/home')),
SubvolumeModification(Path('@log'), Path('/var/log')),
SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')),
]
async def suggest_single_disk_layout(
device: BDevice,
filesystem_type: FilesystemType | None = None,
separate_home: bool | None = None,
) -> DeviceModification:
if not filesystem_type:
filesystem_type = await select_main_filesystem_format()
sector_size = device.device_info.sector_size
total_size = device.device_info.total_size
available_space = total_size
min_size_to_allow_home_part = Size(64, Unit.GiB, sector_size)
if filesystem_type == FilesystemType.BTRFS:
prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n'
result = await Confirmation(
header=prompt,
allow_skip=False,
preset=True,
).show()
using_subvolumes = result.item() == MenuItem.yes()
mount_options = await select_mount_options()
else:
using_subvolumes = False
mount_options = []
device_modification = DeviceModification(device, wipe=True)
using_gpt = device_handler.partition_table.is_gpt()
if using_gpt:
available_space = available_space.gpt_end()
available_space = available_space.align()
# Used for reference: https://wiki.archlinux.org/title/partitioning
boot_partition = _boot_partition(sector_size, using_gpt)
device_modification.add_partition(boot_partition)
if separate_home is False or using_subvolumes or total_size < min_size_to_allow_home_part:
using_home_partition = False
elif separate_home:
using_home_partition = True
else:
prompt = tr('Would you like to create a separate partition for /home?') + '\n'
result = await Confirmation(
header=prompt,
allow_skip=False,
preset=True,
).show()
using_home_partition = result.item() == MenuItem.yes()
# root partition
root_start = boot_partition.start + boot_partition.length
# Set a size for / (/root)
if using_home_partition:
root_length = process_root_partition_size(total_size, sector_size)
else:
root_length = available_space - root_start
root_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
start=root_start,
length=root_length,
mountpoint=Path('/') if not using_subvolumes else None,
fs_type=filesystem_type,
mount_options=mount_options,
)
device_modification.add_partition(root_partition)
if using_subvolumes:
root_partition.btrfs_subvols = get_default_btrfs_subvols()
elif using_home_partition:
# If we don't want to use subvolumes,
# But we want to be able to reuse data between re-installs..
# A second partition for /home would be nice if we have the space for it
home_start = root_partition.start + root_partition.length
home_length = available_space - home_start
flags = []
if using_gpt:
flags.append(PartitionFlag.LINUX_HOME)
home_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
start=home_start,
length=home_length,
mountpoint=Path('/home'),
fs_type=filesystem_type,
mount_options=mount_options,
flags=flags,
)
device_modification.add_partition(home_partition)
return device_modification
async def suggest_multi_disk_layout(
devices: list[BDevice],
filesystem_type: FilesystemType | None = None,
) -> list[DeviceModification]:
if not devices:
return []
# Not really a rock solid foundation of information to stand on, but it's a start:
# https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/
# https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/
min_home_partition_size = Size(40, Unit.GiB, SectorSize.default())
# rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size?
desired_root_partition_size = Size(32, Unit.GiB, SectorSize.default())
mount_options = []
if not filesystem_type:
filesystem_type = await select_main_filesystem_format()
# find proper disk for /home
possible_devices = [d for d in devices if d.device_info.total_size >= min_home_partition_size]
home_device = max(possible_devices, key=lambda d: d.device_info.total_size) if possible_devices else None
# find proper device for /root
devices_delta = {}
for device in devices:
if device is not home_device:
delta = device.device_info.total_size - desired_root_partition_size
devices_delta[device] = delta
sorted_delta: list[tuple[BDevice, Size]] = sorted(devices_delta.items(), key=lambda x: x[1])
root_device: BDevice | None = sorted_delta[0][0]
if home_device is None or root_device is None:
text = tr('The selected drives do not have the minimum capacity required for an automatic suggestion\n')
text += tr('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(Unit.GiB))
text += tr('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(Unit.GiB))
_ = await Notify(text).show()
return []
if filesystem_type == FilesystemType.BTRFS:
mount_options = await select_mount_options()
device_paths = ', '.join(str(d.device_info.path) for d in devices)
debug(f'Suggesting multi-disk-layout for devices: {device_paths}')
debug(f'/root: {root_device.device_info.path}')
debug(f'/home: {home_device.device_info.path}')
root_device_modification = DeviceModification(root_device, wipe=True)
home_device_modification = DeviceModification(home_device, wipe=True)
root_device_sector_size = root_device_modification.device.device_info.sector_size
home_device_sector_size = home_device_modification.device.device_info.sector_size
using_gpt = device_handler.partition_table.is_gpt()
# add boot partition to the root device
boot_partition = _boot_partition(root_device_sector_size, using_gpt)
root_device_modification.add_partition(boot_partition)
root_start = boot_partition.start + boot_partition.length
root_length = root_device.device_info.total_size - root_start
if using_gpt:
root_length = root_length.gpt_end()
root_length = root_length.align()
# add root partition to the root device
root_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
start=root_start,
length=root_length,
mountpoint=Path('/'),
mount_options=mount_options,
fs_type=filesystem_type,
)
root_device_modification.add_partition(root_partition)
home_start = Size(1, Unit.MiB, home_device_sector_size)
home_length = home_device.device_info.total_size - home_start
flags = []
if using_gpt:
home_length = home_length.gpt_end()
flags.append(PartitionFlag.LINUX_HOME)
home_length = home_length.align()
# add home partition to home device
home_partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
start=home_start,
length=home_length,
mountpoint=Path('/home'),
mount_options=mount_options,
fs_type=filesystem_type,
flags=flags,
)
home_device_modification.add_partition(home_partition)
return [root_device_modification, home_device_modification]
async def suggest_lvm_layout(
disk_config: DiskLayoutConfiguration,
filesystem_type: FilesystemType | None = None,
vg_grp_name: str = 'ArchinstallVg',
) -> LvmConfiguration:
if disk_config.config_type != DiskLayoutType.Default:
raise ValueError('LVM suggested volumes are only available for default partitioning')
using_subvolumes = False
btrfs_subvols = []
home_volume = True
mount_options = []
if not filesystem_type:
filesystem_type = await select_main_filesystem_format()
if filesystem_type == FilesystemType.BTRFS:
prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n'
result = await Confirmation(header=prompt, allow_skip=False, preset=True).show()
using_subvolumes = MenuItem.yes() == result.item()
mount_options = await select_mount_options()
if using_subvolumes:
btrfs_subvols = get_default_btrfs_subvols()
home_volume = False
boot_part: PartitionModification | None = None
other_part: list[PartitionModification] = []
for mod in disk_config.device_modifications:
for part in mod.partitions:
if part.is_boot():
boot_part = part
else:
other_part.append(part)
if not boot_part:
raise ValueError('Unable to find boot partition in partition modifications')
total_vol_available = sum(
[p.length for p in other_part],
Size(0, Unit.B, SectorSize.default()),
)
root_vol_size = process_root_partition_size(total_vol_available, SectorSize.default())
home_vol_size = total_vol_available - root_vol_size
lvm_vol_group = LvmVolumeGroup(vg_grp_name, pvs=other_part)
root_vol = LvmVolume(
status=ModificationStatus.CREATE,
name='root',
fs_type=filesystem_type,
length=root_vol_size,
mountpoint=Path('/'),
btrfs_subvols=btrfs_subvols,
mount_options=mount_options,
)
lvm_vol_group.volumes.append(root_vol)
if home_volume:
home_vol = LvmVolume(
status=ModificationStatus.CREATE,
name='home',
fs_type=filesystem_type,
length=home_vol_size,
mountpoint=Path('/home'),
)
lvm_vol_group.volumes.append(home_vol)
return LvmConfiguration(LvmLayoutType.Default, [lvm_vol_group])

View File

@ -0,0 +1,48 @@
import pathlib
import logging
import json
from dataclasses import dataclass
from typing import Optional
from ..exceptions import SysCallError
from ..general import SysCommand
from ..output import log
from .mapperdev import MapperDev
@dataclass
class DMCryptDev:
dev_path :pathlib.Path
@property
def name(self):
with open(f"/sys/devices/virtual/block/{pathlib.Path(self.path).name}/dm/name", "r") as fh:
return fh.read().strip()
@property
def path(self):
return f"/dev/mapper/{self.dev_path}"
@property
def blockdev(self):
pass
@property
def MapperDev(self):
return MapperDev(mappername=self.name)
@property
def mountpoint(self) -> Optional[str]:
try:
data = json.loads(SysCommand(f"findmnt --json -R {self.dev_path}").decode())
for filesystem in data['filesystems']:
return filesystem.get('target')
except SysCallError as error:
# Not mounted anywhere most likely
log(f"Could not locate mount information for {self.dev_path}: {error}", level=logging.WARNING, fg="yellow")
pass
return None
@property
def filesystem(self) -> Optional[str]:
return self.MapperDev.filesystem

View File

@ -1,406 +0,0 @@
from pathlib import Path
from typing import override
from archinstall.lib.disk.fido import Fido2
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Input, Selection, Table
from archinstall.lib.menu.menu_helper import MenuHelper
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.device import (
DEFAULT_ITER_TIME,
DeviceModification,
DiskEncryption,
EncryptionType,
Fido2Device,
LvmConfiguration,
LvmVolume,
PartitionModification,
)
from archinstall.lib.models.users import Password
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def __init__(
self,
device_modifications: list[DeviceModification],
lvm_config: LvmConfiguration | None = None,
preset: DiskEncryption | None = None,
):
if preset:
self._enc_config = preset
else:
self._enc_config = DiskEncryption()
self._device_modifications = device_modifications
self._lvm_config = lvm_config
menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True)
super().__init__(
self._item_group,
self._enc_config,
allow_reset=True,
)
def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=tr('Encryption type'),
action=lambda x: select_encryption_type(self._lvm_config, x),
value=self._enc_config.encryption_type,
preview_action=self._prev_type,
key='encryption_type',
),
MenuItem(
text=tr('Encryption password'),
action=lambda x: select_encrypted_password(),
value=self._enc_config.encryption_password,
dependencies=[self._check_dep_enc_type],
preview_action=self._prev_password,
key='encryption_password',
),
MenuItem(
text=tr('Iteration time'),
action=select_iteration_time,
value=self._enc_config.iter_time,
dependencies=[self._check_dep_enc_type],
preview_action=self._prev_iter_time,
key='iter_time',
),
MenuItem(
text=tr('Partitions'),
action=lambda x: select_partitions_to_encrypt(self._device_modifications, x),
value=self._enc_config.partitions,
dependencies=[self._check_dep_partitions],
preview_action=self._prev_partitions,
key='partitions',
),
MenuItem(
text=tr('LVM volumes'),
action=self._select_lvm_vols,
value=self._enc_config.lvm_volumes,
dependencies=[self._check_dep_lvm_vols],
preview_action=self._prev_lvm_vols,
key='lvm_volumes',
),
MenuItem(
text=tr('HSM'),
action=select_hsm,
value=self._enc_config.hsm_device,
dependencies=[self._check_dep_enc_type],
preview_action=self._prev_hsm,
key='hsm_device',
),
]
async def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]:
if self._lvm_config:
return await select_lvm_vols_to_encrypt(self._lvm_config, preset=preset)
return []
def _check_dep_enc_type(self) -> bool:
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type != EncryptionType.NO_ENCRYPTION:
return True
return False
def _check_dep_partitions(self) -> bool:
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS]:
return True
return False
def _check_dep_lvm_vols(self) -> bool:
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type == EncryptionType.LUKS_ON_LVM:
return True
return False
@override
async def show(self) -> DiskEncryption | None:
enc_config = await super().show()
if enc_config is None:
return None
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
enc_password: Password | None = self._item_group.find_by_key('encryption_password').value
iter_time: int | None = self._item_group.find_by_key('iter_time').value
enc_partitions = self._item_group.find_by_key('partitions').value
enc_lvm_vols = self._item_group.find_by_key('lvm_volumes').value
assert enc_type is not None
assert enc_partitions is not None
assert enc_lvm_vols is not None
if enc_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and enc_partitions:
enc_lvm_vols = []
if enc_type == EncryptionType.LUKS_ON_LVM:
enc_partitions = []
if enc_type != EncryptionType.NO_ENCRYPTION and enc_password and (enc_partitions or enc_lvm_vols):
return DiskEncryption(
encryption_password=enc_password,
encryption_type=enc_type,
partitions=enc_partitions,
lvm_volumes=enc_lvm_vols,
hsm_device=enc_config.hsm_device,
iter_time=iter_time or DEFAULT_ITER_TIME,
)
return None
def _preview(self, item: MenuItem) -> str | None:
output = ''
if (enc_type := self._prev_type(item)) is not None:
output += enc_type
if (enc_pwd := self._prev_password(item)) is not None:
output += f'\n{enc_pwd}'
if (iter_time := self._prev_iter_time(item)) is not None:
output += f'\n{iter_time}'
if (fido_device := self._prev_hsm(item)) is not None:
output += f'\n{fido_device}'
if (partitions := self._prev_partitions(item)) is not None:
output += f'\n\n{partitions}'
if (lvm := self._prev_lvm_vols(item)) is not None:
output += f'\n\n{lvm}'
if not output:
return None
return output
def _prev_type(self, item: MenuItem) -> str | None:
enc_type = self._item_group.find_by_key('encryption_type').value
if enc_type:
enc_text = enc_type.type_to_text()
return f'{tr("Encryption type")}: {enc_text}'
return None
def _prev_password(self, item: MenuItem) -> str | None:
if item.value:
return f'{tr("Encryption password")}: {item.value.hidden()}'
return None
def _prev_partitions(self, item: MenuItem) -> str | None:
if item.value:
output = tr('Partitions to be encrypted') + '\n'
output += as_table(item.value)
return output.rstrip()
return None
def _prev_lvm_vols(self, item: MenuItem) -> str | None:
if item.value:
output = tr('LVM volumes to be encrypted') + '\n'
output += as_table(item.value)
return output.rstrip()
return None
def _prev_hsm(self, item: MenuItem) -> str | None:
if not item.value:
return None
fido_device: Fido2Device = item.value
output = str(fido_device.path)
output += f' ({fido_device.manufacturer}, {fido_device.product})'
return f'{tr("HSM device")}: {output}'
def _prev_iter_time(self, item: MenuItem) -> str | None:
if item.value:
iter_time = item.value
enc_type = self._item_group.find_by_key('encryption_type').value
if iter_time and enc_type != EncryptionType.NO_ENCRYPTION:
return f'{tr("Iteration time")}: {iter_time}ms'
return None
async def select_encryption_type(
lvm_config: LvmConfiguration | None = None,
preset: EncryptionType | None = None,
) -> EncryptionType | None:
options: list[EncryptionType] = []
if lvm_config:
options = [EncryptionType.LVM_ON_LUKS, EncryptionType.LUKS_ON_LVM]
else:
options = [EncryptionType.LUKS]
if not preset:
preset = options[0]
preset_value = preset.type_to_text()
items = [MenuItem(o.type_to_text(), value=o) for o in options]
group = MenuItemGroup(items)
group.set_focus_by_value(preset_value)
result = await Selection[EncryptionType](
group,
header=tr('Select encryption type'),
allow_skip=True,
allow_reset=True,
).show()
match result.type_:
case ResultType.Reset:
return None
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
async def select_encrypted_password() -> Password | None:
header = tr('Enter disk encryption password (leave blank for no encryption)') + '\n'
password = await get_password(
header=header,
allow_skip=True,
)
return password
async def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
header = tr('Select a FIDO2 device to use for HSM') + '\n'
try:
fido_devices = Fido2.get_cryptenroll_devices()
except ValueError:
return None
if fido_devices:
group = MenuHelper(data=fido_devices).create_menu_group()
result = await Selection[Fido2Device](
group,
header=header,
allow_skip=True,
).show()
match result.type_:
case ResultType.Reset:
return None
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
return None
async def select_partitions_to_encrypt(
modification: list[DeviceModification],
preset: list[PartitionModification],
) -> list[PartitionModification]:
partitions: list[PartitionModification] = []
# do not allow encrypting the boot partition
for mod in modification:
partitions += [p for p in mod.partitions if p.mountpoint != Path('/boot') and not p.is_swap()]
# do not allow encrypting existing partitions that are not marked as wipe
avail_partitions = [p for p in partitions if not p.exists()]
if avail_partitions:
group = MenuItemGroup.from_objects(avail_partitions)
group.set_selected_by_value(preset)
result = await Table[PartitionModification](
header=tr('Select disks for the installation'),
group=group,
allow_skip=True,
multi=True,
).show()
match result.type_:
case ResultType.Reset:
return []
case ResultType.Skip:
return preset
case ResultType.Selection:
partitions = result.get_values()
return partitions
return []
async def select_lvm_vols_to_encrypt(
lvm_config: LvmConfiguration,
preset: list[LvmVolume],
) -> list[LvmVolume]:
volumes: list[LvmVolume] = lvm_config.get_all_volumes()
if volumes:
group = MenuItemGroup.from_objects(volumes)
group.set_selected_by_value(preset)
result = await Table[LvmVolume](
header=tr('Select disks for the installation'),
group=group,
allow_skip=True,
multi=True,
).show()
match result.type_:
case ResultType.Reset:
return []
case ResultType.Skip:
return preset
case ResultType.Selection:
volumes = result.get_values()
return volumes
return []
async def select_iteration_time(preset: int | None = None) -> int | None:
header = tr('Enter iteration time for LUKS encryption (in milliseconds)') + '\n'
header += tr('Higher values increase security but slow down boot time') + '\n'
header += tr('Default: {}ms, Recommended range: 1000-60000').format(DEFAULT_ITER_TIME) + '\n'
def validate_iter_time(value: str) -> str | None:
try:
iter_time = int(value)
if iter_time < 100:
return tr('Iteration time must be at least 100ms')
if iter_time > 120000:
return tr('Iteration time must be at most 120000ms')
return None
except ValueError:
return tr('Please enter a valid number')
result = await Input(
header=header,
allow_skip=True,
default_value=str(preset) if preset else str(DEFAULT_ITER_TIME),
validator_callback=validate_iter_time,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
if not result.get_value():
return preset
return int(result.get_value())
case ResultType.Reset:
return None

View File

@ -1,118 +0,0 @@
import getpass
from pathlib import Path
from typing import ClassVar
from archinstall.lib.command import SysCommand, SysCommandWorker
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import error, info
from archinstall.lib.models.device import Fido2Device
from archinstall.lib.models.users import Password
from archinstall.lib.utils.encoding import clear_vt100_escape_codes_from_str
class Fido2:
_loaded_cryptsetup: bool = False
_loaded_u2f: bool = False
_cryptenroll_devices: ClassVar[list[Fido2Device]] = []
_u2f_devices: ClassVar[list[Fido2Device]] = []
@classmethod
def get_fido2_devices(cls) -> list[Fido2Device]:
"""
fido2-tool output example:
/dev/hidraw4: vendor=0x1050, product=0x0407 (Yubico YubiKey OTP+FIDO+CCID)
"""
if not cls._loaded_u2f:
cls._loaded_u2f = True
try:
ret = SysCommand('fido2-token -L').decode()
except Exception as e:
error(f'failed to read fido2 devices: {e}')
return []
fido_devices = clear_vt100_escape_codes_from_str(ret)
if not fido_devices:
return []
for line in fido_devices.splitlines():
path, details = line.replace(',', '').split(':', maxsplit=1)
_, product, manufacturer = details.strip().split(' ', maxsplit=2)
cls._u2f_devices.append(Fido2Device(Path(path.strip()), manufacturer.strip(), product.strip().split('=')[1]))
return cls._u2f_devices
@classmethod
def get_cryptenroll_devices(cls, reload: bool = False) -> list[Fido2Device]:
"""
Uses systemd-cryptenroll to list the FIDO2 devices
connected that supports FIDO2.
Some devices might show up in udevadm as FIDO2 compliant
when they are in fact not.
The drawback of systemd-cryptenroll is that it uses human readable format.
That means we get this weird table like structure that is of no use.
So we'll look for `MANUFACTURER` and `PRODUCT`, we take their index
and we split each line based on those positions.
Output example:
PATH MANUFACTURER PRODUCT
/dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID
"""
# to prevent continuous reloading which will slow
# down moving the cursor in the menu
if not cls._loaded_cryptsetup or reload:
try:
ret = SysCommand('systemd-cryptenroll --fido2-device=list').decode()
except SysCallError:
error('fido2 support is most likely not installed')
raise ValueError('HSM devices can not be detected, is libfido2 installed?')
fido_devices = clear_vt100_escape_codes_from_str(ret)
manufacturer_pos = 0
product_pos = 0
devices = []
for line in fido_devices.split('\r\n'):
if '/dev' not in line:
manufacturer_pos = line.find('MANUFACTURER')
product_pos = line.find('PRODUCT')
continue
path = line[:manufacturer_pos].rstrip()
manufacturer = line[manufacturer_pos:product_pos].rstrip()
product = line[product_pos:]
devices.append(
Fido2Device(Path(path), manufacturer, product),
)
cls._loaded_cryptsetup = True
cls._cryptenroll_devices = devices
return cls._cryptenroll_devices
@staticmethod
def fido2_enroll(hsm_device: Fido2Device, dev_path: Path, password: Password) -> None:
worker = SysCommandWorker(f'systemd-cryptenroll --fido2-device={hsm_device.path} {dev_path}', peek_output=True)
pw_inputted = False
pin_inputted = False
info('You might need to touch the FIDO2 device to unlock it if no prompt comes up after 3 seconds')
while worker.is_alive():
if pw_inputted is False:
if bytes(f'please enter current passphrase for disk {dev_path}', 'UTF-8') in worker._trace_log.lower():
worker.write(bytes(password.plaintext, 'UTF-8'))
pw_inputted = True
elif pin_inputted is False:
if bytes('please enter security token pin', 'UTF-8') in worker._trace_log.lower():
worker.write(bytes(getpass.getpass(' '), 'UTF-8'))
pin_inputted = True

View File

@ -1,327 +1,261 @@
import math from __future__ import annotations
import time import time
from pathlib import Path import logging
import json
import pathlib
from typing import Optional, Dict, Any, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from .blockdevice import BlockDevice
_: Any
from archinstall.lib.disk.device_handler import device_handler from .partition import Partition
from archinstall.lib.disk.luks import Luks2 from .validators import valid_fs_type
from archinstall.lib.disk.lvm import ( from ..exceptions import DiskError, SysCallError
lvm_group_info, from ..general import SysCommand
lvm_pv_create, from ..output import log
lvm_vg_create, from ..storage import storage
lvm_vol_create,
lvm_vol_info,
lvm_vol_reduce,
)
from archinstall.lib.disk.utils import udev_sync
from archinstall.lib.log import debug, info
from archinstall.lib.models.device import (
DiskEncryption,
DiskLayoutConfiguration,
DiskLayoutType,
EncryptionType,
FilesystemType,
LvmConfiguration,
LvmVolume,
LvmVolumeGroup,
PartitionModification,
SectorSize,
Size,
Unit,
)
GPT = 0b00000001
MBR = 0b00000010
class FilesystemHandler: # A sane default is 5MiB, that allows for plenty of buffer for GRUB on MBR
def __init__(self, disk_config: DiskLayoutConfiguration): # but also 4MiB for memory cards for instance. And another 1MiB to avoid issues.
self._disk_config = disk_config # (we've been pestered by disk issues since the start, so please let this be here for a few versions)
self._enc_config = disk_config.disk_encryption DEFAULT_PARTITION_START = '5MiB'
def perform_filesystem_operations(self) -> None: class Filesystem:
if self._disk_config.config_type == DiskLayoutType.Pre_mount: # TODO:
debug('Disk layout configuration is set to pre-mount, not performing any operations') # When instance of a HDD is selected, check all usages and gracefully unmount them
return # as well as close any crypto handles.
def __init__(self, blockdevice :BlockDevice, mode :int):
self.blockdevice = blockdevice
self.mode = mode
device_mods = [d for d in self._disk_config.device_modifications if d.partitions] def __enter__(self, *args :str, **kwargs :str) -> 'Filesystem':
return self
if not device_mods: def __repr__(self) -> str:
debug('No modifications required') return f"Filesystem(blockdevice={self.blockdevice}, mode={self.mode})"
return
# Setup the blockdevice, filesystem (and optionally encryption). def __exit__(self, *args :str, **kwargs :str) -> bool:
# Once that's done, we'll hand over to perform_installation() # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if len(args) >= 2 and args[1]:
raise args[1]
# make sure all devices are unmounted SysCommand('sync')
for mod in device_mods: return True
device_handler.umount_all_existing(mod.device_path)
for mod in device_mods: def partuuid_to_index(self, uuid :str) -> Optional[int]:
device_handler.partition(mod) for i in range(storage['DISK_RETRY_ATTEMPTS']):
self.partprobe()
time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i))
udev_sync() # We'll use unreliable lbslk to grab children under the /dev/<device>
output = json.loads(SysCommand(f"lsblk --json {self.blockdevice.device}").decode('UTF-8'))
if self._disk_config.lvm_config: for device in output['blockdevices']:
for mod in device_mods: for index, partition in enumerate(device['children']):
if boot_part := mod.get_boot_partition(): # But we'll use blkid to reliably grab the PARTUUID for that child device (partition)
debug(f'Formatting boot partition: {boot_part.dev_path}') partition_uuid = SysCommand(f"blkid -s PARTUUID -o value /dev/{partition.get('name')}").decode().strip()
self._format_partitions([boot_part]) if partition_uuid.lower() == uuid.lower():
return index
self.perform_lvm_operations() raise DiskError(f"Failed to convert PARTUUID {uuid} to a partition index number on blockdevice {self.blockdevice.device}")
else:
for mod in device_mods:
self._format_partitions(mod.partitions)
for part_mod in mod.partitions: def load_layout(self, layout :Dict[str, Any]) -> None:
if part_mod.fs_type == FilesystemType.BTRFS and part_mod.is_create_or_modify(): from ..luks import luks2
device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config)
def _format_partitions( # If the layout tells us to wipe the drive, we do so
self, if layout.get('wipe', False):
partitions: list[PartitionModification], if self.mode == GPT:
) -> None: if not self.parted_mklabel(self.blockdevice.device, "gpt"):
raise KeyError(f"Could not create a GPT label on {self}")
elif self.mode == MBR:
if not self.parted_mklabel(self.blockdevice.device, "msdos"):
raise KeyError(f"Could not create a MSDOS label on {self}")
self.blockdevice.flush_cache()
prev_partition = None
# We then iterate the partitions in order
for partition in layout.get('partitions', []):
# We don't want to re-add an existing partition (those containing a UUID already)
if partition.get('wipe', False) and not partition.get('PARTUUID', None):
print(_("Adding partition...."))
start = partition.get('start') or (
prev_partition and f'{prev_partition["device_instance"].end_sectors}s' or DEFAULT_PARTITION_START)
partition['device_instance'] = self.add_partition(partition.get('type', 'primary'),
start=start,
end=partition.get('size', '100%'),
partition_format=partition.get('filesystem', {}).get('format', 'btrfs'))
# TODO: device_instance some times become None
# print('Device instance:', partition['device_instance'])
elif (partition_uuid := partition.get('PARTUUID')) and (partition_instance := self.blockdevice.get_partition(uuid=partition_uuid)):
print(_("Re-using partition instance: {}").format(partition_instance))
partition['device_instance'] = partition_instance
else:
raise ValueError(f"{self}.load_layout() doesn't know how to continue without a new partition definition or a UUID ({partition.get('PARTUUID')}) on the device ({self.blockdevice.get_partition(uuid=partition.get('PARTUUID'))}).")
if partition.get('filesystem', {}).get('format', False):
# needed for backward compatibility with the introduction of the new "format_options"
format_options = partition.get('options',[]) + partition.get('filesystem',{}).get('format_options',[])
if partition.get('encrypted', False):
if not partition['device_instance']:
raise DiskError(f"Internal error caused us to loose the partition. Please report this issue upstream!")
if not partition.get('!password'):
if not storage['arguments'].get('!encryption-password'):
if storage['arguments'] == 'silent':
raise ValueError(f"Missing encryption password for {partition['device_instance']}")
from ..user_interaction import get_password
prompt = str(_('Enter a encryption password for {}').format(partition['device_instance']))
storage['arguments']['!encryption-password'] = get_password(prompt)
partition['!password'] = storage['arguments']['!encryption-password']
if partition.get('mountpoint',None):
loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['mountpoint']).name}loop"
else:
loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['device_instance'].path).name}"
partition['device_instance'].encrypt(password=partition['!password'])
# Immediately unlock the encrypted device to format the inner volume
with luks2(partition['device_instance'], loopdev, partition['!password'], auto_unmount=True) as unlocked_device:
if not partition.get('wipe'):
if storage['arguments'] == 'silent':
raise ValueError(f"Missing fs-type to format on newly created encrypted partition {partition['device_instance']}")
else:
if not partition.get('filesystem'):
partition['filesystem'] = {}
if not partition['filesystem'].get('format', False):
while True:
partition['filesystem']['format'] = input(f"Enter a valid fs-type for newly encrypted partition {partition['filesystem']['format']}: ").strip()
if not partition['filesystem']['format'] or valid_fs_type(partition['filesystem']['format']) is False:
print(_("You need to enter a valid fs-type in order to continue. See `man parted` for valid fs-type's."))
continue
break
unlocked_device.format(partition['filesystem']['format'], options=format_options)
elif partition.get('wipe', False):
if not partition['device_instance']:
raise DiskError(f"Internal error caused us to loose the partition. Please report this issue upstream!")
partition['device_instance'].format(partition['filesystem']['format'], options=format_options)
if partition.get('boot', False):
log(f"Marking partition {partition['device_instance']} as bootable.")
self.set(self.partuuid_to_index(partition['device_instance'].uuid), 'boot on')
prev_partition = partition
def find_partition(self, mountpoint :str) -> Partition:
for partition in self.blockdevice:
if partition.target_mountpoint == mountpoint or partition.mountpoint == mountpoint:
return partition
def partprobe(self) -> bool:
try:
SysCommand(f'partprobe {self.blockdevice.device}')
except SysCallError as error:
log(f"Could not execute partprobe: {error!r}", level=logging.ERROR, fg="red")
raise DiskError(f"Could not run partprobe on {self.blockdevice.device}: {error!r}")
return True
def raw_parted(self, string: str) -> SysCommand:
if (cmd_handle := SysCommand(f'/usr/bin/parted -s {string}')).exit_code != 0:
log(f"Parted ended with a bad exit code: {cmd_handle}", level=logging.ERROR, fg="red")
time.sleep(0.5)
return cmd_handle
def parted(self, string: str) -> bool:
""" """
Format can be given an overriding path, for instance /dev/null to test Performs a parted execution of the given string
the formatting functionality and in essence the support for the given filesystem.
:param string: A raw string passed to /usr/bin/parted -s <string>
:type string: str
""" """
if (parted_handle := self.raw_parted(string)).exit_code == 0:
# don't touch existing partitions return self.partprobe()
create_or_modify_parts = [p for p in partitions if p.is_create_or_modify()]
self._validate_partitions(create_or_modify_parts)
for part_mod in create_or_modify_parts:
# partition will be encrypted
if self._enc_config is not None and part_mod in self._enc_config.partitions:
device_handler.format_encrypted(
part_mod.safe_dev_path,
part_mod.mapper_name,
part_mod.safe_fs_type,
self._enc_config,
)
else:
device_handler.format(part_mod.safe_fs_type, part_mod.safe_dev_path)
# synchronize with udev before using lsblk
udev_sync()
lsblk_info = device_handler.fetch_part_info(part_mod.safe_dev_path)
part_mod.partn = lsblk_info.partn
part_mod.partuuid = lsblk_info.partuuid
part_mod.uuid = lsblk_info.uuid
def _validate_partitions(self, partitions: list[PartitionModification]) -> None:
checks = {
# verify that all partitions have a path set (which implies that they have been created)
lambda x: x.dev_path is None: ValueError('When formatting, all partitions must have a path set'),
# crypto luks is not a valid file system type
lambda x: x.fs_type is FilesystemType.CRYPTO_LUKS: ValueError('Crypto luks cannot be set as a filesystem type'),
# file system type must be set
lambda x: x.fs_type is None: ValueError('File system type must be set for modification'),
}
for check, exc in checks.items():
found = next(filter(check, partitions), None)
if found is not None:
raise exc
def perform_lvm_operations(self) -> None:
info('Setting up LVM config...')
if not self._disk_config.lvm_config:
return
if self._enc_config:
self._setup_lvm_encrypted(
self._disk_config.lvm_config,
self._enc_config,
)
else: else:
self._setup_lvm(self._disk_config.lvm_config) raise DiskError(f"Parted failed to add a partition: {parted_handle}")
self._format_lvm_vols(self._disk_config.lvm_config)
def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption) -> None: def use_entire_disk(self, root_filesystem_type :str = 'ext4') -> Partition:
if enc_config.encryption_type == EncryptionType.LVM_ON_LUKS: # TODO: Implement this with declarative profiles instead.
enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False) raise ValueError("Installation().use_entire_disk() has to be re-worked.")
self._setup_lvm(lvm_config, enc_mods) def add_partition(self, partition_type :str, start :str, end :str, partition_format :Optional[str] = None) -> Partition:
self._format_lvm_vols(lvm_config) log(f'Adding partition to {self.blockdevice}, {start}->{end}', level=logging.INFO)
# Don't close LVM or LUKS during setup - keep everything active previous_partition_uuids = {partition.uuid for partition in self.blockdevice.partitions.values()}
# The installation phase will handle unlocking and mounting
# Closing causes "parent leaked" and lvchange errors
elif enc_config.encryption_type == EncryptionType.LUKS_ON_LVM:
self._setup_lvm(lvm_config)
enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False)
self._format_lvm_vols(lvm_config, enc_vols)
# Lock LUKS devices but keep LVM active if self.mode == MBR:
# LVM volumes must remain active for later re-unlock during installation if len(self.blockdevice.partitions) > 3:
for luks in enc_vols.values(): DiskError("Too many partitions on disk, MBR disks can only have 3 primary partitions")
luks.lock()
def _setup_lvm( if partition_format:
self, parted_string = f'{self.blockdevice.device} mkpart {partition_type} {partition_format} {start} {end}'
lvm_config: LvmConfiguration, else:
enc_mods: dict[PartitionModification, Luks2] = {}, parted_string = f'{self.blockdevice.device} mkpart {partition_type} {start} {end}'
) -> None:
self._lvm_create_pvs(lvm_config, enc_mods)
for vg in lvm_config.vol_groups: log(f"Adding partition using the following parted command: {parted_string}", level=logging.DEBUG)
pv_dev_paths = self._get_all_pv_dev_paths(vg.pvs, enc_mods)
lvm_vg_create(pv_dev_paths, vg.name) if self.parted(parted_string):
count = 0
while count < 10:
new_uuid = None
new_uuid_set = (previous_partition_uuids ^ {partition.uuid for partition in self.blockdevice.partitions.values()})
# figure out what the actual available size in the group is if len(new_uuid_set) > 0:
vg_info = lvm_group_info(vg.name) new_uuid = new_uuid_set.pop()
if not vg_info: if new_uuid:
raise ValueError('Unable to fetch VG info') try:
return self.blockdevice.get_partition(new_uuid)
# the actual available LVM Group size will be smaller than the except Exception as err:
# total PVs size due to reserved metadata storage etc. log(f'Blockdevice: {self.blockdevice}', level=logging.ERROR, fg="red")
# so we'll have a look at the total avail. size, check the delta log(f'Partitions: {self.blockdevice.partitions}', level=logging.ERROR, fg="red")
# to the desired sizes and subtract some equally from the actually log(f'Partition set: {new_uuid_set}', level=logging.ERROR, fg="red")
# created volume log(f'New UUID: {[new_uuid]}', level=logging.ERROR, fg="red")
avail_size = vg_info.vg_size log(f'get_partition(): {self.blockdevice.get_partition}', level=logging.ERROR, fg="red")
desired_size = sum([vol.length for vol in vg.volumes], Size(0, Unit.B, SectorSize.default())) raise err
else:
delta = desired_size - avail_size count += 1
delta_bytes = delta.convert(Unit.B) log(f"Could not get UUID for partition. Waiting before retry attempt {count} of 10 ...",level=logging.DEBUG)
time.sleep(float(storage['arguments'].get('disk-sleep', 0.2)))
# Round the offset up to the next physical extent (PE, 4 MiB by default)
# to ensure lvcreate`s internal rounding doesn`t consume space reserved
# for subsequent logical volumes.
pe_bytes = Size(4, Unit.MiB, SectorSize.default()).convert(Unit.B)
pe_count = math.ceil(delta_bytes.value / pe_bytes.value)
rounded_offset = pe_count * pe_bytes.value
max_vol_offset = Size(rounded_offset, Unit.B, SectorSize.default())
max_vol = max(vg.volumes, key=lambda x: x.length)
for lv in vg.volumes:
offset = max_vol_offset if lv == max_vol else None
debug(f'vg: {vg.name}, vol: {lv.name}, offset: {offset}')
lvm_vol_create(vg.name, lv, offset)
while True:
debug('Fetching LVM volume info')
lv_info = lvm_vol_info(lv.name)
if lv_info is not None:
break
time.sleep(1)
self._lvm_vol_handle_e2scrub(vg)
def _format_lvm_vols(
self,
lvm_config: LvmConfiguration,
enc_vols: dict[LvmVolume, Luks2] = {},
) -> None:
for vol in lvm_config.get_all_volumes():
if enc_vol := enc_vols.get(vol, None):
if not enc_vol.mapper_dev:
raise ValueError('No mapper device defined')
path = enc_vol.mapper_dev
else: else:
path = vol.safe_dev_path log("Add partition is exiting due to excessive wait time", level=logging.ERROR, fg="red")
raise DiskError(f"New partition never showed up after adding new partition on {self}.")
# wait a bit otherwise the mkfs will fail as it can't # TODO: This should never be able to happen
# find the mapper device yet log(f"Could not find the new PARTUUID after adding the partition.", level=logging.ERROR, fg="red")
device_handler.format(vol.fs_type, path) log(f"Previous partitions: {previous_partition_uuids}", level=logging.ERROR, fg="red")
log(f"New partitions: {(previous_partition_uuids ^ {partition.uuid for partition in self.blockdevice.partitions.values()})}", level=logging.ERROR, fg="red")
raise DiskError(f"Could not add partition using: {parted_string}")
if vol.fs_type == FilesystemType.BTRFS: def set_name(self, partition: int, name: str) -> bool:
device_handler.create_lvm_btrfs_subvolumes(path, vol.btrfs_subvols, vol.mount_options) return self.parted(f'{self.blockdevice.device} name {partition + 1} "{name}"') == 0
def _lvm_create_pvs( def set(self, partition: int, string: str) -> bool:
self, log(f"Setting {string} on (parted) partition index {partition+1}", level=logging.INFO)
lvm_config: LvmConfiguration, return self.parted(f'{self.blockdevice.device} set {partition + 1} {string}') == 0
enc_mods: dict[PartitionModification, Luks2] = {},
) -> None:
pv_paths: set[Path] = set()
for vg in lvm_config.vol_groups: def parted_mklabel(self, device: str, disk_label: str) -> bool:
pv_paths |= self._get_all_pv_dev_paths(vg.pvs, enc_mods) log(f"Creating a new partition label on {device}", level=logging.INFO, fg="yellow")
# Try to unmount devices before attempting to run mklabel
try:
SysCommand(f'bash -c "umount {device}?"')
except:
pass
lvm_pv_create(pv_paths) self.partprobe()
worked = self.raw_parted(f'{device} mklabel {disk_label}').exit_code == 0
self.partprobe()
def _get_all_pv_dev_paths( return worked
self,
pvs: list[PartitionModification],
enc_mods: dict[PartitionModification, Luks2] = {},
) -> set[Path]:
pv_paths: set[Path] = set()
for pv in pvs:
if enc_pv := enc_mods.get(pv, None):
if mapper := enc_pv.mapper_dev:
pv_paths.add(mapper)
else:
pv_paths.add(pv.safe_dev_path)
return pv_paths
def _encrypt_lvm_vols(
self,
lvm_config: LvmConfiguration,
enc_config: DiskEncryption,
lock_after_create: bool = True,
) -> dict[LvmVolume, Luks2]:
enc_vols: dict[LvmVolume, Luks2] = {}
for vol in lvm_config.get_all_volumes():
if vol in enc_config.lvm_volumes:
luks_handler = device_handler.encrypt(
vol.safe_dev_path,
vol.mapper_name,
enc_config.encryption_password,
lock_after_create,
iter_time=enc_config.iter_time,
)
enc_vols[vol] = luks_handler
return enc_vols
def _encrypt_partitions(
self,
enc_config: DiskEncryption,
lock_after_create: bool = True,
) -> dict[PartitionModification, Luks2]:
enc_mods: dict[PartitionModification, Luks2] = {}
for mod in self._disk_config.device_modifications:
partitions = mod.partitions
# don't touch existing partitions
filtered_part = [p for p in partitions if not p.exists()]
self._validate_partitions(filtered_part)
enc_mods = {}
for part_mod in filtered_part:
if part_mod in enc_config.partitions:
luks_handler = device_handler.encrypt(
part_mod.safe_dev_path,
part_mod.mapper_name,
enc_config.encryption_password,
lock_after_create=lock_after_create,
iter_time=enc_config.iter_time,
)
enc_mods[part_mod] = luks_handler
return enc_mods
def _lvm_vol_handle_e2scrub(self, vol_gp: LvmVolumeGroup) -> None:
# from arch wiki:
# If a logical volume will be formatted with ext4, leave at least 256 MiB
# free space in the volume group to allow using e2scrub
if any([vol.fs_type == FilesystemType.EXT4 for vol in vol_gp.volumes]):
largest_vol = max(vol_gp.volumes, key=lambda x: x.length)
lvm_vol_reduce(
largest_vol.safe_dev_path,
Size(256, Unit.MiB, SectorSize.default()),
)

Some files were not shown because too many files have changed in this diff Show More