Compare commits
No commits in common. "master" and "v2.4.2" have entirely different histories.
8
.flake8
8
.flake8
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: [archlinux]
|
|
||||||
custom: ['https://archlinux.org/donate/']
|
|
||||||
|
|
@ -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`
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 .
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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/*
|
||||||
|
|
|
||||||
|
|
@ -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/*
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
2
.pypirc
2
.pypirc
|
|
@ -3,4 +3,4 @@ index-servers =
|
||||||
pypi
|
pypi
|
||||||
|
|
||||||
[pypi]
|
[pypi]
|
||||||
repository = https://upload.pypi.org/legacy/
|
repository = https://upload.pypi.org/legacy/
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
84
PKGBUILD
84
PKGBUILD
|
|
@ -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
293
README.md
|
|
@ -6,169 +6,143 @@
|
||||||
[](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml)
|
[](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)
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
// Place cursor configuration here.
|
|
||||||
// Example:
|
|
||||||
// cursor {
|
|
||||||
// xcursor-theme "Adwaita"
|
|
||||||
// xcursor-size 24
|
|
||||||
// }
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
// Place per-output configuration here.
|
|
||||||
// Example:
|
|
||||||
// output "DP-1" {
|
|
||||||
// mode "2560x1440@165"
|
|
||||||
// position x=0 y=0
|
|
||||||
// scale 1
|
|
||||||
// }
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
from archinstall.default_profiles.profile import Profile, ProfileType
|
|
||||||
|
|
||||||
|
|
||||||
class MinimalProfile(Profile):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__(
|
|
||||||
'Minimal',
|
|
||||||
ProfileType.Minimal,
|
|
||||||
)
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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.')
|
|
||||||
|
|
@ -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']
|
|
||||||
|
|
@ -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}')
|
|
||||||
|
|
@ -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']
|
|
||||||
|
|
@ -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']
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
@ -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']
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
@ -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']
|
|
||||||
|
|
@ -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']
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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())
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
@ -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 *
|
||||||
|
|
@ -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}")
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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])
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
Loading…
Reference in New Issue