Compare commits
No commits in common. "master" and "v2.1.4-RC2" have entirely different histories.
master
...
v2.1.4-RC2
8
.flake8
8
.flake8
|
|
@ -1,8 +0,0 @@
|
|||
[flake8]
|
||||
count = True
|
||||
ignore = W191,W503,E704,E203
|
||||
max-complexity = 40
|
||||
max-line-length = 160
|
||||
show-source = True
|
||||
statistics = True
|
||||
exclude = .git,__pycache__,build,docs,actions-runner
|
||||
|
|
@ -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 +0,0 @@
|
|||
on: [ push, pull_request ]
|
||||
name: Bandit security checkup
|
||||
jobs:
|
||||
bandit:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- run: pacman --noconfirm -Syu bandit
|
||||
- name: Security checkup with Bandit
|
||||
run: bandit -r archinstall || exit 0
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
on: [ push, pull_request ]
|
||||
name: flake8 linting
|
||||
jobs:
|
||||
flake8:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- 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
|
||||
# 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
|
||||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
- 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
|
||||
|
|
@ -3,37 +3,35 @@
|
|||
name: Build Arch ISO 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
|
||||
- '.github/**'
|
||||
- 'docs/**'
|
||||
- '**.editorconfig'
|
||||
- '**.gitignore'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- 'PKGBUILD'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
image: archlinux:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- run: pwd
|
||||
- run: find .
|
||||
- run: cat /etc/os-release
|
||||
- run: pacman-key --init
|
||||
- run: pacman --noconfirm -Sy archlinux-keyring
|
||||
- run: ./test_tooling/mkarchiso/build_iso.sh
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: Arch Live ISO
|
||||
path: /tmp/archlive/out/*.iso
|
||||
- uses: actions/checkout@v2
|
||||
- run: pwd
|
||||
- run: find .
|
||||
- run: cat /etc/os-release
|
||||
- run: mkdir -p /tmp/archlive/airootfs/root/archinstall-git; cp -r . /tmp/archlive/airootfs/root/archinstall-git
|
||||
- run: echo "pip uninstall archinstall -y; cd archinstall-git; python setup.py install; echo 'Type python -m archinstall to launch archinstall'" > /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-setuptools" >> /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:
|
||||
name: Arch Live ISO
|
||||
path: /tmp/archlive/out/*.iso
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
on: [ push, pull_request ]
|
||||
name: mypy type checking
|
||||
jobs:
|
||||
mypy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- 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
|
||||
# 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: mypy --version
|
||||
- name: run mypy
|
||||
run: mypy --config-file pyproject.toml
|
||||
|
|
@ -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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- 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 .
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
on: [ push, pull_request ]
|
||||
name: pytest test validation
|
||||
jobs:
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- 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 archinstall dependencies
|
||||
run: pip install --break-system-packages .[dev]
|
||||
- name: Test with pytest
|
||||
run: pytest
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
# This workflow will build Python packages on every commit.
|
||||
|
||||
name: Build archinstall
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- name: Prepare arch
|
||||
run: |
|
||||
pacman-key --init
|
||||
pacman --noconfirm -Sy archlinux-keyring
|
||||
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
|
||||
run: uv build --no-build-isolation --wheel
|
||||
- name: Install archinstall
|
||||
run: |
|
||||
uv pip install dist/*.whl --break-system-packages --system --no-build --no-deps
|
||||
- name: Run archinstall
|
||||
run: |
|
||||
python -V
|
||||
archinstall --script guided -v
|
||||
archinstall --script only_hd -v
|
||||
archinstall --script minimal -v
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: archinstall
|
||||
path: dist/*
|
||||
|
|
@ -1,33 +1,31 @@
|
|||
# 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
|
||||
|
||||
name: Upload archinstall to PyPi
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# IMPORTANT: this permission is mandatory for Trusted Publishing
|
||||
id-token: write
|
||||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
options: --privileged
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- name: Prepare arch
|
||||
run: |
|
||||
pacman-key --init
|
||||
pacman --noconfirm -Sy archlinux-keyring
|
||||
pacman --noconfirm -Syyu
|
||||
pacman --noconfirm -Sy python python-uv python-setuptools python-pip python-pyparted python-pydantic python-textual
|
||||
- name: Build archinstall
|
||||
run: |
|
||||
uv build --no-build-isolation --wheel
|
||||
- name: Publish archinstall to PyPi
|
||||
run: |
|
||||
uv publish --trusted-publishing always
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- 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
|
||||
|
|
@ -7,7 +7,6 @@ SAFETY_LOCK
|
|||
**/**dist
|
||||
**/**.egg*
|
||||
**/**.sh
|
||||
!archinstall/locales/locales_generator.sh
|
||||
**/**.egg-info/
|
||||
**/**build/
|
||||
**/**src/
|
||||
|
|
@ -21,27 +20,4 @@ SAFETY_LOCK
|
|||
**/**.network
|
||||
**/**.target
|
||||
**/**.qcow2
|
||||
**/**.log
|
||||
**/**.fd
|
||||
/test*.py
|
||||
**/archiso
|
||||
/guided.py
|
||||
venv
|
||||
.venv
|
||||
.idea/**
|
||||
**/install.log
|
||||
.DS_Store
|
||||
**/cmd_history.txt
|
||||
**/*.*~
|
||||
/*.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
|
||||
**/test.py
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
# This file contains GitLab CI/CD configuration for the ArchInstall project.
|
||||
# It defines several jobs that get run when a new commit is made, and is comparable to the GitHub workflows.
|
||||
# There is an expectation that a runner exists that has the --privileged flag enabled for the build ISO job to run correctly.
|
||||
# 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.
|
||||
|
||||
image: archlinux/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.
|
||||
before_script:
|
||||
- pacman -Sy
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- test
|
||||
- build
|
||||
- publish
|
||||
|
||||
mypy:
|
||||
stage: lint
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- pacman --noconfirm -Syu python mypy
|
||||
- mypy . --ignore-missing-imports || exit 0
|
||||
|
||||
flake8:
|
||||
stage: lint
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- pacman --noconfirm -Syu python python-pip
|
||||
- python -m pip install --upgrade pip
|
||||
- pip install flake8
|
||||
- flake8 . --count --select=E9,F63,F7 --show-source --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.
|
||||
# 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.
|
||||
.pytest:
|
||||
stage: test
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- pacman --noconfirm -Syu python python-pip
|
||||
- python -m pip install --upgrade pip
|
||||
- pip install pytest
|
||||
- pytest
|
||||
|
||||
# This stage might fail with exit code 137 on a shared runner. This is probably due to the CPU/memory consumption needed to run the build.
|
||||
build_iso:
|
||||
stage: build
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- pwd
|
||||
- find .
|
||||
- cat /etc/os-release
|
||||
- mkdir -p /tmp/archlive/airootfs/root/archinstall-git; cp -r . /tmp/archlive/airootfs/root/archinstall-git
|
||||
- echo "pip uninstall archinstall -y; cd archinstall-git; python setup.py install" > /tmp/archlive/airootfs/root/.zprofile
|
||||
- echo "echo \"This is an unofficial ISO for development and testing of archinstall. No support will be provided.\"" >> /tmp/archlive/airootfs/root/.zprofile
|
||||
- echo "echo \"This ISO was built from Git SHA $CI_COMMIT_SHA\"" >> /tmp/archlive/airootfs/root/.zprofile
|
||||
- echo "echo \"Type archinstall to launch the installer.\"" >> /tmp/archlive/airootfs/root/.zprofile
|
||||
- cat /tmp/archlive/airootfs/root/.zprofile
|
||||
- pacman --noconfirm -S git archiso
|
||||
- cp -r /usr/share/archiso/configs/releng/* /tmp/archlive
|
||||
- echo -e "git\npython\npython-pip\npython-setuptools" >> /tmp/archlive/packages.x86_64
|
||||
- find /tmp/archlive
|
||||
- cd /tmp/archlive; mkarchiso -v -w work/ -o out/ ./
|
||||
artifacts:
|
||||
name: "Arch Live ISO"
|
||||
paths:
|
||||
- /tmp/archlive/out/*.iso
|
||||
expire_in: 1 week
|
||||
|
||||
## This job only runs when a tag is created on the master branch. This is because we do not want to try to publish to PyPi every time we commit.
|
||||
## The following CI/CD variables need to be set to the PyPi username and password in the GitLab project's settings for this stage to work.
|
||||
# * FLIT_USERNAME
|
||||
# * FLIT_PASSWORD
|
||||
publish_pypi:
|
||||
stage: publish
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- pacman --noconfirm -S python python-pip
|
||||
- python -m pip install --upgrade pip
|
||||
- pip install setuptools wheel flit
|
||||
- flit
|
||||
only:
|
||||
- tags
|
||||
except:
|
||||
- branches
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
default_stages: ['pre-commit']
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.16
|
||||
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.1.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
|
||||
6
.pypirc
6
.pypirc
|
|
@ -1,6 +0,0 @@
|
|||
[distutils]
|
||||
index-servers =
|
||||
pypi
|
||||
|
||||
[pypi]
|
||||
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
|
||||
|
|
@ -1,71 +1,51 @@
|
|||
# Contributing to archinstall
|
||||
|
||||
Any contributions through pull requests are welcome as this project aims to be a community based project to ease some Arch Linux installation steps.
|
||||
Bear in mind that in the future this repo might be transferred to the official [GitLab repo under Arch Linux](http://gitlab.archlinux.org/archlinux/) *(if GitLab becomes open to the general public)*.
|
||||
|
||||
Therefore, guidelines and style changes to the code might come into effect as well as guidelines surrounding bug reporting and discussions.
|
||||
|
||||
## Branches
|
||||
|
||||
`master` is currently the default branch, and that's where all future feature work is being done, this means that `master` is a living entity and will most likely never be in a fully stable state.
|
||||
For stable releases, please see the tagged commits.
|
||||
|
||||
Patch releases will be done against their own branches, branched from stable tagged releases and will be named according to the version it will become on release.
|
||||
*(Patches to `v2.1.4` will be done on branch `v2.1.5` for instance)*.
|
||||
Any contributions through pull requests are welcome as this project aims to be a community based project to ease some Arch Linux installation steps.<br>
|
||||
Bear in mind that the future of this repo might be transferred to the official [GitLab repo under Arch Linux](http://gitlab.archlinux.org/archlinux/) *(if they so choose to adopt the project)*.
|
||||
Therefore guidelines and style changes to the code might come into affect as well as guidelines surrounding bug reporting and discussions.
|
||||
|
||||
## Discussions
|
||||
|
||||
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 are also a [archinstall Discord server](https://discord.gg/cqXU88y).
|
||||
|
||||
## Coding convention
|
||||
|
||||
ArchInstall's goal is to follow [PEP8](https://www.python.org/dev/peps/pep-0008/) as best as it can with some minor exceptions.<br>
|
||||
|
||||
Archinstall's goal is to follow [PEP8](https://www.python.org/dev/peps/pep-0008/) as best as it can with some minor exceptions.<br>
|
||||
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
|
||||
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.
|
||||
* [Line length](https://www.python.org/dev/peps/pep-0008/#maximum-line-length) a maximum line length is enforced via flake8 with 160 characters
|
||||
* 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
|
||||
creating formatted strings, double-quoted strings are *preferred* but not required on the outer edges *(
|
||||
Example: `f"Welcome {name}"` rather than `f'Welcome {name}'`)*.
|
||||
* 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 rule are comments that need fine-tuned indentation for documentation purposes.
|
||||
* [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 are consistent within the same code block.
|
||||
* Archinstall should always be saved with **Unix-formatted line endings** and no other platform-specific formats.
|
||||
* [Blank lines](https://www.python.org/dev/peps/pep-0008/#blank-lines) before/after imports and functions are not followed and discouraged. One space is commonly used in archinstall.
|
||||
* Multiple [Imports](https://www.python.org/dev/peps/pep-0008/#imports) on the same line is allowed, but more than five imports should be avoided on any given line. This simply saves up some space at the top of the file *(for non-IDE developers)* and will not be enforced.
|
||||
* [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 *(Example: `f"Welcome {name}"` rather than `f'Welcome {name}'`)*.
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
If you'd like to contribute to the documentation, refer to [this guide](docs/README.md) on how to build the documentation locally.
|
||||
|
||||
## Submitting Changes
|
||||
|
||||
Archinstall uses GitHub's pull-request workflow and all contributions in terms of code should be done through pull requests.<br>
|
||||
Archinstall uses Github's pull-request workflow and all contributions in terms of code should be done through pull requests.<br>
|
||||
|
||||
Anyone interested in archinstall may review your code. One of the core developers will merge your pull request when they
|
||||
think it is ready. For every pull request, we aim to promptly either merge it or say why it is not yet ready; if you go
|
||||
a few days without a reply, please feel free to ping the thread by adding a new comment.
|
||||
Anyone interested in archinstall may review your code. One of the core developers will merge your pull request when they think it is ready.
|
||||
For every pull request, we aim to promptly either merge it or say why it is not yet ready; if you go a few days without a reply, please feel free to ping the thread by adding a new comment.
|
||||
|
||||
To get your pull request merged sooner, you should explain why you are making the change. For example, you can point to
|
||||
a code sample that is outdated in terms of Arch Linux command lines. It is also helpful to add links to online
|
||||
documentation or to the implementation of the code you are changing.
|
||||
To get your pull request merged sooner, you should explain why you are making the change. For example, you can point to a code sample that is outdated in terms of Arch Linux command lines.
|
||||
It is also helpful to add links to online 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
|
||||
squash commits when the pull request is merged.
|
||||
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.
|
||||
|
||||
Maintainer:
|
||||
* Anton Hvornum ([@Torxed](https://github.com/Torxed))
|
||||
At present the current contributors are (alphabetically):
|
||||
|
||||
[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))
|
||||
* 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))
|
||||
|
|
|
|||
92
PKGBUILD
92
PKGBUILD
|
|
@ -1,89 +1,25 @@
|
|||
# Maintainer: David Runge <dvzrv@archlinux.org>
|
||||
# Maintainer: Giancarlo Razzolini <grazzolini@archlinux.org>
|
||||
# Maintainer: Anton Hvornum <torxed@archlinux.org>
|
||||
# Contributor: Anton Hvornum <anton@hvornum.se>
|
||||
# Maintainer: Anton Hvornum <anton@hvornum.se>
|
||||
# Contributor: Giancarlo Razzolini <grazzolini@archlinux.org>
|
||||
# Contributor: demostanis worlds <demostanis@protonmail.com>
|
||||
|
||||
pkgname=archinstall
|
||||
pkgver=4.3
|
||||
pkgname=archinstall-git
|
||||
pkgver=$(git describe --long | sed 's/\([^-]*-g\)/r\1/;s/-/./g')
|
||||
pkgrel=1
|
||||
pkgdesc="Just another guided/automated Arch Linux installer with a twist"
|
||||
arch=(any)
|
||||
arch=('any')
|
||||
url="https://github.com/archlinux/archinstall"
|
||||
license=(GPL-3.0-only)
|
||||
depends=(
|
||||
'arch-install-scripts'
|
||||
'btrfs-progs'
|
||||
'coreutils'
|
||||
'cryptsetup'
|
||||
'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=(
|
||||
$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/$pkgver.tar.gz
|
||||
$pkgname-$pkgver.tar.gz.sig::$url/releases/download/$pkgver/$pkgname-$pkgver.tar.gz.sig
|
||||
)
|
||||
sha512sums=()
|
||||
b2sums=()
|
||||
validpgpkeys=('8AA2213C8464C82D879C8127D4B58E897A929F2E') # torxed@archlinux.org
|
||||
|
||||
check() {
|
||||
cd $pkgname-$pkgver
|
||||
ruff check
|
||||
}
|
||||
|
||||
pkgver() {
|
||||
cd $pkgname-$pkgver
|
||||
|
||||
awk '$1 ~ /^__version__/ {gsub("\"", ""); print $3}' archinstall/__init__.py
|
||||
}
|
||||
license=('GPL')
|
||||
depends=('python')
|
||||
makedepends=('python-setuptools')
|
||||
provides=('python-archinstall')
|
||||
conflicts=('archinstall' 'python-archinstall' 'python-archinstall-git')
|
||||
|
||||
build() {
|
||||
cd $pkgname-$pkgver
|
||||
|
||||
python -m build --wheel --no-isolation
|
||||
PYTHONDONTWRITEBYTECODE=1 make man -C docs
|
||||
cd "$startdir"
|
||||
python setup.py build
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$pkgname-$pkgver"
|
||||
|
||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||
install -vDm 644 docs/_build/man/archinstall.1 -t "$pkgdir/usr/share/man/man1/"
|
||||
cd "$startdir"
|
||||
python setup.py install --root="${pkgdir}" --optimize=1 --skip-build
|
||||
}
|
||||
|
|
|
|||
319
README.md
319
README.md
|
|
@ -1,269 +1,150 @@
|
|||
<!-- <div align="center"> -->
|
||||
<img src="https://github.com/archlinux/archinstall/raw/master/docs/logo.png" alt="drawing" width="200"/>
|
||||
|
||||
<!-- </div> -->
|
||||
# Arch Installer
|
||||
[](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml)
|
||||
<!-- </div> -->
|
||||
|
||||
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/cqXU88y) server
|
||||
* archinstall [matrix.org](https://app.element.io/#/room/#archinstall:matrix.org) channel
|
||||
* archinstall [#archinstall@freenode (IRC)](irc://#archinstall@FreeNode)
|
||||
* archinstall [documentation](https://python-archinstall.readthedocs.io/en/latest/index.html)
|
||||
|
||||
* archinstall [discord](https://discord.gg/aDeMffrxNg) server
|
||||
* archinstall [#archinstall:matrix.org](https://matrix.to/#/#archinstall:matrix.org) Matrix channel
|
||||
* archinstall [#archinstall@irc.libera.chat:6697](https://web.libera.chat/?channel=#archinstall)
|
||||
* archinstall [documentation](https://archinstall.archlinux.page/)
|
||||
|
||||
# Installation & Usage
|
||||
> [!TIP]
|
||||
> In the ISO you are root by default. Use sudo if running from an existing system.
|
||||
|
||||
```shell
|
||||
pacman-key --init
|
||||
pacman -Sy archinstall
|
||||
archinstall
|
||||
```
|
||||
$ sudo pacman -S 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](examples/guided.py) installer
|
||||
|
||||
Upgrading archinstall on the ISO needs to be done via a full system upgrade using
|
||||
Assuming you are on a Arch Linux live-ISO and booted into EFI mode.
|
||||
|
||||
```shell
|
||||
pacman -Syu
|
||||
```
|
||||
# python -m archinstall guided
|
||||
|
||||
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.
|
||||
In case one runs into this issue, any of the following can be used
|
||||
# Mission Statement
|
||||
|
||||
* 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
|
||||
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.
|
||||
|
||||
## Running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
`archinstall` can be run with a JSON configuration file. There are 2 different configuration files to consider,
|
||||
the `user_configuration.json` contains all general installation configuration, whereas the `user_credentials.json`
|
||||
contains the sensitive user configuration such as user password, root password, and encryption password.
|
||||
|
||||
An example of the user configuration file can be found here
|
||||
[configuration file](https://github.com/archlinux/archinstall/blob/master/examples/config-sample.json)
|
||||
and 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
|
||||
points and then going to `Save configuration`.
|
||||
|
||||
To load the configuration file into `archinstall` run the following command
|
||||
```shell
|
||||
archinstall --config <path to user config file or URL> --creds <path to user credentials config file or URL>
|
||||
```
|
||||
|
||||
### Credentials configuration file encryption
|
||||
By default, all user account credentials are hashed with `yescrypt` and only the hash is stored in the saved `user_credentials.json` file.
|
||||
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.
|
||||
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
|
||||
|
||||
|
||||
# Help or Issues
|
||||
|
||||
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:
|
||||
* Provide the stacktrace of the output if applicable
|
||||
* 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`.
|
||||
Archinstall has one fundamental function which is to be a flexible library to manage services, packages and other aspects inside the installed system. This library is in turn used by the provided guided installer but is also for anyone who wants to script their own installations.
|
||||
|
||||
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 interactive installation
|
||||
You could just copy [guided.py](examples/guided.py) as a starting point.
|
||||
|
||||
For an example of a fully scripted, interactive installation please refer to the example
|
||||
[interactive_installation.py](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py)
|
||||
But assuming you're building your own ISO and want to create an automated install process, or you want to install virtual machines on to local disk images.<br>
|
||||
This is probably what you'll need, a [minimal example](examples/minimal.py) of how to install using archinstall as a Python library.
|
||||
|
||||
```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_disks())
|
||||
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
|
||||
[full_automated_installation.py](https://github.com/archlinux/archinstall/blob/master/examples/full_automated_installation.py)
|
||||
# First, we configure the basic filesystem layout
|
||||
with archinstall.Filesystem(harddrive, archinstall.GPT) as fs:
|
||||
# We create a filesystem layout that will use the entire drive
|
||||
# (this is a helper function, you can partition manually as well)
|
||||
fs.use_entire_disk(root_filesystem_type='btrfs')
|
||||
|
||||
# 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)
|
||||
- [Server](https://github.com/archlinux/archinstall/tree/master/archinstall/default_profiles/servers)
|
||||
# Set the flag for encrypted to allow for encryption and then encrypt
|
||||
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
|
||||
[default profiles](https://github.com/archlinux/archinstall/tree/master/archinstall/default_profiles)
|
||||
with archinstall.luks2(root, 'luksloop', disk_password) as unlocked_root:
|
||||
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.
|
||||
* 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.
|
||||
* Install additional packages *(nano, wget, git)*
|
||||
* Installs a profile with a window manager called [awesome](https://github.com/archlinux/archinstall/blob/master/profiles/awesome.py) *(more on profile installations in the [documentation](https://python-archinstall.readthedocs.io/en/latest/archinstall/Profile.html))*.
|
||||
|
||||
> **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 a [unattended](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](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](profiles/52-54-00-12-34-56.py). If it's found, the unattended installation will commence and source that profile as it's installation proceedure.
|
||||
|
||||
# Help
|
||||
|
||||
Submit an issue on Github, or submit a post in the discord help channel.<br>
|
||||
When doing so, attach any `install-session_*.log` to the issue ticket which can be found under `~/.cache/archinstall/`.
|
||||
|
||||
# Testing
|
||||
|
||||
## 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,
|
||||
replace the archinstall version with a newer one and execute the subsequent steps defined below.
|
||||
If you want to test a commit, branch or bleeding edge release from the repository using the vanilla Arch Live ISO image, 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
|
||||
2. Install the build requirements with `pacman -Sy; pacman -S git python-pip gcc pkgconf`
|
||||
*(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`
|
||||
4. Now clone the latest repository with `git clone https://github.com/archlinux/archinstall`
|
||||
5. Enter the repository with `cd archinstall`
|
||||
*At this stage, you can choose to check out a feature branch for instance with `git checkout v2.3.1-rc1`*
|
||||
6. To run the source code, there are 2 different options:
|
||||
- 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
|
||||
- Installing the branch version with `pip install --break-system-packages .` and `archinstall`
|
||||
1. You need a working network connection
|
||||
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)*
|
||||
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`
|
||||
5. Enter the repository with `cd archinstall`
|
||||
*At this stage, you can choose to check out a feature branch for instance with `git checkout torxed-v2.2.0`*
|
||||
6. Build the project and install it using `python setup.py install`
|
||||
|
||||
After this, running archinstall with `python -m archinstall` will run against whatever branch you chose in step 5.
|
||||
|
||||
## Without a Live ISO Image
|
||||
|
||||
To test this without a live ISO, the simplest approach is to use a local image and create a loop device.<br>
|
||||
This can be done by installing `pacman -S arch-install-scripts util-linux` locally and doing the following:
|
||||
|
||||
# truncate -s 20G testimage.img
|
||||
# losetup --partscan --show ./testimage.img
|
||||
# dd if=/dev/zero of=./testimage.img bs=1G count=5
|
||||
# losetup -fP ./testimage.img
|
||||
# losetup -a | grep "testimage.img" | awk -F ":" '{print $1}'
|
||||
# pip install --upgrade archinstall
|
||||
# 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
|
||||
# python -m archinstall 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/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>
|
||||
`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>
|
||||
*(You'd actually need to do some EFI magic in order to point the EFI vars to the partition 0 in the test medium, so this won't work entirely out of the box, but that gives you a general idea of what we're going for here)*
|
||||
This will create a *5GB* `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,<br>
|
||||
~~you can use qemu/kvm to boot the test media.~~ *(You'd actually need to do some EFI magic in order to point the EFI vars to the partition 0 in the test medium so this won't work entirely out of the box, but gives you a general idea of what we're going for here)*
|
||||
|
||||
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.
|
||||
|
||||
## 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,61 @@
|
|||
from archinstall.lib.plugins import plugin
|
||||
from .lib.general import *
|
||||
from .lib.disk import *
|
||||
from .lib.user_interaction import *
|
||||
from .lib.exceptions import *
|
||||
from .lib.installer import *
|
||||
from .lib.profiles import *
|
||||
from .lib.luks import *
|
||||
from .lib.mirrors import *
|
||||
from .lib.networking import *
|
||||
from .lib.locale_helpers import *
|
||||
from .lib.services import *
|
||||
from .lib.packages import *
|
||||
from .lib.output import *
|
||||
from .lib.storage import *
|
||||
from .lib.hardware import *
|
||||
|
||||
__all__ = ['plugin']
|
||||
__version__ = "2.1.4"
|
||||
|
||||
## Basic version of arg.parse() supporting:
|
||||
## --key=value
|
||||
## --boolean
|
||||
arguments = {}
|
||||
positionals = []
|
||||
for arg in sys.argv[1:]:
|
||||
if '--' == arg[:2]:
|
||||
if '=' in arg:
|
||||
key, val = [x.strip() for x in arg[2:].split('=', 1)]
|
||||
else:
|
||||
key, val = arg[2:], True
|
||||
arguments[key] = val
|
||||
else:
|
||||
positionals.append(arg)
|
||||
|
||||
|
||||
# TODO: Learn the dark arts of argparse...
|
||||
# (I summon thee dark spawn of cPython)
|
||||
|
||||
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'))
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
sys.argv.append('guided')
|
||||
|
||||
try:
|
||||
script = Script(sys.argv[1])
|
||||
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,6 @@
|
|||
import archinstall
|
||||
import sys
|
||||
|
||||
from archinstall.main import main
|
||||
import os
|
||||
|
||||
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,671 +0,0 @@
|
|||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
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.dataclasses import dataclass as p_dataclass
|
||||
|
||||
from archinstall.lib.crypt import decrypt
|
||||
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.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: 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')
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
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=str,
|
||||
default=None,
|
||||
help='File path to a plugin 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:
|
||||
plugin_path = Path(args.plugin)
|
||||
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 _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,253 +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:
|
||||
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_selected_by_value(preset.value if preset else None)
|
||||
|
||||
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,255 +0,0 @@
|
|||
import json
|
||||
import readline
|
||||
import stat
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from archinstall.lib.args import ArchConfig, ArchConfigType
|
||||
from archinstall.lib.crypt import encrypt
|
||||
from archinstall.lib.log import debug, logger, warn
|
||||
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||
from archinstall.lib.menu.util import get_password, prompt_dir
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.lib.utils.format import as_key_value_pair
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
class ConfigurationOutput:
|
||||
def __init__(self, config: ArchConfig):
|
||||
"""
|
||||
Configuration output handler to parse the existing
|
||||
configuration data structure and prepare for output on the
|
||||
console and for saving it to configuration files
|
||||
|
||||
:param config: Archinstall configuration object
|
||||
:type config: ArchConfig
|
||||
"""
|
||||
|
||||
self._config = config
|
||||
self._default_save_path = logger.directory
|
||||
self._user_config_file = Path('user_configuration.json')
|
||||
self._user_creds_file = Path('user_credentials.json')
|
||||
|
||||
@property
|
||||
def user_configuration_file(self) -> Path:
|
||||
return self._user_config_file
|
||||
|
||||
@property
|
||||
def user_credentials_file(self) -> Path:
|
||||
return self._user_creds_file
|
||||
|
||||
def user_config_to_json(self) -> str:
|
||||
config = self._config.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._config.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 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._config.plain_cfg().items():
|
||||
cfg[key.text()] = value
|
||||
|
||||
for config_type, obj in self._config.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
|
||||
|
||||
async def confirm_config(self) -> bool:
|
||||
header = f'{tr("The specified configuration will be applied")}. '
|
||||
header += tr('Would you like to continue?') + '\n'
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_preview_for_all(lambda x: self.user_config_to_json())
|
||||
|
||||
result = await Confirmation(
|
||||
group=group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
preset=True,
|
||||
preview_location='bottom',
|
||||
preview_header=tr('Configuration preview'),
|
||||
).show()
|
||||
|
||||
if not result.get_value():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _is_valid_path(self, dest_path: Path) -> bool:
|
||||
dest_path_ok = dest_path.exists() and dest_path.is_dir()
|
||||
if not dest_path_ok:
|
||||
warn(
|
||||
f'Destination directory {dest_path.resolve()} does not exist or is not a directory\n.',
|
||||
'Configuration files can not be saved',
|
||||
)
|
||||
return dest_path_ok
|
||||
|
||||
def save_user_config(self, dest_path: Path) -> None:
|
||||
if self._is_valid_path(dest_path):
|
||||
target = dest_path / self._user_config_file
|
||||
target.write_text(self.user_config_to_json())
|
||||
target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
|
||||
|
||||
def save_user_creds(
|
||||
self,
|
||||
dest_path: Path,
|
||||
password: str | None = None,
|
||||
) -> None:
|
||||
data = self.user_credentials_to_json()
|
||||
|
||||
if password:
|
||||
data = encrypt(password, data)
|
||||
|
||||
if self._is_valid_path(dest_path):
|
||||
target = dest_path / self._user_creds_file
|
||||
target.write_text(data)
|
||||
target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
|
||||
|
||||
def save(
|
||||
self,
|
||||
dest_path: Path | None = None,
|
||||
creds: bool = False,
|
||||
password: str | None = None,
|
||||
) -> None:
|
||||
save_path = dest_path or self._default_save_path
|
||||
|
||||
if self._is_valid_path(save_path):
|
||||
self.save_user_config(save_path)
|
||||
if creds:
|
||||
self.save_user_creds(save_path, password=password)
|
||||
|
||||
|
||||
async def save_config(config: ArchConfig) -> None:
|
||||
def preview(item: MenuItem) -> str | None:
|
||||
match item.value:
|
||||
case 'user_config':
|
||||
serialized = config_output.user_config_to_json()
|
||||
return f'{config_output.user_configuration_file}\n{serialized}'
|
||||
case 'user_creds':
|
||||
if maybe_serial := config_output.user_credentials_to_json():
|
||||
return f'{config_output.user_credentials_file}\n{maybe_serial}'
|
||||
return tr('No configuration')
|
||||
case 'all':
|
||||
output = [str(config_output.user_configuration_file)]
|
||||
config_output.user_credentials_to_json()
|
||||
output.append(str(config_output.user_credentials_file))
|
||||
return '\n'.join(output)
|
||||
return None
|
||||
|
||||
config_output = ConfigurationOutput(config)
|
||||
|
||||
items = [
|
||||
MenuItem(
|
||||
tr('Save user configuration (including disk layout)'),
|
||||
value='user_config',
|
||||
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)
|
||||
result = await Selection[str](
|
||||
group,
|
||||
allow_skip=True,
|
||||
preview_location='right',
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return
|
||||
case ResultType.Selection:
|
||||
save_option = result.get_value()
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
readline.set_completer_delims('\t\n=')
|
||||
readline.parse_and_bind('tab: complete')
|
||||
|
||||
dest_path = await prompt_dir(
|
||||
tr('Enter a directory for the configuration(s) to be saved') + '\n',
|
||||
allow_skip=True,
|
||||
)
|
||||
|
||||
if not dest_path:
|
||||
return
|
||||
|
||||
header = tr('Do you want to save the configuration file(s) to {}?').format(dest_path)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
if password:
|
||||
enc_password = password.plaintext
|
||||
|
||||
match save_option:
|
||||
case 'user_config':
|
||||
config_output.save_user_config(dest_path)
|
||||
case 'user_creds':
|
||||
config_output.save_user_creds(dest_path, password=enc_password)
|
||||
case 'all':
|
||||
config_output.save(dest_path, creds=True, password=enc_password)
|
||||
|
|
@ -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,613 @@
|
|||
import glob, re, os, json, time, hashlib
|
||||
import pathlib, traceback
|
||||
from collections import OrderedDict
|
||||
from .exceptions import DiskError
|
||||
from .general import *
|
||||
from .output import log, LOG_LEVELS
|
||||
from .storage import storage
|
||||
|
||||
ROOT_DIR_PATTERN = re.compile('^.*?/devices')
|
||||
GPT = 0b00000001
|
||||
MBR = 0b00000010
|
||||
|
||||
#import ctypes
|
||||
#import ctypes.util
|
||||
#libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
|
||||
#libc.mount.argtypes = (ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_ulong, ctypes.c_char_p)
|
||||
|
||||
class BlockDevice():
|
||||
def __init__(self, path, info=None):
|
||||
if not info:
|
||||
# If we don't give any information, we need to auto-fill it.
|
||||
# Otherwise any subsequent usage will break.
|
||||
info = all_disks()[path].info
|
||||
|
||||
self.path = path
|
||||
self.info = info
|
||||
self.keep_partitions = True
|
||||
self.part_cache = OrderedDict()
|
||||
# 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.
|
||||
self.encryption_password = None
|
||||
|
||||
def __repr__(self, *args, **kwargs):
|
||||
return f"BlockDevice({self.device})"
|
||||
|
||||
def __iter__(self):
|
||||
for partition in self.partitions:
|
||||
yield self.partitions[partition]
|
||||
|
||||
def __getitem__(self, key, *args, **kwargs):
|
||||
if key not in self.info:
|
||||
raise KeyError(f'{self} does not contain information: "{key}"')
|
||||
return self.info[key]
|
||||
|
||||
def json(self):
|
||||
"""
|
||||
json() has precedence over __dump__, so this is a way
|
||||
to give less/partial information for user readability.
|
||||
"""
|
||||
return {
|
||||
'path' : self.path,
|
||||
'size' : self.info['size'] if 'size' in self.info else '<unknown>',
|
||||
'model' : self.info['model'] if 'model' in self.info else '<unknown>'
|
||||
}
|
||||
|
||||
def __dump__(self):
|
||||
return {
|
||||
'path': self.path,
|
||||
'info': self.info,
|
||||
'partition_cache': self.part_cache
|
||||
}
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
"""
|
||||
Returns the actual device-endpoint of the BlockDevice.
|
||||
If it's a loop-back-device it returns the back-file,
|
||||
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 "type" not in self.info:
|
||||
raise DiskError(f'Could not locate backplane info for "{self.path}"')
|
||||
|
||||
if self.info['type'] == 'loop':
|
||||
for drive in json.loads(b''.join(sys_command(f'losetup --json', hide_from_log=True)).decode('UTF_8'))['loopdevices']:
|
||||
if not drive['name'] == self.path: continue
|
||||
|
||||
return drive['back-file']
|
||||
elif self.info['type'] == 'disk':
|
||||
return self.path
|
||||
elif self.info['type'][:4] == 'raid':
|
||||
# This should catch /dev/md## raid devices
|
||||
return self.path
|
||||
elif self.info['type'] == '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['type']}", level=LOG_LEVELS.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):
|
||||
o = b''.join(sys_command(f'partprobe {self.path}'))
|
||||
|
||||
#o = b''.join(sys_command('/usr/bin/lsblk -o name -J -b {dev}'.format(dev=dev)))
|
||||
o = b''.join(sys_command(f'/usr/bin/lsblk -J {self.path}'))
|
||||
|
||||
if b'not a block device' in o:
|
||||
raise DiskError(f'Can not read partitions off something that isn\'t a block device: {self.path}')
|
||||
|
||||
if not o[:1] == b'{':
|
||||
raise DiskError(f'Error getting JSON output from:', f'/usr/bin/lsblk -J {self.path}')
|
||||
|
||||
r = json.loads(o.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, self, part_id=part_id, size=part['size'])
|
||||
|
||||
return {k: self.part_cache[k] for k in sorted(self.part_cache)}
|
||||
|
||||
@property
|
||||
def partition(self):
|
||||
all_partitions = self.partitions
|
||||
return [all_partitions[k] for k in all_partitions]
|
||||
|
||||
@property
|
||||
def partition_table_type(self):
|
||||
return GPT
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
log(f'BlockDevice().uuid is untested!', level=LOG_LEVELS.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.
|
||||
"""
|
||||
lsblk = b''.join(sys_command(f'lsblk -J -o+UUID {self.path}'))
|
||||
for partition in json.loads(lsblk.decode('UTF-8'))['blockdevices']:
|
||||
return partition.get('uuid', None)
|
||||
|
||||
def has_partitions(self):
|
||||
return len(self.partitions)
|
||||
|
||||
def has_mount_point(self, mountpoint):
|
||||
for partition in self.partitions:
|
||||
if self.partitions[partition].mountpoint == mountpoint:
|
||||
return True
|
||||
return False
|
||||
|
||||
def flush_cache(self):
|
||||
self.part_cache = OrderedDict()
|
||||
|
||||
class Partition():
|
||||
def __init__(self, path :str, block_device :BlockDevice, part_id=None, size=-1, filesystem=None, mountpoint=None, encrypted=False, autodetect_filesystem=True):
|
||||
if not part_id:
|
||||
part_id = os.path.basename(path)
|
||||
|
||||
self.block_device = block_device
|
||||
self.path = path
|
||||
self.part_id = part_id
|
||||
self.mountpoint = mountpoint
|
||||
self.target_mountpoint = mountpoint
|
||||
self.filesystem = filesystem
|
||||
self.size = size # TODO: Refresh?
|
||||
self._encrypted = None
|
||||
self.encrypted = encrypted
|
||||
self.allow_formatting = False # A fail-safe for unconfigured partitions, such as windows NTFS partitions.
|
||||
|
||||
if mountpoint:
|
||||
self.mount(mountpoint)
|
||||
|
||||
mount_information = get_mount_info(self.path)
|
||||
|
||||
if self.mountpoint != mount_information.get('target', None) and mountpoint:
|
||||
raise DiskError(f"{self} was given a mountpoint but the actual mountpoint differs: {mount_information.get('target', None)}")
|
||||
|
||||
if (target := mount_information.get('target', None)):
|
||||
self.mountpoint = target
|
||||
|
||||
if not self.filesystem and autodetect_filesystem:
|
||||
if (fstype := mount_information.get('fstype', get_filesystem_type(path))):
|
||||
self.filesystem = fstype
|
||||
|
||||
if self.filesystem == 'crypto_LUKS':
|
||||
self.encrypted = True
|
||||
|
||||
def __lt__(self, left_comparitor):
|
||||
if type(left_comparitor) == Partition:
|
||||
left_comparitor = left_comparitor.path
|
||||
else:
|
||||
left_comparitor = str(left_comparitor)
|
||||
return self.path < left_comparitor # Not quite sure the order here is correct. But /dev/nvme0n1p1 comes before /dev/nvme0n1p5 so seems correct.
|
||||
|
||||
def __repr__(self, *args, **kwargs):
|
||||
mount_repr = ''
|
||||
if self.mountpoint:
|
||||
mount_repr = f", mounted={self.mountpoint}"
|
||||
elif self.target_mountpoint:
|
||||
mount_repr = f", rel_mountpoint={self.target_mountpoint}"
|
||||
|
||||
if self._encrypted:
|
||||
return f'Partition(path={self.path}, size={self.size}, real_device={self.real_device}, fs={self.filesystem}{mount_repr})'
|
||||
else:
|
||||
return f'Partition(path={self.path}, size={self.size}, fs={self.filesystem}{mount_repr})'
|
||||
|
||||
@property
|
||||
def uuid(self) -> str:
|
||||
"""
|
||||
Returns the PARTUUID 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.
|
||||
"""
|
||||
lsblk = b''.join(sys_command(f'lsblk -J -o+PARTUUID {self.path}'))
|
||||
for partition in json.loads(lsblk.decode('UTF-8'))['blockdevices']:
|
||||
return partition.get('partuuid', None)
|
||||
|
||||
@property
|
||||
def encrypted(self):
|
||||
return self._encrypted
|
||||
|
||||
@encrypted.setter
|
||||
def encrypted(self, value :bool):
|
||||
if value:
|
||||
log(f'Marking {self} as encrypted: {value}', level=LOG_LEVELS.Debug)
|
||||
log(f"Callstrack when marking the partition: {''.join(traceback.format_stack())}", level=LOG_LEVELS.Debug)
|
||||
|
||||
self._encrypted = value
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.real_device
|
||||
|
||||
@property
|
||||
def real_device(self):
|
||||
for blockdevice in json.loads(b''.join(sys_command('lsblk -J')).decode('UTF-8'))['blockdevices']:
|
||||
if (parent := self.find_parent_of(blockdevice, os.path.basename(self.path))):
|
||||
return f"/dev/{parent}"
|
||||
# raise DiskError(f'Could not find appropriate parent for encrypted partition {self}')
|
||||
return self.path
|
||||
|
||||
def detect_inner_filesystem(self, password):
|
||||
log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=LOG_LEVELS.Info)
|
||||
from .luks import luks2
|
||||
|
||||
try:
|
||||
with luks2(self, 'luksloop', password, auto_unmount=True) as unlocked_device:
|
||||
return unlocked_device.filesystem
|
||||
except SysCallError:
|
||||
return None
|
||||
|
||||
def has_content(self):
|
||||
if not get_filesystem_type(self.path):
|
||||
return False
|
||||
|
||||
temporary_mountpoint = '/tmp/'+hashlib.md5(bytes(f"{time.time()}", 'UTF-8')+os.urandom(12)).hexdigest()
|
||||
temporary_path = pathlib.Path(temporary_mountpoint)
|
||||
|
||||
temporary_path.mkdir(parents=True, exist_ok=True)
|
||||
if (handle := sys_command(f'/usr/bin/mount {self.path} {temporary_mountpoint}')).exit_code != 0:
|
||||
raise DiskError(f'Could not mount and check for content on {self.path} because: {b"".join(handle)}')
|
||||
|
||||
files = len(glob.glob(f"{temporary_mountpoint}/*"))
|
||||
sys_command(f'/usr/bin/umount {temporary_mountpoint}')
|
||||
|
||||
temporary_path.rmdir()
|
||||
|
||||
return True if files > 0 else False
|
||||
|
||||
def safe_to_format(self):
|
||||
if self.allow_formatting is False:
|
||||
log(f"Partition {self} is not marked for formatting.", level=LOG_LEVELS.Debug)
|
||||
return False
|
||||
elif self.target_mountpoint == '/boot':
|
||||
try:
|
||||
if self.has_content():
|
||||
log(f"Partition {self} is a boot partition and has content inside.", level=LOG_LEVELS.Debug)
|
||||
return False
|
||||
except SysCallError as err:
|
||||
log(err.message, LOG_LEVELS.Debug)
|
||||
log(f"Partition {self} was identified as /boot but we could not mount to check for content, continuing!", level=LOG_LEVELS.Debug)
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def encrypt(self, *args, **kwargs):
|
||||
"""
|
||||
A wrapper function for luks2() instances and the .encrypt() method of that instance.
|
||||
"""
|
||||
from .luks import luks2
|
||||
|
||||
if not self._encrypted:
|
||||
raise DiskError(f"Attempting to encrypt a partition that was not marked for encryption: {self}")
|
||||
|
||||
if not self.safe_to_format():
|
||||
log(f"Partition {self} was marked as protected but encrypt() was called on it!", level=LOG_LEVELS.Error, fg="red")
|
||||
return False
|
||||
|
||||
handle = luks2(self, None, None)
|
||||
return handle.encrypt(self, *args, **kwargs)
|
||||
|
||||
def format(self, filesystem=None, path=None, allow_formatting=None, log_formatting=True):
|
||||
"""
|
||||
Format can be given an overriding path, for instance /dev/null to test
|
||||
the formatting functionality and in essence the support for the given filesystem.
|
||||
"""
|
||||
if filesystem is None:
|
||||
filesystem = self.filesystem
|
||||
|
||||
if path is None:
|
||||
path = self.path
|
||||
if allow_formatting is None:
|
||||
allow_formatting = self.allow_formatting
|
||||
|
||||
# To avoid "unable to open /dev/x: No such file or directory"
|
||||
start_wait = time.time()
|
||||
while pathlib.Path(path).exists() is False and time.time() - start_wait < 10:
|
||||
time.sleep(0.025)
|
||||
|
||||
if not allow_formatting:
|
||||
raise PermissionError(f"{self} is not formatable either because instance is locked ({self.allow_formatting}) or a blocking flag was given ({allow_formatting})")
|
||||
|
||||
if log_formatting:
|
||||
log(f'Formatting {path} -> {filesystem}', level=LOG_LEVELS.Info)
|
||||
|
||||
if filesystem == 'btrfs':
|
||||
o = b''.join(sys_command(f'/usr/bin/mkfs.btrfs -f {path}'))
|
||||
if b'UUID' not in o:
|
||||
raise DiskError(f'Could not format {path} with {filesystem} because: {o}')
|
||||
self.filesystem = 'btrfs'
|
||||
|
||||
elif filesystem == 'vfat':
|
||||
o = b''.join(sys_command(f'/usr/bin/mkfs.vfat -F32 {path}'))
|
||||
if (b'mkfs.fat' not in o and b'mkfs.vfat' not in o) or b'command not found' in o:
|
||||
raise DiskError(f'Could not format {path} with {filesystem} because: {o}')
|
||||
self.filesystem = 'vfat'
|
||||
|
||||
elif filesystem == 'ext4':
|
||||
if (handle := sys_command(f'/usr/bin/mkfs.ext4 -F {path}')).exit_code != 0:
|
||||
raise DiskError(f'Could not format {path} with {filesystem} because: {b"".join(handle)}')
|
||||
self.filesystem = 'ext4'
|
||||
|
||||
elif filesystem == 'xfs':
|
||||
if (handle := sys_command(f'/usr/bin/mkfs.xfs -f {path}')).exit_code != 0:
|
||||
raise DiskError(f'Could not format {path} with {filesystem} because: {b"".join(handle)}')
|
||||
self.filesystem = 'xfs'
|
||||
|
||||
elif filesystem == 'f2fs':
|
||||
if (handle := sys_command(f'/usr/bin/mkfs.f2fs -f {path}')).exit_code != 0:
|
||||
raise DiskError(f'Could not format {path} with {filesystem} because: {b"".join(handle)}')
|
||||
self.filesystem = 'f2fs'
|
||||
|
||||
elif filesystem == 'crypto_LUKS':
|
||||
# from .luks import luks2
|
||||
# encrypted_partition = luks2(self, None, None)
|
||||
# encrypted_partition.format(path)
|
||||
self.filesystem = 'crypto_LUKS'
|
||||
|
||||
else:
|
||||
raise UnknownFilesystemFormat(f"Fileformat '{filesystem}' is not yet implemented.")
|
||||
|
||||
if get_filesystem_type(path) == 'crypto_LUKS' or get_filesystem_type(self.real_device) == 'crypto_LUKS':
|
||||
self.encrypted = True
|
||||
else:
|
||||
self.encrypted = False
|
||||
|
||||
return True
|
||||
|
||||
def find_parent_of(self, data, name, parent=None):
|
||||
if data['name'] == name:
|
||||
return parent
|
||||
elif 'children' in data:
|
||||
for child in data['children']:
|
||||
if (parent := self.find_parent_of(child, name, parent=data['name'])):
|
||||
return parent
|
||||
|
||||
def mount(self, target, fs=None, options=''):
|
||||
if not self.mountpoint:
|
||||
log(f'Mounting {self} to {target}', level=LOG_LEVELS.Info)
|
||||
if not fs:
|
||||
if not self.filesystem: raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.')
|
||||
fs = self.filesystem
|
||||
|
||||
pathlib.Path(target).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
sys_command(f'/usr/bin/mount {self.path} {target}')
|
||||
except SysCallError as err:
|
||||
raise err
|
||||
|
||||
self.mountpoint = target
|
||||
return True
|
||||
|
||||
def unmount(self):
|
||||
try:
|
||||
exit_code = sys_command(f'/usr/bin/umount {self.path}').exit_code
|
||||
except SysCallError as err:
|
||||
exit_code = err.exit_code
|
||||
|
||||
# Without to much research, it seams that low error codes are errors.
|
||||
# And above 8k is indicators such as "/dev/x not mounted.".
|
||||
# So anything in between 0 and 8k are errors (?).
|
||||
if exit_code > 0 and exit_code < 8000:
|
||||
raise err
|
||||
|
||||
self.mountpoint = None
|
||||
return True
|
||||
|
||||
def umount(self):
|
||||
return self.unmount()
|
||||
|
||||
def filesystem_supported(self):
|
||||
"""
|
||||
The support for a filesystem (this partition) is tested by calling
|
||||
partition.format() with a path set to '/dev/null' which returns two exceptions:
|
||||
1. SysCallError saying that /dev/null is not formattable - but the filesystem is supported
|
||||
2. UnknownFilesystemFormat that indicates that we don't support the given filesystem type
|
||||
"""
|
||||
try:
|
||||
self.format(self.filesystem, '/dev/null', log_formatting=False, allow_formatting=True)
|
||||
except SysCallError:
|
||||
pass # We supported it, but /dev/null is not formatable as expected so the mkfs call exited with an error code
|
||||
except UnknownFilesystemFormat as err:
|
||||
raise err
|
||||
return True
|
||||
|
||||
class Filesystem():
|
||||
# TODO:
|
||||
# When instance of a HDD is selected, check all usages and gracefully unmount them
|
||||
# as well as close any crypto handles.
|
||||
def __init__(self, blockdevice, mode=GPT):
|
||||
self.blockdevice = blockdevice
|
||||
self.mode = mode
|
||||
|
||||
def __enter__(self, *args, **kwargs):
|
||||
if self.blockdevice.keep_partitions is False:
|
||||
log(f'Wiping {self.blockdevice} by using partition format {self.mode}', level=LOG_LEVELS.Debug)
|
||||
if self.mode == GPT:
|
||||
if self.raw_parted(f'{self.blockdevice.device} mklabel gpt').exit_code == 0:
|
||||
self.blockdevice.flush_cache()
|
||||
return self
|
||||
else:
|
||||
raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel gpt')
|
||||
else:
|
||||
raise DiskError(f'Unknown mode selected to format in: {self.mode}')
|
||||
|
||||
# TODO: partition_table_type is hardcoded to GPT at the moment. This has to be changed.
|
||||
elif self.mode == self.blockdevice.partition_table_type:
|
||||
log(f'Kept partition format {self.mode} for {self.blockdevice}', level=LOG_LEVELS.Debug)
|
||||
else:
|
||||
raise DiskError(f'The selected partition table format {self.mode} does not match that of {self.blockdevice}.')
|
||||
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return f"Filesystem(blockdevice={self.blockdevice}, mode={self.mode})"
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
# 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]
|
||||
b''.join(sys_command(f'sync'))
|
||||
return True
|
||||
|
||||
def find_partition(self, mountpoint):
|
||||
for partition in self.blockdevice:
|
||||
if partition.target_mountpoint == mountpoint or partition.mountpoint == mountpoint:
|
||||
return partition
|
||||
|
||||
def raw_parted(self, string:str):
|
||||
x = sys_command(f'/usr/bin/parted -s {string}')
|
||||
log(f"'parted -s {string}' returned: {b''.join(x)}", level=LOG_LEVELS.Debug)
|
||||
return x
|
||||
|
||||
def parted(self, string:str):
|
||||
"""
|
||||
Performs a parted execution of the given string
|
||||
|
||||
:param string: A raw string passed to /usr/bin/parted -s <string>
|
||||
:type string: str
|
||||
"""
|
||||
return self.raw_parted(string).exit_code
|
||||
|
||||
def use_entire_disk(self, root_filesystem_type='ext4'):
|
||||
log(f"Using and formatting the entire {self.blockdevice}.", level=LOG_LEVELS.Debug)
|
||||
self.add_partition('primary', start='1MiB', end='513MiB', format='fat32')
|
||||
self.set_name(0, 'EFI')
|
||||
self.set(0, 'boot on')
|
||||
# TODO: Probably redundant because in GPT mode 'esp on' is an alias for "boot on"?
|
||||
# https://www.gnu.org/software/parted/manual/html_node/set.html
|
||||
self.set(0, 'esp on')
|
||||
self.add_partition('primary', start='513MiB', end='100%')
|
||||
|
||||
self.blockdevice.partition[0].filesystem = 'vfat'
|
||||
self.blockdevice.partition[1].filesystem = root_filesystem_type
|
||||
log(f"Set the root partition {self.blockdevice.partition[1]} to use filesystem {root_filesystem_type}.", level=LOG_LEVELS.Debug)
|
||||
|
||||
self.blockdevice.partition[0].target_mountpoint = '/boot'
|
||||
self.blockdevice.partition[1].target_mountpoint = '/'
|
||||
|
||||
self.blockdevice.partition[0].allow_formatting = True
|
||||
self.blockdevice.partition[1].allow_formatting = True
|
||||
|
||||
def add_partition(self, type, start, end, format=None):
|
||||
log(f'Adding partition to {self.blockdevice}', level=LOG_LEVELS.Info)
|
||||
|
||||
previous_partitions = self.blockdevice.partitions
|
||||
if format:
|
||||
partitioning = self.parted(f'{self.blockdevice.device} mkpart {type} {format} {start} {end}') == 0
|
||||
else:
|
||||
partitioning = self.parted(f'{self.blockdevice.device} mkpart {type} {start} {end}') == 0
|
||||
|
||||
if partitioning:
|
||||
start_wait = time.time()
|
||||
while previous_partitions == self.blockdevice.partitions:
|
||||
time.sleep(0.025) # Let the new partition come up in the kernel
|
||||
if time.time() - start_wait > 10:
|
||||
raise DiskError(f"New partition never showed up after adding new partition on {self} (timeout 10 seconds).")
|
||||
|
||||
return True
|
||||
|
||||
def set_name(self, partition:int, name:str):
|
||||
return self.parted(f'{self.blockdevice.device} name {partition+1} "{name}"') == 0
|
||||
|
||||
def set(self, partition:int, string:str):
|
||||
return self.parted(f'{self.blockdevice.device} set {partition+1} {string}') == 0
|
||||
|
||||
def device_state(name, *args, **kwargs):
|
||||
# Based out of: https://askubuntu.com/questions/528690/how-to-get-list-of-all-non-removable-disk-device-names-ssd-hdd-and-sata-ide-onl/528709#528709
|
||||
if os.path.isfile('/sys/block/{}/device/block/{}/removable'.format(name, name)):
|
||||
with open('/sys/block/{}/device/block/{}/removable'.format(name, name)) as f:
|
||||
if f.read(1) == '1':
|
||||
return
|
||||
|
||||
path = ROOT_DIR_PATTERN.sub('', os.readlink('/sys/block/{}'.format(name)))
|
||||
hotplug_buses = ("usb", "ieee1394", "mmc", "pcmcia", "firewire")
|
||||
for bus in hotplug_buses:
|
||||
if os.path.exists('/sys/bus/{}'.format(bus)):
|
||||
for device_bus in os.listdir('/sys/bus/{}/devices'.format(bus)):
|
||||
device_link = ROOT_DIR_PATTERN.sub('', os.readlink('/sys/bus/{}/devices/{}'.format(bus, device_bus)))
|
||||
if re.search(device_link, path):
|
||||
return
|
||||
return True
|
||||
|
||||
# lsblk --json -l -n -o path
|
||||
def all_disks(*args, **kwargs):
|
||||
kwargs.setdefault("partitions", False)
|
||||
drives = OrderedDict()
|
||||
#for drive in json.loads(sys_command(f'losetup --json', *args, **lkwargs, hide_from_log=True)).decode('UTF_8')['loopdevices']:
|
||||
for drive in json.loads(b''.join(sys_command(f'lsblk --json -l -n -o path,size,type,mountpoint,label,pkname,model', *args, **kwargs, hide_from_log=True)).decode('UTF_8'))['blockdevices']:
|
||||
if not kwargs['partitions'] and drive['type'] == 'part': continue
|
||||
|
||||
drives[drive['path']] = BlockDevice(drive['path'], drive)
|
||||
return drives
|
||||
|
||||
def convert_to_gigabytes(string):
|
||||
unit = string.strip()[-1]
|
||||
size = float(string.strip()[:-1])
|
||||
|
||||
if unit == 'M':
|
||||
size = size/1024
|
||||
elif unit == 'T':
|
||||
size = size*1024
|
||||
|
||||
return size
|
||||
|
||||
def harddrive(size=None, model=None, fuzzy=False):
|
||||
collection = all_disks()
|
||||
for drive in collection:
|
||||
if size and convert_to_gigabytes(collection[drive]['size']) != size:
|
||||
continue
|
||||
if model and (collection[drive]['model'] is None or collection[drive]['model'].lower() != model.lower()):
|
||||
continue
|
||||
|
||||
return collection[drive]
|
||||
|
||||
def get_mount_info(path):
|
||||
try:
|
||||
output = b''.join(sys_command(f'/usr/bin/findmnt --json {path}'))
|
||||
except SysCallError:
|
||||
return {}
|
||||
|
||||
output = output.decode('UTF-8')
|
||||
output = json.loads(output)
|
||||
if 'filesystems' in output:
|
||||
if len(output['filesystems']) > 1:
|
||||
raise DiskError(f"Path '{path}' contains multiple mountpoints: {output['filesystems']}")
|
||||
|
||||
return output['filesystems'][0]
|
||||
|
||||
def get_partitions_in_use(mountpoint):
|
||||
try:
|
||||
output = b''.join(sys_command(f'/usr/bin/findmnt --json -R {mountpoint}'))
|
||||
except SysCallError:
|
||||
return {}
|
||||
|
||||
mounts = []
|
||||
|
||||
output = output.decode('UTF-8')
|
||||
output = json.loads(output)
|
||||
for target in output.get('filesystems', []):
|
||||
mounts.append(Partition(target['source'], None, filesystem=target.get('fstype', None), mountpoint=target['target']))
|
||||
|
||||
for child in target.get('children', []):
|
||||
mounts.append(Partition(child['source'], None, filesystem=child.get('fstype', None), mountpoint=child['target']))
|
||||
|
||||
return mounts
|
||||
|
||||
def get_filesystem_type(path):
|
||||
try:
|
||||
handle = sys_command(f"blkid -o value -s TYPE {path}")
|
||||
return b''.join(handle).strip().decode('UTF-8')
|
||||
except SysCallError:
|
||||
return None
|
||||
|
|
@ -1,626 +0,0 @@
|
|||
import logging
|
||||
import os
|
||||
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,
|
||||
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,
|
||||
PartitionGUID,
|
||||
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,
|
||||
) -> 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 = PartitionGUID.LINUX_ROOT_X86_64.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()]
|
||||
|
||||
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)
|
||||
|
||||
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,876 +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
|
||||
|
||||
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])
|
||||
|
|
@ -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 +0,0 @@
|
|||
import math
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from archinstall.lib.disk.device_handler import device_handler
|
||||
from archinstall.lib.disk.luks import Luks2
|
||||
from archinstall.lib.disk.lvm import (
|
||||
lvm_group_info,
|
||||
lvm_pv_create,
|
||||
lvm_vg_create,
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class FilesystemHandler:
|
||||
def __init__(self, disk_config: DiskLayoutConfiguration):
|
||||
self._disk_config = disk_config
|
||||
self._enc_config = disk_config.disk_encryption
|
||||
|
||||
def perform_filesystem_operations(self) -> None:
|
||||
if self._disk_config.config_type == DiskLayoutType.Pre_mount:
|
||||
debug('Disk layout configuration is set to pre-mount, not performing any operations')
|
||||
return
|
||||
|
||||
device_mods = [d for d in self._disk_config.device_modifications if d.partitions]
|
||||
|
||||
if not device_mods:
|
||||
debug('No modifications required')
|
||||
return
|
||||
|
||||
# Setup the blockdevice, filesystem (and optionally encryption).
|
||||
# Once that's done, we'll hand over to perform_installation()
|
||||
|
||||
# make sure all devices are unmounted
|
||||
for mod in device_mods:
|
||||
device_handler.umount_all_existing(mod.device_path)
|
||||
|
||||
for mod in device_mods:
|
||||
device_handler.partition(mod)
|
||||
|
||||
udev_sync()
|
||||
|
||||
if self._disk_config.lvm_config:
|
||||
for mod in device_mods:
|
||||
if boot_part := mod.get_boot_partition():
|
||||
debug(f'Formatting boot partition: {boot_part.dev_path}')
|
||||
self._format_partitions([boot_part])
|
||||
|
||||
self.perform_lvm_operations()
|
||||
else:
|
||||
for mod in device_mods:
|
||||
self._format_partitions(mod.partitions)
|
||||
|
||||
for part_mod in mod.partitions:
|
||||
if part_mod.fs_type == FilesystemType.BTRFS and part_mod.is_create_or_modify():
|
||||
device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config)
|
||||
|
||||
def _format_partitions(
|
||||
self,
|
||||
partitions: list[PartitionModification],
|
||||
) -> None:
|
||||
"""
|
||||
Format can be given an overriding path, for instance /dev/null to test
|
||||
the formatting functionality and in essence the support for the given filesystem.
|
||||
"""
|
||||
|
||||
# don't touch existing partitions
|
||||
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:
|
||||
self._setup_lvm(self._disk_config.lvm_config)
|
||||
self._format_lvm_vols(self._disk_config.lvm_config)
|
||||
|
||||
def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption) -> None:
|
||||
if enc_config.encryption_type == EncryptionType.LVM_ON_LUKS:
|
||||
enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False)
|
||||
|
||||
self._setup_lvm(lvm_config, enc_mods)
|
||||
self._format_lvm_vols(lvm_config)
|
||||
|
||||
# Don't close LVM or LUKS during setup - keep everything active
|
||||
# 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
|
||||
# LVM volumes must remain active for later re-unlock during installation
|
||||
for luks in enc_vols.values():
|
||||
luks.lock()
|
||||
|
||||
def _setup_lvm(
|
||||
self,
|
||||
lvm_config: LvmConfiguration,
|
||||
enc_mods: dict[PartitionModification, Luks2] = {},
|
||||
) -> None:
|
||||
self._lvm_create_pvs(lvm_config, enc_mods)
|
||||
|
||||
for vg in lvm_config.vol_groups:
|
||||
pv_dev_paths = self._get_all_pv_dev_paths(vg.pvs, enc_mods)
|
||||
|
||||
lvm_vg_create(pv_dev_paths, vg.name)
|
||||
|
||||
# figure out what the actual available size in the group is
|
||||
vg_info = lvm_group_info(vg.name)
|
||||
|
||||
if not vg_info:
|
||||
raise ValueError('Unable to fetch VG info')
|
||||
|
||||
# the actual available LVM Group size will be smaller than the
|
||||
# total PVs size due to reserved metadata storage etc.
|
||||
# so we'll have a look at the total avail. size, check the delta
|
||||
# to the desired sizes and subtract some equally from the actually
|
||||
# created volume
|
||||
avail_size = vg_info.vg_size
|
||||
desired_size = sum([vol.length for vol in vg.volumes], Size(0, Unit.B, SectorSize.default()))
|
||||
|
||||
delta = desired_size - avail_size
|
||||
delta_bytes = delta.convert(Unit.B)
|
||||
|
||||
# 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:
|
||||
path = vol.safe_dev_path
|
||||
|
||||
# wait a bit otherwise the mkfs will fail as it can't
|
||||
# find the mapper device yet
|
||||
device_handler.format(vol.fs_type, path)
|
||||
|
||||
if vol.fs_type == FilesystemType.BTRFS:
|
||||
device_handler.create_lvm_btrfs_subvolumes(path, vol.btrfs_subvols, vol.mount_options)
|
||||
|
||||
def _lvm_create_pvs(
|
||||
self,
|
||||
lvm_config: LvmConfiguration,
|
||||
enc_mods: dict[PartitionModification, Luks2] = {},
|
||||
) -> None:
|
||||
pv_paths: set[Path] = set()
|
||||
|
||||
for vg in lvm_config.vol_groups:
|
||||
pv_paths |= self._get_all_pv_dev_paths(vg.pvs, enc_mods)
|
||||
|
||||
lvm_pv_create(pv_paths)
|
||||
|
||||
def _get_all_pv_dev_paths(
|
||||
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