Compare commits

..

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

262 changed files with 11971 additions and 33113 deletions

4
.github/FUNDING.yml vendored
View File

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

View File

@ -41,8 +41,8 @@ body:
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`
you can easily post the installation log using the following command:
`curl -F'file=@/var/log/archinstall/install.log' https://0x0.st`
- type: textarea
id: freeform

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- run: pacman --noconfirm -Syu bandit
- name: Security checkup with Bandit
run: bandit -r archinstall || exit 0

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Prepare arch
run: |
pacman-key --init

View File

@ -21,8 +21,8 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # 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

View File

@ -26,14 +26,14 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- 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
- run: ./build_iso.sh
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: Arch Live ISO
path: /tmp/archlive/out/*.iso

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Prepare arch
run: |
pacman-key --init

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Prepare arch
run: |
pacman-key --init

View File

@ -7,7 +7,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Prepare arch
run: |
pacman-key --init

View File

@ -11,14 +11,14 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- 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
pacman --noconfirm -Sy python-pyparted python-pydantic
- name: Remove existing archinstall (if any)
run:
uv pip uninstall archinstall --break-system-packages --system
@ -33,7 +33,7 @@ jobs:
archinstall --script guided -v
archinstall --script only_hd -v
archinstall --script minimal -v
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: archinstall
path: dist/*

View File

@ -18,13 +18,13 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- 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
pacman --noconfirm -Sy python python-uv python-setuptools python-pip python-pyparted python-pydantic
- name: Build archinstall
run: |
uv build --no-build-isolation --wheel

View File

@ -4,6 +4,6 @@ jobs:
ruff_format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: astral-sh/ruff-action@57714a7c8a2e59f32539362ba31877a1957dded1 # v3.5.1
- run: ruff format --diff

View File

@ -4,5 +4,5 @@ jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: astral-sh/ruff-action@57714a7c8a2e59f32539362ba31877a1957dded1 # v3.5.1

View File

@ -1,22 +1,28 @@
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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
#on:
# push:
# paths:
# - 'archinstall/locales/**'
# pull_request:
# paths:
# - 'archinstall/locales/**'
#name: Verify local_generate script was run on translation changes
#jobs:
# translation-check:
# runs-on: ubuntu-latest
# container:
# image: archlinux/archlinux:latest
# steps:
# - uses: actions/checkout@v4
# - run: pacman --noconfirm -Syu python git diffutils
# - name: Verify all translation scripts are up to date
# run: |
# cd ..
# cp -r archinstall archinstall_orig
# cd archinstall/archinstall/locales
# bash locales_generator.sh 1> /dev/null
# cd ../../..
# git diff \
# --quiet --no-index --name-only \
# archinstall_orig/archinstall/locales \
# archinstall/archinstall/locales \
# || (echo "Translation files have not been updated after translation, please run ./locales_generator.sh once more and commit" && exit 1)

View File

@ -1,40 +0,0 @@
# This workflow will build an Arch Linux UKI file with the commit on it
name: Build Arch UKI with ArchInstall Commit
on:
push:
branches:
- master
- main # In case we adopt this convention in the future
pull_request:
paths-ignore:
- 'docs/**'
- '**.editorconfig'
- '**.gitignore'
- '**.md'
- 'LICENSE'
- 'PKGBUILD'
release:
types:
- created
jobs:
build:
runs-on: ubuntu-latest
container:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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

5
.gitignore vendored
View File

@ -39,9 +39,4 @@ requirements.txt
/.gitconfig
/actions-runner
/cmd_output.txt
node_modules/
uv.lock
test_tooling/mkosi/mkosi.output/*image*
test_tooling/mkosi/mkosi.cache/**
test_tooling/mkosi/mkosi.tools/**
test_tooling/mkosi/mkosi.tools.manifest

View File

@ -36,7 +36,7 @@ flake8:
- 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.
# When a stage name is preceeded with a '.' it's treated as "disabled" by GitLab and is not executed, so it's fine for it to be declared.
.pytest:
stage: test
tags:

View File

@ -1,7 +1,7 @@
default_stages: ['pre-commit']
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.14
rev: v0.14.3
hooks:
# fix unused imports and sort them
- id: ruff
@ -31,7 +31,7 @@ repos:
args: [--config=.flake8]
fail_fast: true
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v1.18.2
hooks:
- id: mypy
args: [
@ -41,7 +41,7 @@ repos:
additional_dependencies:
- pydantic
- pytest
- hypothesis
- pytest-mock
- cryptography
- textual
- repo: local

View File

@ -5,7 +5,7 @@
# Contributor: demostanis worlds <demostanis@protonmail.com>
pkgname=archinstall
pkgver=4.3
pkgver=3.0.13
pkgrel=1
pkgdesc="Just another guided/automated Arch Linux installer with a twist"
arch=(any)
@ -29,14 +29,12 @@ depends=(
'python-pydantic'
'python-pyparted'
'python-textual'
'python-markdown-it-py'
'python-linkify-it-py'
'systemd'
'util-linux'
'xfsprogs'
'lvm2'
'f2fs-tools'
'libfido2'
'ntfs-3g'
)
makedepends=(
'python-build'
@ -46,7 +44,6 @@ makedepends=(
'python-wheel'
'python-sphinx_rtd_theme'
'python-pylint'
'python-pylint-pydantic'
'ruff'
)
optdepends=(

View File

@ -6,7 +6,7 @@
[![Lint Python and Find Syntax Errors](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml/badge.svg)](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml)
Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist.
The installer also doubles as a python library to install Arch Linux and manage services, packages, and other things inside the installed system *(Usually from a live medium or from an existing installation)*.
The installer also doubles as a python library to install Arch Linux and manage services, packages, and other things inside the installed system *(Usually from a live medium)*.
* archinstall [discord](https://discord.gg/aDeMffrxNg) server
* archinstall [#archinstall:matrix.org](https://matrix.to/#/#archinstall:matrix.org) Matrix channel
@ -14,53 +14,28 @@ The installer also doubles as a python library to install Arch Linux and manage
* 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`.
## Upgrade `archinstall` on live Arch ISO image
Upgrading archinstall on the ISO needs to be done via a full system upgrade using
```shell
pacman -Syu
```
When booting from a live USB, the space on the ramdisk is limited and may not be sufficient to allow running a re-installation or upgrade of the installer.
In case one runs into this issue, any of the following can be used
* Resize the root partition https://wiki.archlinux.org/title/Archiso#Adjusting_the_size_of_the_root_file_system
* Specify the boot parameter copytoram=y (https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio-archiso/-/blob/master/docs/README.bootparams#L26) which will copy the root filesystem to tmpfs
Alternative ways to install are `git clone` the repository or `pip install --upgrade archinstall`.
## Running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer
Assuming you are on an Arch Linux live-ISO or installed via `pip`, `archinstall` will use the `guided` script by default
Assuming you are on an Arch Linux live-ISO or installed via `pip`:
```shell
archinstall
```
similar goes for running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer using `git
## 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>
# cd archinstall-git
# python -m archinstall
```
#### 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`.
Some additional options that most users do not need are hidden behind the `--advanced` flag.
## Running from a declarative configuration file or URL
@ -82,7 +57,7 @@ archinstall --config <path to user config file or URL> --creds <path to user cre
```
### 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.
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.
@ -95,15 +70,15 @@ there are multiple ways to provide the decryption key:
# Help or Issues
If you come across any issues, kindly submit your issue here on GitHub or post your query in the
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>
* To extract the log from the ISO image, one way is to use<br>
```shell
archinstall share-log
curl -F'file=@/var/log/archinstall/install.log' https://0x0.st
```
@ -158,6 +133,12 @@ The profiles' definitions and the packages they will install can be directly vie
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.
*Note: 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
- Resize the root partition https://wiki.archlinux.org/title/Archiso#Adjusting_the_size_of_the_root_file_system
- The boot parameter `copytoram=y` (https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio-archiso/-/blob/master/docs/README.bootparams#L26)
can be specified which will copy the root filesystem to tmpfs.*
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)*
@ -176,10 +157,10 @@ To test this without a live ISO, the simplest approach is to use a local image a
This can be done by installing `pacman -S arch-install-scripts util-linux` locally and doing the following:
# truncate -s 20G testimage.img
# losetup --partscan --show ./testimage.img
# losetup --partscan --show --find ./testimage.img
# 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
# 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.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.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>
@ -188,41 +169,9 @@ This will create a *20 GB* `testimage.img` and create a loop device which we can
There's also a [Building and Testing](https://github.com/archlinux/archinstall/wiki/Building-and-Testing) guide.<br>
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.

View File

@ -1,3 +1,166 @@
from archinstall.lib.plugins import plugin
"""Arch Linux installer - guided, templates etc."""
__all__ = ['plugin']
import importlib
import os
import sys
import time
import traceback
from archinstall.lib.args import arch_config_handler
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.network.wifi_handler import wifi_handler
from archinstall.lib.networking import ping
from archinstall.lib.packages.packages import check_package_upgrade
from archinstall.tui.ui.components import tui as ttui
from .lib.hardware import SysInfo
from .lib.output import FormattedOutput, debug, error, info, log, warn
from .lib.pacman import Pacman
from .lib.plugins import load_plugin, plugins
from .lib.translationhandler import Language, tr, translation_handler
from .tui.curses_menu import Tui
# @archinstall.plugin decorator hook to programmatically add
# plugins in runtime. Useful in profiles_bck and other things.
def plugin(f, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
plugins[f.__name__] = f
def _log_sys_info() -> None:
# Log various information about hardware before starting the installation. This might assist in troubleshooting
debug(f'Hardware model detected: {SysInfo.sys_vendor()} {SysInfo.product_name()}; UEFI mode: {SysInfo.has_uefi()}')
debug(f'Processor model detected: {SysInfo.cpu_model()}')
debug(f'Memory statistics: {SysInfo.mem_available()} available out of {SysInfo.mem_total()} total installed')
debug(f'Virtualization detected: {SysInfo.virtualization()}; is VM: {SysInfo.is_vm()}')
debug(f'Graphics devices detected: {SysInfo._graphics_devices().keys()}')
# For support reasons, we'll log the disk layout pre installation to match against post-installation layout
debug(f'Disk states before installing:\n{disk_layouts()}')
def _check_online() -> None:
try:
ping('1.1.1.1')
except OSError as ex:
if 'Network is unreachable' in str(ex):
if not arch_config_handler.args.skip_wifi_check:
success = not wifi_handler.setup()
if not success:
exit(0)
def _fetch_arch_db() -> None:
info('Fetching Arch Linux package database...')
try:
Pacman.run('-Sy')
except Exception as e:
error('Failed to sync Arch Linux package database.')
if 'could not resolve host' in str(e).lower():
error('Most likely due to a missing network connection or DNS issue.')
error('Run archinstall --debug and check /var/log/archinstall/install.log for details.')
debug(f'Failed to sync Arch Linux package database: {e}')
exit(1)
def check_version_upgrade() -> str | None:
info('Checking version...')
upgrade = None
upgrade = check_package_upgrade('archinstall')
if upgrade is None:
debug('No archinstall upgrades found')
return None
text = tr('New version available') + f': {upgrade}'
info(text)
return text
def main() -> int:
"""
This can either be run as the compiled and installed application: python setup.py install
OR straight as a module: python -m archinstall
In any case we will be attempting to load the provided script to be run from the scripts/ folder
"""
if '--help' in sys.argv or '-h' in sys.argv:
arch_config_handler.print_help()
return 0
if os.getuid() != 0:
print(tr('Archinstall requires root privileges to run. See --help for more.'))
return 1
_log_sys_info()
ttui.global_header = 'Archinstall'
if not arch_config_handler.args.offline:
_check_online()
_fetch_arch_db()
if not arch_config_handler.args.skip_version_check:
new_version = check_version_upgrade()
if new_version:
ttui.global_header = f'{ttui.global_header} {new_version}'
info(new_version)
time.sleep(3)
script = arch_config_handler.get_script()
mod_name = f'archinstall.scripts.{script}'
# by loading the module we'll automatically run the script
importlib.import_module(mod_name)
return 0
def run_as_a_module() -> None:
rc = 0
exc = None
try:
rc = main()
except Exception as e:
exc = e
finally:
# restore the terminal to the original state
Tui.shutdown()
if exc:
err = ''.join(traceback.format_exception(exc))
error(err)
text = (
'Archinstall experienced the above error. If you think this is a bug, please report it to\n'
'https://github.com/archlinux/archinstall and include the log file "/var/log/archinstall/install.log".\n\n'
"Hint: To extract the log from a live ISO \ncurl -F'file=@/var/log/archinstall/install.log' https://0x0.st\n"
)
warn(text)
rc = 1
exit(rc)
__all__ = [
'FormattedOutput',
'Language',
'Pacman',
'SysInfo',
'Tui',
'arch_config_handler',
'debug',
'disk_layouts',
'error',
'info',
'load_plugin',
'log',
'plugin',
'translation_handler',
'warn',
]

View File

@ -1,6 +1,4 @@
import sys
from archinstall.main import main
import archinstall
if __name__ == '__main__':
sys.exit(main())
archinstall.run_as_a_module()

View File

@ -1,9 +1,9 @@
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
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
@ -30,8 +30,8 @@ class AudioApp:
def _enable_pipewire(
self,
install_session: Installer,
users: list[User] | None = None,
install_session: 'Installer',
users: list['User'] | None = None,
) -> None:
if users is None:
return
@ -56,7 +56,7 @@ class AudioApp:
def install(
self,
install_session: Installer,
install_session: 'Installer',
audio_config: AudioConfiguration,
users: list[User] | None = None,
) -> None:

View File

@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
@ -20,7 +20,7 @@ class BluetoothApp:
'bluetooth.service',
]
def install(self, install_session: Installer) -> None:
def install(self, install_session: 'Installer') -> None:
debug('Installing Bluetooth')
install_session.add_additional_packages(self.packages)
install_session.enable_service(self.services)

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,217 @@
# from typing import List, Dict, Optional, TYPE_CHECKING, Any
#
# from ..lib import menu
# from archinstall.lib.output import log, FormattedOutput
# from archinstall.lib.profile.profiles_handler import profile_handler
# from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult, ProfileInfo, TProfile
#
# if TYPE_CHECKING:
# from archinstall.lib.installer import Installer
# _: Any
#
#
# class CustomProfileList(menu.ListManager):
# def __init__(self, prompt: str, profiles: List[TProfile]):
# self._actions = [
# str(_('Add profile')),
# str(_('Edit profile')),
# str(_('Delete profile'))
# ]
# super().__init__(prompt, profiles, [self._actions[0]], self._actions[1:])
#
# def reformat(self, data: List[TProfile]) -> Dict[str, Optional[TProfile]]:
# table = FormattedOutput.as_table(data)
# rows = table.split('\n')
#
# # these are the header rows of the table and do not map to any profile obviously
# # we're adding 2 spaces as prefix because the menu selector '> ' will be put before
# # the selectable rows so the header has to be aligned
# display_data: Dict[str, Optional[TProfile]] = {f' {rows[0]}': None, f' {rows[1]}': None}
#
# for row, profile in zip(rows[2:], data):
# row = row.replace('|', '\\|')
# display_data[row] = profile
#
# return display_data
#
# def selected_action_display(self, profile: TProfile) -> str:
# return profile.name
#
# def handle_action(
# self,
# action: str,
# entry: Optional['CustomTypeProfile'],
# data: List['CustomTypeProfile']
# ) -> List['CustomTypeProfile']:
# if action == self._actions[0]: # add
# new_profile = self._add_profile()
# if new_profile is not None:
# # in case a profile with the same name as an existing profile
# # was created we'll replace the existing one
# data = [d for d in data if d.name != new_profile.name]
# data += [new_profile]
# elif entry is not None:
# if action == self._actions[1]: # edit
# new_profile = self._add_profile(entry)
# if new_profile is not None:
# # we'll remove the original profile and add the modified version
# data = [d for d in data if d.name != entry.name and d.name != new_profile.name]
# data += [new_profile]
# elif action == self._actions[2]: # delete
# data = [d for d in data if d != entry]
#
# return data
#
# def _is_new_profile_name(self, name: str) -> bool:
# existing_profile = profile_handler.get_profile_by_name(name)
# if existing_profile is not None and existing_profile.profile_type != ProfileType.CustomType:
# return False
# return True
#
# def _add_profile(self, editing: Optional['CustomTypeProfile'] = None) -> Optional['CustomTypeProfile']:
# name_prompt = '\n\n' + str(_('Profile name: '))
#
# while True:
# profile_name = menu.TextInput(name_prompt, editing.name if editing else '').run().strip()
#
# if not profile_name:
# return None
#
# if not self._is_new_profile_name(profile_name):
# error_prompt = str(_("The profile name you entered is already in use. Try again"))
# print(error_prompt)
# else:
# break
#
# packages_prompt = str(_('Packages to be install with this profile (space separated, leave blank to skip): '))
# edit_packages = ' '.join(editing.packages) if editing else ''
# packages = menu.TextInput(packages_prompt, edit_packages).run().strip()
#
# services_prompt = str(_('Services to be enabled with this profile (space separated, leave blank to skip): '))
# edit_services = ' '.join(editing.services) if editing else ''
# services = menu.TextInput(services_prompt, edit_services).run().strip()
#
# choice = menu.Menu(
# str(_('Should this profile be enabled for installation?')),
# menu.Menu.yes_no(),
# skip=False,
# default_option=menu.Menu.no(),
# clear_screen=False,
# show_search_hint=False
# ).run()
#
# enable_profile = True if choice.value == menu.Menu.yes() else False
#
# profile = CustomTypeProfile(
# profile_name,
# enabled=enable_profile,
# packages=packages.split(' '),
# services=services.split(' ')
# )
#
# return profile
#
#
# # TODO
# # Still needs some ironing out
# class CustomProfile():
# def __init__(self):
# super().__init__(
# 'Custom',
# ProfileType.Custom,
# )
#
# def json(self) -> Dict[str, Any]:
# data: Dict[str, Any] = {'main': self.name, 'gfx_driver': self.gfx_driver, 'custom': []}
#
# for profile in self._current_selection:
# data['custom'].append({
# 'name': profile.name,
# 'packages': profile.packages,
# 'services': profile.services,
# 'enabled': profile.custom_enabled
# })
#
# return data
#
# def do_on_select(self) -> SelectResult:
# custom_profile_list = CustomProfileList('', profile_handler.get_custom_profiles())
# custom_profiles = custom_profile_list.run()
#
# # we'll first remove existing custom default_profiles with
# # the same name and then add the new ones this
# # will avoid errors of default_profiles with duplicate naming
# profile_handler.remove_custom_profiles(custom_profiles)
# profile_handler.add_custom_profiles(custom_profiles)
#
# self.set_current_selection(custom_profiles)
#
# if custom_profile_list.is_last_choice_cancel():
# return SelectResult.SameSelection
#
# enabled_profiles = [p for p in self._current_selection if p.custom_enabled]
# # in case we only created inactive default_profiles we wanna store them but
# # we want to reset the original setting
# if not enabled_profiles:
# return SelectResult.ResetCurrent
#
# return SelectResult.NewSelection
#
# def post_install(self, install_session: 'Installer'):
# for profile in self._current_selection:
# profile.post_install(install_session)
#
# def install(self, install_session: 'Installer'):
# driver_packages = self.gfx_driver_packages()
# install_session.add_additional_packages(driver_packages)
#
# for profile in self._current_selection:
# if profile.custom_enabled:
# log(f'Installing custom profile {profile.name}...')
#
# install_session.add_additional_packages(profile.packages)
# install_session.enable_service(profile.services)
#
# profile.install(install_session)
#
# def info(self) -> Optional[ProfileInfo]:
# enabled_profiles = [p for p in self._current_selection if p.custom_enabled]
# if enabled_profiles:
# details = ', '.join([p.name for p in enabled_profiles])
# gfx_driver = self.gfx_driver
# return ProfileInfo(self.name, details, gfx_driver)
#
# return None
#
# def reset(self):
# for profile in self._current_selection:
# profile.set_enabled(False)
#
# self.gfx_driver = None
#
#
# class CustomTypeProfile(Profile):
# def __init__(
# self,
# name: str,
# enabled: bool = False,
# packages: List[str] = [],
# services: List[str] = []
# ):
# super().__init__(
# name,
# ProfileType.CustomType,
# packages=packages,
# services=services,
# support_gfx_driver=True
# )
#
# self.custom_enabled = enabled
#
# def json(self) -> Dict[str, Any]:
# return {
# 'name': self.name,
# 'packages': self.packages,
# 'services': self.services,
# 'enabled': self.custom_enabled
# }

View File

@ -1,19 +1,19 @@
from typing import TYPE_CHECKING, Self, override
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType, SelectResult
from archinstall.lib.log import info
from archinstall.lib.menu.helpers import Selection
from archinstall.default_profiles.profile import GreeterType, Profile, ProfileType, SelectResult
from archinstall.lib.output import info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import FrameProperties, PreviewStyle
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:
def __init__(self, current_selection: list[Profile] = []) -> None:
super().__init__(
'Desktop',
ProfileType.Desktop,
@ -30,6 +30,9 @@ class DesktopProfile(Profile):
'openssh',
'htop',
'wget',
'iwd',
'wireless_tools',
'wpa_supplicant',
'smartmontools',
'xdg-utils',
]
@ -48,17 +51,17 @@ class DesktopProfile(Profile):
return None
async def _do_on_select_profiles(self) -> None:
def _do_on_select_profiles(self) -> None:
for profile in self.current_selection:
await profile.do_on_select()
profile.do_on_select()
@override
async def do_on_select(self) -> SelectResult:
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,
preview_action=lambda x: x.value.preview_text(),
)
for p in profile_handler.get_desktop_profiles()
]
@ -66,18 +69,20 @@ class DesktopProfile(Profile):
group = MenuItemGroup(items, sort_items=True, sort_case_sensitive=False)
group.set_selected_by_value(self.current_selection)
result = await Selection[Self](
result = SelectMenu[Profile](
group,
multi=True,
allow_reset=True,
allow_skip=True,
preview_location='right',
).show()
preview_style=PreviewStyle.RIGHT,
preview_size='auto',
preview_frame=FrameProperties.max('Info'),
).run()
match result.type_:
case ResultType.Selection:
self.current_selection = result.get_values()
await self._do_on_select_profiles()
self._do_on_select_profiles()
return SelectResult.NewSelection
case ResultType.Skip:
return SelectResult.SameSelection
@ -85,30 +90,19 @@ class DesktopProfile(Profile):
return SelectResult.ResetCurrent
@override
def post_install(self, install_session: Installer) -> None:
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)
@override
def install(self, install_session: Installer) -> None:
def install(self, install_session: 'Installer') -> None:
# Install common packages for all desktop environments
install_session.add_additional_packages(self.packages)
xorg_installed = False
for profile in self.current_selection:
info(f'Installing profile {profile.name}...')
install_session.add_additional_packages(profile.packages)
install_session.enable_service(profile.services)
if not xorg_installed and profile.display_server == DisplayServerType.Xorg:
install_session.add_additional_packages(['xorg-server', 'xorg-xinit'])
xorg_installed = True
profile.install(install_session)

View File

@ -0,0 +1,6 @@
from enum import Enum
class SeatAccess(Enum):
seatd = 'seatd'
polkit = 'polkit'

View File

@ -1,26 +1,23 @@
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import DisplayServerType, Profile, ProfileType
from archinstall.default_profiles.profile import ProfileType
from archinstall.default_profiles.xorg import XorgProfile
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class AwesomeProfile(Profile):
class AwesomeProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Awesome',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('Awesome', ProfileType.WindowMgr)
@property
@override
def packages(self) -> list[str]:
return [
return super().packages + [
'awesome',
'alacritty',
'xorg-xinit',
'xorg-xrandr',
'xterm',
'feh',
@ -32,7 +29,7 @@ class AwesomeProfile(Profile):
]
@override
def install(self, install_session: Installer) -> None:
def install(self, install_session: 'Installer') -> None:
super().install(install_session)
# TODO: Copy a full configuration to ~/.config/awesome/rc.lua instead.

View File

@ -1,22 +1,17 @@
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
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class BspwmProfile(Profile):
class BspwmProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Bspwm',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('Bspwm', ProfileType.WindowMgr)
@property
@override
def packages(self) -> list[str]:
# return super().packages + [
return [
'bspwm',
'sxhkd',
@ -29,11 +24,3 @@ class BspwmProfile(Profile):
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm
@override
def provision(self, install_session: Installer, users: list[User]) -> None:
for user in users:
install_session.arch_chroot('mkdir -p ~/.config/bspwm ~/.config/sxhkd', run_as=user.username)
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/bspwmrc ~/.config/bspwm/', run_as=user.username)
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/sxhkdrc ~/.config/sxhkd/', run_as=user.username)
install_session.arch_chroot('chmod +x ~/.config/bspwm/bspwmrc', run_as=user.username)

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class BudgieProfile(Profile):
class BudgieProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Budgie',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
super().__init__('Budgie', ProfileType.DesktopEnv)
@property
@override
@ -20,7 +16,6 @@ class BudgieProfile(Profile):
'budgie',
'mate-terminal',
'nemo',
'nemo-fileroller',
'papirus-icon-theme',
]

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class CinnamonProfile(Profile):
class CinnamonProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Cinnamon',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('Cinnamon', ProfileType.DesktopEnv)
@property
@override

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class CosmicProfile(Profile):
class CosmicProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Cosmic',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
super().__init__('cosmic-epoch', ProfileType.DesktopEnv, advanced=True)
@property
@override

View File

@ -0,0 +1,22 @@
from typing import override
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class CutefishProfile(XorgProfile):
def __init__(self) -> None:
super().__init__('Cutefish', ProfileType.DesktopEnv)
@property
@override
def packages(self) -> list[str]:
return [
'cutefish',
'noto-fonts',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Sddm

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class DeepinProfile(Profile):
class DeepinProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Deepin',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('Deepin', ProfileType.DesktopEnv)
@property
@override

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class EnlightenmentProfile(Profile):
class EnlighenmentProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Enlightenment',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('Enlightenment', ProfileType.WindowMgr)
@property
@override

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class GnomeProfile(Profile):
class GnomeProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'GNOME',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
super().__init__('GNOME', ProfileType.DesktopEnv)
@property
@override

View File

@ -1,19 +1,20 @@
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
from archinstall.default_profiles.desktops import SeatAccess
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties
class HyprlandProfile(Profile):
class HyprlandProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Hyprland',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
super().__init__('Hyprland', ProfileType.DesktopEnv)
self.custom_settings = {CustomSetting.SeatAccess: None}
self.custom_settings = {'seat_access': None}
@property
@override
@ -41,12 +42,33 @@ class HyprlandProfile(Profile):
@property
@override
def services(self) -> list[str]:
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
if pref := self.custom_settings.get('seat_access', None):
return [pref]
return []
def _ask_seat_access(self) -> None:
# need to activate seat service and add to seat group
header = tr('Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
header += '\n' + tr('Choose an option to give Hyprland access to your hardware') + '\n'
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
default = self.custom_settings.get('seat_access', None)
group.set_default_by_value(default)
result = SelectMenu[SeatAccess](
group,
header=header,
allow_skip=False,
frame=FrameProperties.min(tr('Seat access')),
alignment=Alignment.CENTER,
).run()
if result.type_ == ResultType.Selection:
self.custom_settings['seat_access'] = result.get_value().value
@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
def do_on_select(self) -> None:
self._ask_seat_access()
return None

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class I3wmProfile(Profile):
class I3wmProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'i3-wm',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('i3-wm', ProfileType.WindowMgr)
@property
@override

View File

@ -1,25 +1,29 @@
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
from archinstall.default_profiles.desktops import SeatAccess
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties
class LabwcProfile(Profile):
class LabwcProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Labwc',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
self.custom_settings = {CustomSetting.SeatAccess: None}
self.custom_settings = {'seat_access': None}
@property
@override
def packages(self) -> list[str]:
additional = []
if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
if seat := self.custom_settings.get('seat_access', None):
additional = [seat]
return [
@ -35,12 +39,33 @@ class LabwcProfile(Profile):
@property
@override
def services(self) -> list[str]:
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
if pref := self.custom_settings.get('seat_access', None):
return [pref]
return []
def _ask_seat_access(self) -> None:
# need to activate seat service and add to seat group
header = tr('labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
header += '\n' + tr('Choose an option to give labwc access to your hardware') + '\n'
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
default = self.custom_settings.get('seat_access', None)
group.set_default_by_value(default)
result = SelectMenu[SeatAccess](
group,
header=header,
allow_skip=False,
frame=FrameProperties.min(tr('Seat access')),
alignment=Alignment.CENTER,
).run()
if result.type_ == ResultType.Selection:
self.custom_settings['seat_access'] = result.get_value().value
@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
def do_on_select(self) -> None:
self._ask_seat_access()
return None

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class LxqtProfile(Profile):
class LxqtProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Lxqt',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('Lxqt', ProfileType.DesktopEnv)
# 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.

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class MateProfile(Profile):
class MateProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Mate',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('Mate', ProfileType.DesktopEnv)
@property
@override

View File

@ -1,25 +1,29 @@
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
from archinstall.default_profiles.desktops import SeatAccess
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties
class NiriProfile(Profile):
class NiriProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'niri',
'Niri',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
self.custom_settings = {CustomSetting.SeatAccess: None}
self.custom_settings = {'seat_access': None}
@property
@override
def packages(self) -> list[str]:
additional = []
if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
if seat := self.custom_settings.get('seat_access', None):
additional = [seat]
return [
@ -43,12 +47,33 @@ class NiriProfile(Profile):
@property
@override
def services(self) -> list[str]:
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
if pref := self.custom_settings.get('seat_access', None):
return [pref]
return []
def _ask_seat_access(self) -> None:
# need to activate seat service and add to seat group
header = tr('niri needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
header += '\n' + tr('Choose an option to give niri access to your hardware') + '\n'
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
default = self.custom_settings.get('seat_access', None)
group.set_default_by_value(default)
result = SelectMenu[SeatAccess](
group,
header=header,
allow_skip=False,
frame=FrameProperties.min(tr('Seat access')),
alignment=Alignment.CENTER,
).run()
if result.type_ == ResultType.Selection:
self.custom_settings['seat_access'] = result.get_value().value
@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
def do_on_select(self) -> None:
self._ask_seat_access()
return None

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,120 +1,26 @@
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
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
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):
class PlasmaProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'KDE Plasma',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
super().__init__('KDE Plasma', ProfileType.DesktopEnv)
@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
return [
'plasma-meta',
'konsole',
'kate',
'dolphin',
'ark',
'plasma-workspace',
]
@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()
return GreeterType.Sddm

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class QtileProfile(Profile):
class QtileProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Qtile',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('Qtile', ProfileType.WindowMgr)
@property
@override

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class RiverProfile(Profile):
class RiverProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'River',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
super().__init__('River', ProfileType.WindowMgr)
@property
@override

View File

@ -1,25 +1,29 @@
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
from archinstall.default_profiles.desktops import SeatAccess
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties
class SwayProfile(Profile):
class SwayProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Sway',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
self.custom_settings = {CustomSetting.SeatAccess: None}
self.custom_settings = {'seat_access': None}
@property
@override
def packages(self) -> list[str]:
additional = []
if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
if seat := self.custom_settings.get('seat_access', None):
additional = [seat]
return [
@ -45,12 +49,33 @@ class SwayProfile(Profile):
@property
@override
def services(self) -> list[str]:
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
if pref := self.custom_settings.get('seat_access', None):
return [pref]
return []
def _ask_seat_access(self) -> None:
# need to activate seat service and add to seat group
header = tr('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
header += '\n' + tr('Choose an option to give Sway access to your hardware') + '\n'
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
default = self.custom_settings.get('seat_access', None)
group.set_default_by_value(default)
result = SelectMenu[SeatAccess](
group,
header=header,
allow_skip=False,
frame=FrameProperties.min(tr('Seat access')),
alignment=Alignment.CENTER,
).run()
if result.type_ == ResultType.Selection:
self.custom_settings['seat_access'] = result.get_value().value
@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
def do_on_select(self) -> None:
self._ask_seat_access()
return None

View File

@ -1,33 +0,0 @@
from enum import Enum
from archinstall.lib.menu.helpers import Selection
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'
async def select_seat_access(profile_name: str, default: str | None) -> SeatAccess:
header = tr('{} needs access to your seat').format(profile_name)
header += f' ({tr("collection of hardware devices i.e. keyboard, mouse")})' + '\n'
header += tr('Choose an option how to give {} access to your hardware').format(profile_name)
items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(default)
result = await Selection[SeatAccess](
group,
header=header,
allow_skip=False,
).show()
if result.type_ == ResultType.Selection:
return result.get_value()
else:
raise ValueError('Unexpected result type from seat access selection')

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class Xfce4Profile(Profile):
class Xfce4Profile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Xfce4',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('Xfce4', ProfileType.DesktopEnv)
@property
@override

View File

@ -1,16 +1,12 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.profile import GreeterType, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class XmonadProfile(Profile):
class XmonadProfile(XorgProfile):
def __init__(self) -> None:
super().__init__(
'Xmonad',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
super().__init__('Xmonad', ProfileType.WindowMgr)
@property
@override

View File

@ -1,16 +1,13 @@
from enum import Enum, StrEnum, auto
from typing import TYPE_CHECKING, Self
from __future__ import annotations
import sys
from enum import Enum, auto
from typing import TYPE_CHECKING
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'
from ..lib.installer import Installer
class ProfileType(Enum):
@ -26,6 +23,7 @@ class ProfileType(Enum):
DesktopEnv = 'Desktop Environment'
CustomType = 'CustomType'
# special things
Tailored = 'Tailored'
Application = 'Application'
@ -35,9 +33,10 @@ class GreeterType(Enum):
Sddm = 'sddm'
Gdm = 'gdm'
Ly = 'ly'
CosmicSession = 'cosmic-greeter'
PlasmaLoginManager = 'plasma-login-manager'
GreetdDms = 'dms-greeter'
# .. todo:: Remove when we un-hide cosmic behind --advanced
if '--advanced' in sys.argv:
CosmicSession = 'cosmic-greeter'
class SelectResult(Enum):
@ -46,30 +45,25 @@ class SelectResult(Enum):
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] = [],
current_selection: list[Profile] = [],
packages: list[str] = [],
services: list[str] = [],
support_gfx_driver: bool = False,
support_greeter: bool = False,
display_server: DisplayServerType | None = None,
advanced: bool = False,
) -> None:
self.name = name
self.profile_type = profile_type
self.custom_settings: dict[CustomSetting, str | None] = {}
self.custom_settings: dict[str, str | None] = {}
self.advanced = advanced
self._support_gfx_driver = support_gfx_driver
self._support_greeter = support_greeter
self._display_server = display_server
# self.gfx_driver: str | None = None
@ -103,38 +97,40 @@ class Profile:
"""
return None
def install(self, install_session: Installer) -> None:
def _advanced_check(self) -> bool:
"""
Used to control if the Profile() should be visible or not in different contexts.
Returns True if --advanced is given on a Profile(advanced=True) instance.
"""
from archinstall.lib.args import arch_config_handler
return self.advanced is False or arch_config_handler.args.advanced is True
def install(self, install_session: 'Installer') -> None:
"""
Performs installation steps when this profile was selected
"""
def post_install(self, install_session: Installer) -> None:
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:
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:
def set_custom_settings(self, settings: dict[str, str | None]) -> None:
"""
Set the custom settings for the profile.
This is also called when the settings are parsed from the config
@ -155,16 +151,19 @@ class Profile:
return self.profile_type in top_levels
def is_desktop_profile(self) -> bool:
return self.profile_type == ProfileType.Desktop
return self.profile_type == ProfileType.Desktop if self._advanced_check() else False
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
return (self.profile_type == ProfileType.DesktopEnv or self.profile_type == ProfileType.WindowMgr) if self._advanced_check() else False
def is_xorg_type_profile(self) -> bool:
return self.profile_type == ProfileType.Xorg
return self.profile_type == ProfileType.Xorg if self._advanced_check() else False
def is_tailored(self) -> bool:
return self.profile_type == ProfileType.Tailored
def is_custom_type_profile(self) -> bool:
return self.profile_type == ProfileType.CustomType
@ -180,23 +179,10 @@ class Profile:
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:
@ -213,6 +199,6 @@ class Profile:
text = tr('Installed packages') + ':\n'
for pkg in sorted(packages):
text += f' - {pkg}\n'
text += f'\t- {pkg}\n'
return text

View File

@ -1,19 +1,19 @@
from typing import TYPE_CHECKING, Self, override
from typing import TYPE_CHECKING, 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.output import info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import FrameProperties, PreviewStyle
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] = []):
def __init__(self, current_value: list[Profile] = []):
super().__init__(
'Server',
ProfileType.Server,
@ -21,12 +21,12 @@ class ServerProfile(Profile):
)
@override
async def do_on_select(self) -> SelectResult:
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,
preview_action=lambda x: x.value.preview_text(),
)
for p in profile_handler.get_server_profiles()
]
@ -34,13 +34,15 @@ class ServerProfile(Profile):
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(self.current_selection)
result = await Selection[Self](
result = SelectMenu[Profile](
group,
allow_reset=True,
allow_skip=True,
preview_style=PreviewStyle.RIGHT,
preview_size='auto',
preview_frame=FrameProperties.max('Info'),
multi=True,
preview_location='right',
).show()
).run()
match result.type_:
case ResultType.Selection:
@ -53,17 +55,12 @@ class ServerProfile(Profile):
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:
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:
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}')

View File

@ -4,7 +4,6 @@ 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):
@ -25,6 +24,9 @@ class DockerProfile(Profile):
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}')
def post_install(self, install_session: 'Installer') -> None:
from archinstall.lib.args import arch_config_handler
if auth_config := arch_config_handler.config.auth_config:
for user in auth_config.users:
install_session.arch_chroot(f'usermod -a -G docker {user.username}')

View File

@ -24,5 +24,5 @@ class MariadbProfile(Profile):
return ['mariadb']
@override
def post_install(self, install_session: Installer) -> None:
def post_install(self, install_session: 'Installer') -> None:
install_session.arch_chroot('mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql')

View File

@ -24,5 +24,5 @@ class PostgresqlProfile(Profile):
return ['postgresql']
@override
def post_install(self, install_session: Installer) -> None:
def post_install(self, install_session: 'Installer') -> None:
install_session.arch_chroot('initdb -D /var/lib/postgres/data', run_as='postgres')

View File

@ -0,0 +1,22 @@
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import ProfileType
from archinstall.default_profiles.xorg import XorgProfile
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class TailoredProfile(XorgProfile):
def __init__(self) -> None:
super().__init__('52-54-00-12-34-56', ProfileType.Tailored)
@property
@override
def packages(self) -> list[str]:
return ['nano', 'wget', 'git']
@override
def install(self, install_session: 'Installer') -> None:
super().install(install_session)
# do whatever you like here :)

View File

@ -1,9 +1,7 @@
from typing import TYPE_CHECKING, override
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, Profile, ProfileType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
from archinstall.default_profiles.profile import Profile, ProfileType
from archinstall.lib.translationhandler import tr
class XorgProfile(Profile):
@ -11,22 +9,26 @@ class XorgProfile(Profile):
self,
name: str = 'Xorg',
profile_type: ProfileType = ProfileType.Xorg,
advanced: bool = False,
):
super().__init__(
name,
profile_type,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
advanced=advanced,
)
@override
def preview_text(self) -> str:
text = tr('Environment type: {}').format(self.profile_type.value)
if packages := self.packages_text():
text += f'\n{packages}'
return text
@property
@override
def packages(self) -> list[str]:
return [
'xorg-server',
'xorg-xinit',
]
@override
def install(self, install_session: Installer) -> None:
install_session.add_additional_packages(self.packages)

View File

@ -2,10 +2,6 @@ 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
@ -18,7 +14,7 @@ class ApplicationHandler:
def __init__(self) -> None:
pass
def install_applications(self, install_session: Installer, app_config: ApplicationConfiguration, users: list[User] | None = None) -> None:
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)
@ -29,23 +25,5 @@ class ApplicationHandler:
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,
)
application_handler = ApplicationHandler()

View File

@ -1,24 +1,12 @@
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.models.application import ApplicationConfiguration, Audio, AudioConfiguration, BluetoothConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, Orientation
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
@ -31,8 +19,8 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
else:
self._app_config = ApplicationConfiguration()
menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_options, checkmarks=True)
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
super().__init__(
self._item_group,
@ -41,8 +29,8 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
)
@override
async def show(self) -> ApplicationConfiguration | None:
_ = await super().show()
def run(self, additional_title: str | None = None) -> ApplicationConfiguration:
super().run(additional_title=additional_title)
return self._app_config
def _define_menu_options(self) -> list[MenuItem]:
@ -60,45 +48,13 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
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 = 'Bluetooth: '
output += tr('Enabled') if bluetooth_config.enabled else tr('Disabled')
return output
return None
@ -109,101 +65,48 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
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 select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None:
group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.no()
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
if preset is not None:
group.set_selected_by_value(preset.enabled)
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(
result = SelectMenu[bool](
group,
header=header,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
allow_skip=True,
preset=preset_val,
).show()
).run()
match result.type_:
case ResultType.Selection:
return BluetoothConfiguration(result.get_value())
enabled = result.item() == MenuItem.yes()
return BluetoothConfiguration(enabled)
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:
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](
result = SelectMenu[Audio](
group,
header=tr('Select audio configuration'),
allow_skip=True,
).show()
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Audio')),
).run()
match result.type_:
case ResultType.Skip:
@ -212,52 +115,3 @@ async def select_audio(preset: AudioConfiguration | None = None) -> AudioConfigu
return AudioConfiguration(audio=result.get_value())
case ResultType.Reset:
raise ValueError('Unhandled result type')
async def select_firewall(preset: FirewallConfiguration | None = None) -> FirewallConfiguration | None:
group = MenuItemGroup.from_enum(Firewall)
if preset:
group.set_focus_by_value(preset.firewall)
result = await Selection[Firewall](
group,
allow_skip=True,
allow_reset=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return FirewallConfiguration(firewall=result.get_value())
case ResultType.Reset:
return None
async def select_fonts(preset: FontsConfiguration | None = None) -> FontsConfiguration | None:
items = [MenuItem(f'{f.value} ({f.description()})', value=f) for f in FontPackage]
group = MenuItemGroup(items)
if preset:
for f in preset.fonts:
group.set_selected_by_value(f)
result = await Selection[FontPackage](
group,
header=tr('Select font packages to install'),
allow_skip=True,
allow_reset=True,
multi=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
selected = result.get_values()
if selected:
return FontsConfiguration(fonts=selected)
return None
case ResultType.Reset:
return None

View File

@ -1,42 +1,33 @@
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 importlib.metadata import version
from pathlib import Path
from typing import Any, Self
from typing import Any
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.application import ApplicationConfiguration
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.bootloader import Bootloader
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.output import debug, error, logger, warn
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'
from archinstall.lib.utils.util import get_password
from archinstall.tui.curses_menu import Tui
@p_dataclass
@ -62,83 +53,6 @@ class Arguments:
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:
@ -150,101 +64,77 @@ class ArchConfig:
profile_config: ProfileConfiguration | None = None
mirror_config: MirrorConfiguration | None = None
network_config: NetworkConfiguration | None = None
bootloader_config: BootloaderConfiguration | None = None
bootloader: Bootloader | None = None
uki: bool = False
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])
kernels: list[str] = field(default_factory=lambda: ['linux'])
ntp: bool = True
packages: list[str] = field(default_factory=list)
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
parallel_downloads: int = 0
swap: bool = True
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] = {}
def unsafe_json(self) -> dict[str, Any]:
config: dict[str, 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]
config['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
config['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
config['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(),
def safe_json(self) -> dict[str, Any]:
config: Any = {
'version': self.version,
'script': self.script,
'archinstall-language': self.archinstall_language.json(),
'hostname': self.hostname,
'kernels': self.kernels,
'uki': self.uki,
'ntp': self.ntp,
'packages': self.packages,
'parallel_downloads': self.parallel_downloads,
'swap': self.swap,
'timezone': self.timezone,
'services': self.services,
'custom_commands': self.custom_commands,
'bootloader': self.bootloader.json() if self.bootloader else None,
'app_config': self.app_config.json() if self.app_config else None,
'auth_config': self.auth_config.json() if self.auth_config else None,
}
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
config['locale_config'] = self.locale_config.json()
if self.disk_config:
config['disk_config'] = self.disk_config.json()
if self.profile_config:
cfg[ArchConfigType.PROFILE_CONFIG] = self.profile_config
config['profile_config'] = self.profile_config.json()
if self.mirror_config:
config['mirror_config'] = self.mirror_config.json()
if self.network_config:
cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config
config['network_config'] = self.network_config.json()
if self.app_config:
cfg[ArchConfigType.APP_CONFIG] = self.app_config
return cfg
return config
@classmethod
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
arch_config = cls()
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> 'ArchConfig':
arch_config = ArchConfig()
arch_config.locale_config = LocaleConfiguration.parse_arg(args_config)
@ -253,7 +143,6 @@ class ArchConfig:
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', '')
@ -290,15 +179,13 @@ class ArchConfig:
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)
if bootloader_config := args_config.get('bootloader', None):
arch_config.bootloader = Bootloader.from_arg(bootloader_config, args.skip_boot)
arch_config.uki = args_config.get('uki', False)
if args_config.get('uki') and (arch_config.bootloader is None or not arch_config.bootloader.has_uki_support()):
arch_config.uki = False
# deprecated: backwards compatibility
audio_config_args = args_config.get('audio_config', None)
@ -321,14 +208,10 @@ class ArchConfig:
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))
if parallel_downloads := args_config.get('parallel_downloads', 0):
arch_config.parallel_downloads = parallel_downloads
swap_arg = args_config.get('swap')
if swap_arg is not None:
arch_config.swap = ZramConfiguration.parse_arg(swap_arg)
arch_config.swap = args_config.get('swap', True)
if timezone := args_config.get('timezone', 'UTC'):
arch_config.timezone = timezone
@ -349,7 +232,7 @@ class ArchConfig:
arch_config.auth_config = AuthenticationConfiguration()
arch_config.auth_config.root_enc_password = root_password
# DEPRECATED: backwards compatibility
# DEPRECATED: backwards copatibility
users: list[User] = []
if args_users := args_config.get('!users', None):
users = User.parse_arguments(args_users)
@ -371,17 +254,17 @@ class ArchConfig:
class ArchConfigHandler:
def __init__(self) -> None:
self._parser: ArgumentParser = self._define_arguments()
self._add_sub_parsers()
args: Arguments = self._parse_args()
self._args = args
self._args: Arguments = self._parse_args()
config = self._parse_config()
try:
self._config = ArchConfig.from_config(config, self._args)
self._config.version = get_version()
self._config = ArchConfig.from_config(config, args)
self._config.version = self._get_version()
except ValueError as err:
warn(str(err))
sys.exit(1)
exit(1)
@property
def config(self) -> ArchConfig:
@ -403,19 +286,20 @@ class ArchConfigHandler:
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 _get_version(self) -> str:
try:
return version('archinstall')
except Exception:
return 'Archinstall version not found'
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(),
version='%(prog)s ' + self._get_version(),
)
parser.add_argument(
'--config',
@ -545,6 +429,7 @@ class ArchConfigHandler:
default=False,
help='Enabled verbose options',
)
return parser
def _parse_args(self) -> Arguments:
@ -605,37 +490,37 @@ class ArchConfigHandler:
except ValueError as err:
if 'Invalid password' in str(err):
error(tr('Incorrect credentials file decryption password'))
sys.exit(1)
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
incorrect_password = False
while True:
decryption_pwd: Password | None = tui.run(
lambda p=prompt: get_password( # type: ignore[misc]
header=p,
with Tui():
while True:
header = tr('Incorrect password') if incorrect_password else None
decryption_pwd = get_password(
text=tr('Credentials file decryption password'),
header=header,
allow_skip=False,
no_confirmation=True,
skip_confirmation=True,
)
)
if not decryption_pwd:
return None
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
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')
incorrect_password = True
else:
debug(f'Error decrypting credentials file: {err}')
raise err from err
return json.loads(creds_data)
@ -650,12 +535,12 @@ class ArchConfigHandler:
else:
error('Not a valid url')
sys.exit(1)
exit(1)
def _read_file(self, path: Path) -> str:
if not path.exists():
error(f'Could not find file {path}')
sys.exit(1)
exit(1)
return path.read_text()
@ -669,3 +554,6 @@ class ArchConfigHandler:
clean_args[key] = val
return clean_args
arch_config_handler: ArchConfigHandler = ArchConfigHandler()

View File

@ -2,11 +2,12 @@ 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.general import SysCommandWorker
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import User
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import Tui
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
@ -15,20 +16,20 @@ if TYPE_CHECKING:
class AuthenticationHandler:
def setup_auth(
self,
install_session: Installer,
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:
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,
install_session: 'Installer',
u2f_config: U2FLoginConfiguration,
) -> None:
match u2f_config.u2f_login_method:
@ -53,7 +54,7 @@ class AuthenticationHandler:
def _add_u2f_entry(self, file: Path, entry: str) -> None:
if not file.exists():
debug(f'File does not exist: {file}')
return
return None
content = file.read_text().splitlines()
@ -72,7 +73,7 @@ class AuthenticationHandler:
def _configure_u2f_mapping(
self,
install_session: Installer,
install_session: 'Installer',
u2f_config: U2FLoginConfiguration,
users: list[User],
hostname: str,
@ -81,7 +82,7 @@ class AuthenticationHandler:
install_session.pacman.strap('pam-u2f')
print(tr('Setting up U2F login: {}').format(u2f_config.u2f_login_method.value))
Tui.print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}'))
# https://developers.yubico.com/pam-u2f/
u2f_auth_file = install_session.target / 'etc/u2f_mappings'
@ -91,9 +92,9 @@ class AuthenticationHandler:
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'))
Tui.print('')
Tui.print(tr('Setting up U2F device for user: {}').format(user.username))
Tui.print(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}']
@ -124,3 +125,6 @@ class AuthenticationHandler:
existing_keys = all_keys
u2f_auth_file.write_text(existing_keys)
auth_handler = AuthenticationHandler()

View File

@ -1,16 +1,17 @@
from typing import override
from archinstall.lib.disk.fido import Fido2
from archinstall.lib.interactions.manage_users_conf import ask_for_additional_users
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.output import FormattedOutput
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.lib.utils.util import get_password
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, Orientation
class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
@ -20,8 +21,8 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
else:
self._auth_config = AuthenticationConfiguration()
menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_options, checkmarks=True)
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
super().__init__(
self._item_group,
@ -30,14 +31,15 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
)
@override
async def show(self) -> AuthenticationConfiguration | None:
return await super().show()
def run(self, additional_title: str | None = None) -> AuthenticationConfiguration:
super().run(additional_title=additional_title)
return self._auth_config
def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=tr('Root password'),
action=lambda x: select_root_password(),
action=select_root_password,
preview_action=self._prev_root_pwd,
key='root_enc_password',
),
@ -56,16 +58,16 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
),
]
async def _create_user_account(self, preset: list[User] | None = None) -> list[User]:
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)
users = ask_for_additional_users(defined_users=preset)
return users
def _prev_users(self, item: MenuItem) -> str | None:
users: list[User] | None = item.value
if users:
return as_table(users)
return FormattedOutput.as_table(users)
return None
def _prev_root_pwd(self, item: MenuItem) -> str | None:
@ -99,12 +101,12 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
return None
async def select_root_password() -> Password | None:
password = await get_password(header=tr('Enter root password'), allow_skip=True)
def select_root_password(preset: str | None = None) -> Password | None:
password = get_password(text=tr('Root password'), allow_skip=True)
return password
async def select_u2f_login(preset: U2FLoginConfiguration | None) -> U2FLoginConfiguration | None:
def select_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | None:
devices = Fido2.get_fido2_devices()
if not devices:
return None
@ -118,22 +120,30 @@ async def select_u2f_login(preset: U2FLoginConfiguration | None) -> U2FLoginConf
if preset is not None:
group.set_selected_by_value(preset.u2f_login_method)
result = await Selection[U2FLoginMethod](
result = SelectMenu[U2FLoginMethod](
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('U2F Login Method')),
allow_skip=True,
allow_reset=True,
).show()
).run()
match result.type_:
case ResultType.Selection:
u2f_method = result.get_value()
group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.no()
header = tr('Enable passwordless sudo?')
result_sudo = await Confirmation(
result_sudo = SelectMenu[bool](
group,
header=header,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
allow_skip=True,
preset=False,
).show()
).run()
passwordless_sudo = result_sudo.item() == MenuItem.yes()
@ -145,3 +155,5 @@ async def select_u2f_login(preset: U2FLoginConfiguration | None) -> U2FLoginConf
return preset
case ResultType.Reset:
return None
case _:
raise ValueError('Unhandled result type')

View File

@ -1,33 +1,28 @@
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
from .exceptions import SysCallError
from .general import SysCommand, SysCommandWorker, locate_binary
from .installer import Installer
from .output import error
from .storage import storage
class Boot:
_active_boot: ClassVar[Self | None] = None
def __init__(self, path: Path | str):
if isinstance(path, Path):
path = str(path)
self.path = path
def __init__(self, installation: Installer):
self.instance = installation
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:
def __enter__(self) -> 'Boot':
if (existing_session := storage.get('active_boot', None)) and existing_session.instance != self.instance:
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
if existing_session:
self.session = existing_session.session
self.ready = existing_session.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.
@ -35,7 +30,7 @@ class Boot:
[
'systemd-nspawn',
'-D',
self.path,
str(self.instance.target),
'--timezone=off',
'-b',
'--no-pager',
@ -50,7 +45,7 @@ class Boot:
self.ready = True
break
Boot._active_boot = self
storage['active_boot'] = self
return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
@ -60,7 +55,7 @@ class Boot:
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}',
f'The error above occurred in a temporary boot-up of the installation {self.instance}',
)
shutdown = None
@ -79,12 +74,12 @@ class Boot:
shutdown_exit_code = shutdown.exit_code
if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0):
Boot._active_boot = None
storage['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}',
f'Could not shut down temporary boot of {self.instance}: {session_exit_code}/{shutdown_exit_code}',
exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])),
)
@ -105,7 +100,17 @@ class Boot:
return self.session.is_alive()
def SysCommand(self, cmd: list[str], *args, **kwargs) -> SysCommand: # type: ignore[no-untyped-def]
if cmd[0][0] != '/' and cmd[0][:2] != './':
# This check is also done in SysCommand & SysCommandWorker.
# However, that check is done for `machinectl` and not for our chroot command.
# So this wrapper for SysCommand will do this additionally.
cmd[0] = locate_binary(cmd[0])
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]
if cmd[0][0] != '/' and cmd[0][:2] != './':
cmd[0] = locate_binary(cmd[0])
return SysCommandWorker(['systemd-run', f'--machine={self.container_name}', '--pty', *cmd], *args, **kwargs)

View File

@ -1,217 +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
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,
),
]
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')
@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_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')

View File

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

View File

@ -2,20 +2,18 @@ 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.models.network import NetworkConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_key_value_pair
from archinstall.tui.curses_menu import SelectMenu, Tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
from .args import ArchConfig
from .crypt import encrypt
from .general import JSON, UNSAFE_JSON
from .output import debug, logger, warn
from .utils.util import get_password, prompt_dir
class ConfigurationOutput:
@ -43,86 +41,43 @@ class ConfigurationOutput:
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)
out = self._config.safe_json()
return json.dumps(out, indent=4, sort_keys=True, cls=JSON)
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)
out = self._config.unsafe_json()
return json.dumps(out, indent=4, sort_keys=True, cls=UNSAFE_JSON)
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, show_install_warnings: bool = False) -> bool:
def confirm_config(self) -> bool:
header = f'{tr("The specified configuration will be applied")}. '
header += tr('Would you like to continue?') + '\n'
if show_install_warnings:
header += self._render_install_warnings()
with Tui():
group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.yes()
group.set_preview_for_all(lambda x: self.user_config_to_json())
group = MenuItemGroup.yes_no()
group.set_preview_for_all(lambda x: self.user_config_to_json())
result = SelectMenu[bool](
group,
header=header,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
allow_skip=False,
preview_size='auto',
preview_style=PreviewStyle.BOTTOM,
preview_frame=FrameProperties.max(tr('Configuration')),
).run()
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
if result.item() != MenuItem.yes():
return False
return True
def get_install_warnings(self) -> list[str]:
warnings: list[str] = []
if not isinstance(self._config.network_config, NetworkConfiguration):
warnings.append(tr('Warning: no network configuration selected. Network will need to be set up manually on the installed system.'))
return warnings
def _render_install_warnings(self) -> str:
warnings = self.get_install_warnings()
if not warnings:
return ''
return '\n' + '\n'.join(f'[yellow]{w}[/]' for w in warnings) + '\n'
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:
@ -167,7 +122,7 @@ class ConfigurationOutput:
self.save_user_creds(save_path, password=password)
async def save_config(config: ArchConfig) -> None:
def save_config(config: ArchConfig) -> None:
def preview(item: MenuItem) -> str | None:
match item.value:
case 'user_config':
@ -205,11 +160,13 @@ async def save_config(config: ArchConfig) -> None:
]
group = MenuItemGroup(items)
result = await Selection[str](
result = SelectMenu[str](
group,
allow_skip=True,
preview_location='right',
).show()
preview_frame=FrameProperties.max(tr('Configuration')),
preview_size='auto',
preview_style=PreviewStyle.RIGHT,
).run()
match result.type_:
case ResultType.Skip:
@ -222,8 +179,9 @@ async def save_config(config: ArchConfig) -> None:
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',
dest_path = prompt_dir(
tr('Directory'),
tr('Enter a directory for the configuration(s) to be saved (tab completion enabled)') + '\n',
allow_skip=True,
)
@ -232,39 +190,50 @@ async def save_config(config: ArchConfig) -> None:
header = tr('Do you want to save the configuration file(s) to {}?').format(dest_path)
save_result = await Confirmation(
group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.yes()
result = SelectMenu(
group,
header=header,
allow_skip=False,
preset=True,
).show()
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
).run()
match save_result.type_:
match result.type_:
case ResultType.Selection:
if not save_result.get_value():
if result.item() == MenuItem.no():
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(
group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.no()
result = SelectMenu(
group,
header=header,
allow_skip=False,
preset=False,
).show()
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
).run()
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,
)
match result.type_:
case ResultType.Selection:
if result.item() == MenuItem.yes():
password = get_password(
text=tr('Credentials file encryption password'),
allow_skip=True,
)
if password:
enc_password = password.plaintext
if password:
enc_password = password.plaintext
match save_option:
case 'user_config':

View File

@ -6,7 +6,7 @@ from pathlib import Path
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
from archinstall.lib.log import debug
from .output import debug
libcrypt = ctypes.CDLL('libcrypt.so')
@ -48,10 +48,10 @@ def crypt_gen_salt(prefix: str | bytes, rounds: int) -> bytes:
def crypt_yescrypt(plaintext: str) -> str:
"""
By default chpasswd in Arch uses PAM to hash the password with crypt_yescrypt
By default chpasswd in Arch uses PAM to 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
If no value was specified (or commented out) a default of 5 is choosen
"""
value = _search_login_defs('YESCRYPT_COST_FACTOR')
if value is not None:

View File

@ -1,22 +1,19 @@
from __future__ import annotations
import json
import logging
import os
import time
from collections.abc import Iterable
from pathlib import Path
from typing import Literal, overload
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 (
from ..exceptions import DiskError, SysCallError, UnknownFilesystemFormat
from ..general import SysCommand, SysCommandWorker
from ..luks import Luks2
from ..models.device import (
DEFAULT_ITER_TIME,
BDevice,
BtrfsMountOption,
@ -24,19 +21,33 @@ from archinstall.lib.models.device import (
DiskEncryption,
FilesystemType,
LsblkInfo,
LvmGroupInfo,
LvmPVInfo,
LvmVolume,
LvmVolumeGroup,
LvmVolumeInfo,
ModificationStatus,
PartitionFlag,
PartitionGUID,
PartitionModification,
PartitionTable,
SectorSize,
Size,
SubvolumeModification,
Unit,
_BtrfsSubvolumeInfo,
_DeviceInfo,
_PartitionInfo,
)
from archinstall.lib.models.users import Password
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
from ..models.users import Password
from ..output import debug, error, info, log
from ..utils.util import is_subpath
from .utils import (
find_lsblk_info,
get_all_lsblk_info,
get_lsblk_info,
umount,
)
class DeviceHandler:
@ -58,11 +69,13 @@ class DeviceHandler:
def load_devices(self) -> None:
block_devices = {}
udev_sync()
self.udev_sync()
all_lsblk_info = get_all_lsblk_info()
devices = getAllDevices()
devices.extend(self.get_loop_devices())
archiso_mountpoint = Path('/run/archiso/airootfs')
for device in devices:
dev_lsblk_info = find_lsblk_info(device.path, all_lsblk_info)
@ -74,7 +87,7 @@ class DeviceHandler:
continue
# exclude archiso loop device
if dev_lsblk_info.mountpoint == ARCHISO_MOUNTPOINT:
if dev_lsblk_info.mountpoint == archiso_mountpoint:
continue
try:
@ -99,7 +112,7 @@ class DeviceHandler:
fs_type = self._determine_fs_type(partition, lsblk_info)
subvol_infos = []
if fs_type == FilesystemType.BTRFS:
if fs_type == FilesystemType.Btrfs:
subvol_infos = self.get_btrfs_info(partition.path, lsblk_info)
partition_infos.append(
@ -147,8 +160,8 @@ class DeviceHandler:
) -> FilesystemType | None:
try:
if partition.fileSystem:
if partition.fileSystem.type == FilesystemType.LINUX_SWAP.parted_value:
return FilesystemType.LINUX_SWAP
if partition.fileSystem.type == FilesystemType.LinuxSwap.parted_value:
return FilesystemType.LinuxSwap
return FilesystemType(partition.fileSystem.type)
elif lsblk_info is not None:
return FilesystemType(lsblk_info.fstype) if lsblk_info.fstype else None
@ -175,6 +188,23 @@ class DeviceHandler:
return part
return None
def get_parent_device_path(self, dev_path: Path) -> Path:
lsblk = get_lsblk_info(dev_path)
return Path(f'/dev/{lsblk.pkname}')
def get_unique_path_for_device(self, dev_path: Path) -> Path | None:
paths = Path('/dev/disk/by-id').glob('*')
linked_targets = {p.resolve(): p for p in paths}
linked_wwn_targets = {p: linked_targets[p] for p in linked_targets if p.name.startswith('wwn-') or p.name.startswith('nvme-eui.')}
if dev_path in linked_wwn_targets:
return linked_wwn_targets[dev_path]
if dev_path in linked_targets:
return linked_targets[dev_path]
return None
def get_uuid_for_path(self, path: Path) -> str | None:
partition = self.find_partition(path)
return partition.partuuid if partition else None
@ -190,7 +220,7 @@ class DeviceHandler:
subvol_infos: list[_BtrfsSubvolumeInfo] = []
if not lsblk_info.mountpoint:
mount(dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
self.mount(dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
mountpoint = self._TMP_BTRFS_MOUNT
else:
# when multiple subvolumes are mounted then the lsblk output may look like
@ -241,20 +271,23 @@ class DeviceHandler:
options = []
match fs_type:
case FilesystemType.BTRFS | FilesystemType.XFS:
case FilesystemType.Btrfs | FilesystemType.Xfs:
# Force overwrite
options.append('-f')
case FilesystemType.F2FS:
case FilesystemType.F2fs:
options.append('-f')
options.extend(('-O', 'extra_attr'))
case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4:
case FilesystemType.Ext2 | FilesystemType.Ext3 | FilesystemType.Ext4:
# Force create
options.append('-F')
case _ if fs_type.is_fat():
case FilesystemType.Fat12 | FilesystemType.Fat16 | FilesystemType.Fat32:
mkfs_type = 'fat'
# Set FAT size
options.extend(('-F', fs_type.value.removeprefix(mkfs_type)))
case FilesystemType.LINUX_SWAP:
case FilesystemType.Ntfs:
# Skip zeroing and bad sector check
options.append('--fast')
case FilesystemType.LinuxSwap:
command = 'mkswap'
case _:
raise UnknownFilesystemFormat(f'Filetype "{fs_type.value}" is not supported')
@ -289,7 +322,7 @@ class DeviceHandler:
key_file = luks_handler.encrypt(iter_time=iter_time)
udev_sync()
self.udev_sync()
luks_handler.unlock(key_file=key_file)
@ -320,7 +353,7 @@ class DeviceHandler:
key_file = luks_handler.encrypt(iter_time=enc_conf.iter_time)
udev_sync()
self.udev_sync()
luks_handler.unlock(key_file=key_file)
@ -333,6 +366,145 @@ class DeviceHandler:
info(f'luks2 locking device: {dev_path}')
luks_handler.lock()
def _lvm_info(
self,
cmd: str,
info_type: Literal['lv', 'vg', 'pvseg'],
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
raw_info = SysCommand(cmd).decode().split('\n')
# for whatever reason the output sometimes contains
# "File descriptor X leaked leaked on vgs invocation
data = '\n'.join([raw for raw in raw_info if 'File descriptor' not in raw])
debug(f'LVM info: {data}')
reports = json.loads(data)
for report in reports['report']:
if len(report[info_type]) != 1:
raise ValueError('Report does not contain any entry')
entry = report[info_type][0]
match info_type:
case 'pvseg':
return LvmPVInfo(
pv_name=Path(entry['pv_name']),
lv_name=entry['lv_name'],
vg_name=entry['vg_name'],
)
case 'lv':
return LvmVolumeInfo(
lv_name=entry['lv_name'],
vg_name=entry['vg_name'],
lv_size=Size(int(entry['lv_size'][:-1]), Unit.B, SectorSize.default()),
)
case 'vg':
return LvmGroupInfo(
vg_uuid=entry['vg_uuid'],
vg_size=Size(int(entry['vg_size'][:-1]), Unit.B, SectorSize.default()),
)
return None
@overload
def _lvm_info_with_retry(self, cmd: str, info_type: Literal['lv']) -> LvmVolumeInfo | None: ...
@overload
def _lvm_info_with_retry(self, cmd: str, info_type: Literal['vg']) -> LvmGroupInfo | None: ...
@overload
def _lvm_info_with_retry(self, cmd: str, info_type: Literal['pvseg']) -> LvmPVInfo | None: ...
def _lvm_info_with_retry(
self,
cmd: str,
info_type: Literal['lv', 'vg', 'pvseg'],
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
while True:
try:
return self._lvm_info(cmd, info_type)
except ValueError:
time.sleep(3)
def lvm_vol_info(self, lv_name: str) -> LvmVolumeInfo | None:
cmd = f'lvs --reportformat json --unit B -S lv_name={lv_name}'
return self._lvm_info_with_retry(cmd, 'lv')
def lvm_group_info(self, vg_name: str) -> LvmGroupInfo | None:
cmd = f'vgs --reportformat json --unit B -o vg_name,vg_uuid,vg_size -S vg_name={vg_name}'
return self._lvm_info_with_retry(cmd, 'vg')
def lvm_pvseg_info(self, vg_name: str, lv_name: str) -> LvmPVInfo | None:
cmd = f'pvs --segments -o+lv_name,vg_name -S vg_name={vg_name},lv_name={lv_name} --reportformat json '
return self._lvm_info_with_retry(cmd, 'pvseg')
def lvm_vol_change(self, vol: LvmVolume, activate: bool) -> None:
active_flag = 'y' if activate else 'n'
cmd = f'lvchange -a {active_flag} {vol.safe_dev_path}'
debug(f'lvchange volume: {cmd}')
SysCommand(cmd)
def lvm_export_vg(self, vg: LvmVolumeGroup) -> None:
cmd = f'vgexport {vg.name}'
debug(f'vgexport: {cmd}')
SysCommand(cmd)
def lvm_import_vg(self, vg: LvmVolumeGroup) -> None:
cmd = f'vgimport {vg.name}'
debug(f'vgimport: {cmd}')
SysCommand(cmd)
def lvm_vol_reduce(self, vol_path: Path, amount: Size) -> None:
val = amount.format_size(Unit.B, include_unit=False)
cmd = f'lvreduce -L -{val}B {vol_path}'
debug(f'Reducing LVM volume size: {cmd}')
SysCommand(cmd)
def lvm_pv_create(self, pvs: Iterable[Path]) -> None:
cmd = 'pvcreate ' + ' '.join([str(pv) for pv in pvs])
debug(f'Creating LVM PVS: {cmd}')
worker = SysCommandWorker(cmd)
worker.poll()
worker.write(b'y\n', line_ending=False)
def lvm_vg_create(self, pvs: Iterable[Path], vg_name: str) -> None:
pvs_str = ' '.join([str(pv) for pv in pvs])
cmd = f'vgcreate --yes {vg_name} {pvs_str}'
debug(f'Creating LVM group: {cmd}')
worker = SysCommandWorker(cmd)
worker.poll()
worker.write(b'y\n', line_ending=False)
def lvm_vol_create(self, vg_name: str, volume: LvmVolume, offset: Size | None = None) -> None:
if offset is not None:
length = volume.length - offset
else:
length = volume.length
length_str = length.format_size(Unit.B, include_unit=False)
cmd = f'lvcreate --yes -L {length_str}B {vg_name} -n {volume.name}'
debug(f'Creating volume: {cmd}')
worker = SysCommandWorker(cmd)
worker.poll()
worker.write(b'y\n', line_ending=False)
volume.vg_name = vg_name
volume.dev_path = Path(f'/dev/{vg_name}/{volume.name}')
def _setup_partition(
self,
part_mod: PartitionModification,
@ -342,7 +514,7 @@ class DeviceHandler:
) -> 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]:
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)
@ -351,7 +523,7 @@ class DeviceHandler:
disk.deletePartition(part_info.partition)
if part_mod.status == ModificationStatus.DELETE:
if part_mod.status == ModificationStatus.Delete:
return
start_sector = part_mod.start.convert(
@ -428,7 +600,7 @@ class DeviceHandler:
) -> None:
info(f'Creating subvolumes: {path}')
mount(path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
self.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}')
@ -463,7 +635,7 @@ class DeviceHandler:
if not part_mod.mapper_name:
raise ValueError('No device path specified for modification')
luks_handler = unlock_luks2_dev(
luks_handler = self.unlock_luks2_dev(
part_mod.safe_dev_path,
part_mod.mapper_name,
enc_conf.encryption_password,
@ -477,7 +649,7 @@ class DeviceHandler:
luks_handler = None
dev_path = part_mod.safe_dev_path
mount(
self.mount(
dev_path,
self._TMP_BTRFS_MOUNT,
create_target_mountpoint=True,
@ -496,6 +668,19 @@ class DeviceHandler:
if luks_handler is not None and luks_handler.mapper_dev is not None:
luks_handler.lock()
def unlock_luks2_dev(
self,
dev_path: Path,
mapper_name: str,
enc_password: Password | None,
) -> Luks2:
luks_handler = Luks2(dev_path, mapper_name=mapper_name, password=enc_password)
if not luks_handler.is_unlocked():
luks_handler.unlock()
return luks_handler
def umount_all_existing(self, device_path: Path) -> None:
debug(f'Unmounting all existing partitions: {device_path}')
@ -505,7 +690,7 @@ class DeviceHandler:
debug(f'Unmounting: {partition.path}')
# un-mount for existing encrypted partitions
if partition.fs_type == FilesystemType.CRYPTO_LUKS:
if partition.fs_type == FilesystemType.Crypto_luks:
Luks2(partition.path).lock()
else:
umount(partition.path, recursive=True)
@ -544,16 +729,49 @@ class DeviceHandler:
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}')
@staticmethod
def swapon(path: Path) -> None:
try:
SysCommand(['swapon', str(path)])
except SysCallError as err:
raise DiskError(f'Could not enable swap {path}:\n{err.message}')
# Sync with udev after wiping signatures
if filtered_part:
udev_sync()
def mount(
self,
dev_path: Path,
target_mountpoint: Path,
mount_fs: str | None = None,
create_target_mountpoint: bool = True,
options: list[str] = [],
) -> None:
if create_target_mountpoint and not target_mountpoint.exists():
target_mountpoint.mkdir(parents=True, exist_ok=True)
if not target_mountpoint.exists():
raise ValueError('Target mountpoint does not exist')
lsblk_info = get_lsblk_info(dev_path)
if target_mountpoint in lsblk_info.mountpoints:
info(f'Device already mounted at {target_mountpoint}')
return
cmd = ['mount']
if len(options):
cmd.extend(('-o', ','.join(options)))
if mount_fs:
cmd.extend(('-t', mount_fs))
cmd.extend((str(dev_path), str(target_mountpoint)))
command = ' '.join(cmd)
debug(f'Mounting {dev_path}: {command}')
try:
SysCommand(command)
except SysCallError as err:
raise DiskError(f'Could not mount {dev_path}: {command}\n{err.message}')
def detect_pre_mounted_mods(self, base_mountpoint: Path) -> list[DeviceModification]:
part_mods: dict[Path, list[PartitionModification]] = {}
@ -561,7 +779,7 @@ class DeviceHandler:
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):
if is_subpath(mountpoint, base_mountpoint):
path = Path(part_info.disk.device.path)
part_mods.setdefault(path, [])
part_mod = PartitionModification.from_existing_partition(part_info)
@ -622,5 +840,12 @@ class DeviceHandler:
self._wipe(block_device.device_info.path)
@staticmethod
def udev_sync() -> None:
try:
SysCommand('udevadm settle')
except SysCallError as err:
debug(f'Failed to synchronize with udev: {err}')
device_handler = DeviceHandler()

View File

@ -1,45 +1,27 @@
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.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties
from ..interactions.disk_conf import select_disk_config, select_lvm_config
from ..menu.abstract_menu import AbstractSubMenu
from ..output import FormattedOutput
@dataclass
@ -50,7 +32,7 @@ class DiskMenuConfig:
disk_encryption: DiskEncryption | None
class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
def __init__(self, disk_layout_config: DiskLayoutConfiguration | None):
if not disk_layout_config:
self._disk_menu_config = DiskMenuConfig(
@ -69,8 +51,8 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
btrfs_snapshot_config=snapshot_config,
)
menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True)
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
super().__init__(
self._item_group,
@ -113,16 +95,14 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
]
@override
async def show(self) -> DiskLayoutConfiguration | None: # type: ignore[override]
config: DiskMenuConfig | None = await super().show()
if config is None:
return None
def run(self, additional_title: str | None = None) -> DiskLayoutConfiguration | None:
super().run(additional_title=additional_title)
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
if self._disk_menu_config.disk_config:
self._disk_menu_config.disk_config.lvm_config = self._disk_menu_config.lvm_config
self._disk_menu_config.disk_config.btrfs_options = BtrfsOptions(snapshot_config=self._disk_menu_config.btrfs_snapshot_config)
self._disk_menu_config.disk_config.disk_encryption = self._disk_menu_config.disk_encryption
return self._disk_menu_config.disk_config
return None
@ -142,7 +122,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
return False
async def _select_disk_encryption(self, preset: DiskEncryption | None) -> DiskEncryption | None:
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
@ -154,12 +134,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
if not DiskEncryption.validate_enc(modifications, lvm_config):
return None
disk_encryption = await DiskEncryptionMenu(modifications, lvm_config=lvm_config, preset=preset).show()
disk_encryption = DiskEncryptionMenu(modifications, lvm_config=lvm_config, preset=preset).run()
return disk_encryption
async def _select_disk_layout_config(self, preset: DiskLayoutConfiguration | None) -> DiskLayoutConfiguration | None:
disk_config = await select_disk_config(preset)
def _select_disk_layout_config(self, preset: DiskLayoutConfiguration | None) -> DiskLayoutConfiguration | None:
disk_config = select_disk_config(preset)
if disk_config != preset:
self._menu_item_group.find_by_key('lvm_config').value = None
@ -167,20 +147,20 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
return disk_config
async def _select_lvm_config(self, preset: LvmConfiguration | None) -> LvmConfiguration | None:
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)
lvm_config = 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:
def _select_btrfs_snapshots(self, preset: SnapshotConfig | None) -> SnapshotConfig | None:
preset_type = preset.snapshot_type if preset else None
group = MenuItemGroup.from_enum(
@ -189,11 +169,13 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
preset=preset_type,
)
result = await Selection[SnapshotType](
result = SelectMenu[SnapshotType](
group,
allow_reset=True,
allow_skip=True,
).show()
frame=FrameProperties.min(tr('Snapshot type')),
alignment=Alignment.CENTER,
).run()
match result.type_:
case ResultType.Skip:
@ -222,7 +204,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
for mod in device_mods:
# create partition table
partition_table = as_table(mod.partitions)
partition_table = FormattedOutput.as_table(mod.partitions)
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe)
@ -231,7 +213,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
# 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_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n'
output = output_partition + output_btrfs
return output.rstrip()
@ -247,12 +229,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
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)
pv_table = FormattedOutput.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)
lvm_volumes = FormattedOutput.as_table(vol_gp.volumes)
output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes)
return output
@ -268,20 +250,19 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
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):
if disk_config and not DiskEncryption.validate_enc(disk_config.device_modifications, disk_config.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'
output = tr('Encryption type') + f': {EncryptionType.type_to_text(enc_type)}\n'
if enc_config.encryption_password:
output += tr('Password') + f': {enc_config.encryption_password.hidden()}\n'
if enc_type != EncryptionType.NO_ENCRYPTION:
if enc_type != EncryptionType.NoEncryption:
output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n'
if enc_config.partitions:
@ -295,582 +276,3 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
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])

View File

@ -1,26 +1,27 @@
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.curses_menu import EditMenu, SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties
from ..menu.abstract_menu import AbstractSubMenu
from ..models.device import DEFAULT_ITER_TIME, Fido2Device
from ..models.users import Password
from ..output import FormattedOutput
from ..utils.util import get_password
from .fido import Fido2
class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
@ -38,8 +39,8 @@ class DiskEncryptionMenu(AbstractSubMenu[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)
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
super().__init__(
self._item_group,
@ -51,9 +52,9 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
return [
MenuItem(
text=tr('Encryption type'),
action=lambda x: select_encryption_type(self._lvm_config, x),
action=lambda x: select_encryption_type(self._device_modifications, self._lvm_config, x),
value=self._enc_config.encryption_type,
preview_action=self._prev_type,
preview_action=self._preview,
key='encryption_type',
),
MenuItem(
@ -61,7 +62,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
action=lambda x: select_encrypted_password(),
value=self._enc_config.encryption_password,
dependencies=[self._check_dep_enc_type],
preview_action=self._prev_password,
preview_action=self._preview,
key='encryption_password',
),
MenuItem(
@ -69,7 +70,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
action=select_iteration_time,
value=self._enc_config.iter_time,
dependencies=[self._check_dep_enc_type],
preview_action=self._prev_iter_time,
preview_action=self._preview,
key='iter_time',
),
MenuItem(
@ -77,7 +78,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
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,
preview_action=self._preview,
key='partitions',
),
MenuItem(
@ -85,7 +86,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
action=self._select_lvm_vols,
value=self._enc_config.lvm_volumes,
dependencies=[self._check_dep_lvm_vols],
preview_action=self._prev_lvm_vols,
preview_action=self._preview,
key='lvm_volumes',
),
MenuItem(
@ -93,39 +94,37 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
action=select_hsm,
value=self._enc_config.hsm_device,
dependencies=[self._check_dep_enc_type],
preview_action=self._prev_hsm,
preview_action=self._preview,
key='hsm_device',
),
]
async def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]:
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 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:
if enc_type and enc_type != EncryptionType.NoEncryption:
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]:
if enc_type and enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks]:
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:
if enc_type and enc_type == EncryptionType.LuksOnLvm:
return True
return False
@override
async def show(self) -> DiskEncryption | None:
enc_config = await super().show()
if enc_config is None:
return None
def run(self, additional_title: str | None = None) -> DiskEncryption | None:
super().run(additional_title=additional_title)
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
@ -137,19 +136,19 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
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:
if enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and enc_partitions:
enc_lvm_vols = []
if enc_type == EncryptionType.LUKS_ON_LVM:
if enc_type == EncryptionType.LuksOnLvm:
enc_partitions = []
if enc_type != EncryptionType.NO_ENCRYPTION and enc_password and (enc_partitions or enc_lvm_vols):
if enc_type != EncryptionType.NoEncryption 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,
hsm_device=self._enc_config.hsm_device,
iter_time=iter_time or DEFAULT_ITER_TIME,
)
@ -158,22 +157,22 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _preview(self, item: MenuItem) -> str | None:
output = ''
if (enc_type := self._prev_type(item)) is not None:
if (enc_type := self._prev_type()) is not None:
output += enc_type
if (enc_pwd := self._prev_password(item)) is not None:
if (enc_pwd := self._prev_password()) is not None:
output += f'\n{enc_pwd}'
if (iter_time := self._prev_iter_time(item)) is not None:
if (iter_time := self._prev_iter_time()) is not None:
output += f'\n{iter_time}'
if (fido_device := self._prev_hsm(item)) is not None:
if (fido_device := self._prev_hsm()) is not None:
output += f'\n{fido_device}'
if (partitions := self._prev_partitions(item)) is not None:
if (partitions := self._prev_partitions()) is not None:
output += f'\n\n{partitions}'
if (lvm := self._prev_lvm_vols(item)) is not None:
if (lvm := self._prev_lvm_vols()) is not None:
output += f'\n\n{lvm}'
if not output:
@ -181,84 +180,91 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
return output
def _prev_type(self, item: MenuItem) -> str | None:
def _prev_type(self) -> str | None:
enc_type = self._item_group.find_by_key('encryption_type').value
if enc_type:
enc_text = enc_type.type_to_text()
enc_text = EncryptionType.type_to_text(enc_type)
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()}'
def _prev_password(self) -> str | None:
enc_pwd = self._item_group.find_by_key('encryption_password').value
if enc_pwd:
return f'{tr("Encryption password")}: {enc_pwd.hidden()}'
return None
def _prev_partitions(self, item: MenuItem) -> str | None:
if item.value:
def _prev_partitions(self) -> str | None:
partitions: list[PartitionModification] | None = self._item_group.find_by_key('partitions').value
if partitions:
output = tr('Partitions to be encrypted') + '\n'
output += as_table(item.value)
output += FormattedOutput.as_table(partitions)
return output.rstrip()
return None
def _prev_lvm_vols(self, item: MenuItem) -> str | None:
if item.value:
def _prev_lvm_vols(self) -> str | None:
volumes: list[PartitionModification] | None = self._item_group.find_by_key('lvm_volumes').value
if volumes:
output = tr('LVM volumes to be encrypted') + '\n'
output += as_table(item.value)
output += FormattedOutput.as_table(volumes)
return output.rstrip()
return None
def _prev_hsm(self, item: MenuItem) -> str | None:
if not item.value:
return None
def _prev_hsm(self) -> str | None:
fido_device: Fido2Device | None = self._item_group.find_by_key('hsm_device').value
fido_device: Fido2Device = item.value
if not fido_device:
return None
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
def _prev_iter_time(self) -> str | None:
iter_time = self._item_group.find_by_key('iter_time').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'
if iter_time and enc_type != EncryptionType.NoEncryption:
return f'{tr("Iteration time")}: {iter_time}ms'
return None
async def select_encryption_type(
def select_encryption_type(
device_modifications: list[DeviceModification],
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]
options = [EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm]
else:
options = [EncryptionType.LUKS]
options = [EncryptionType.Luks]
if not preset:
preset = options[0]
preset_value = preset.type_to_text()
preset_value = EncryptionType.type_to_text(preset)
items = [MenuItem(o.type_to_text(), value=o) for o in options]
items = [MenuItem(EncryptionType.type_to_text(o), value=o) for o in options]
group = MenuItemGroup(items)
group.set_focus_by_value(preset_value)
result = await Selection[EncryptionType](
result = SelectMenu[EncryptionType](
group,
header=tr('Select encryption type'),
allow_skip=True,
allow_reset=True,
).show()
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Encryption type')),
).run()
match result.type_:
case ResultType.Reset:
@ -269,9 +275,10 @@ async def select_encryption_type(
return result.get_value()
async def select_encrypted_password() -> Password | None:
def select_encrypted_password() -> Password | None:
header = tr('Enter disk encryption password (leave blank for no encryption)') + '\n'
password = await get_password(
password = get_password(
text=tr('Disk encryption password'),
header=header,
allow_skip=True,
)
@ -279,7 +286,7 @@ async def select_encrypted_password() -> Password | None:
return password
async def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
header = tr('Select a FIDO2 device to use for HSM') + '\n'
try:
@ -290,11 +297,12 @@ async def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
if fido_devices:
group = MenuHelper(data=fido_devices).create_menu_group()
result = await Selection[Fido2Device](
result = SelectMenu[Fido2Device](
group,
header=header,
alignment=Alignment.CENTER,
allow_skip=True,
).show()
).run()
match result.type_:
case ResultType.Reset:
@ -307,7 +315,7 @@ async def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
return None
async def select_partitions_to_encrypt(
def select_partitions_to_encrypt(
modification: list[DeviceModification],
preset: list[PartitionModification],
) -> list[PartitionModification]:
@ -321,15 +329,15 @@ async def select_partitions_to_encrypt(
avail_partitions = [p for p in partitions if not p.exists()]
if avail_partitions:
group = MenuItemGroup.from_objects(avail_partitions)
group = MenuHelper(data=avail_partitions).create_menu_group()
group.set_selected_by_value(preset)
result = await Table[PartitionModification](
header=tr('Select disks for the installation'),
group=group,
allow_skip=True,
result = SelectMenu[PartitionModification](
group,
alignment=Alignment.CENTER,
multi=True,
).show()
allow_skip=True,
).run()
match result.type_:
case ResultType.Reset:
@ -343,22 +351,20 @@ async def select_partitions_to_encrypt(
return []
async def select_lvm_vols_to_encrypt(
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)
group = MenuHelper(data=volumes).create_menu_group()
result = await Table[LvmVolume](
header=tr('Select disks for the installation'),
group=group,
allow_skip=True,
result = SelectMenu[LvmVolume](
group,
alignment=Alignment.CENTER,
multi=True,
).show()
).run()
match result.type_:
case ResultType.Reset:
@ -372,12 +378,15 @@ async def select_lvm_vols_to_encrypt(
return []
async def select_iteration_time(preset: int | None = None) -> int | None:
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'
header += tr(f'Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000') + '\n'
def validate_iter_time(value: str | None) -> str | None:
if not value:
return None
def validate_iter_time(value: str) -> str | None:
try:
iter_time = int(value)
if iter_time < 100:
@ -388,19 +397,21 @@ async def select_iteration_time(preset: int | None = None) -> int | None:
except ValueError:
return tr('Please enter a valid number')
result = await Input(
result = EditMenu(
tr('Iteration time'),
header=header,
alignment=Alignment.CENTER,
allow_skip=True,
default_value=str(preset) if preset else str(DEFAULT_ITER_TIME),
validator_callback=validate_iter_time,
).show()
default_text=str(preset) if preset else str(DEFAULT_ITER_TIME),
validator=validate_iter_time,
).input()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
if not result.get_value():
if not result.text():
return preset
return int(result.get_value())
return int(result.text())
case ResultType.Reset:
return None

View File

@ -1,13 +1,15 @@
from __future__ import annotations
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
from ..exceptions import SysCallError
from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes_from_str
from ..models.users import Password
from ..output import error, info
class Fido2:
@ -61,8 +63,8 @@ class Fido2:
Output example:
PATH MANUFACTURER PRODUCT
/dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID
PATH MANUFACTURER PRODUCT
/dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID
"""
# to prevent continuous reloading which will slow
@ -99,14 +101,17 @@ class Fido2:
return cls._cryptenroll_devices
@staticmethod
def fido2_enroll(hsm_device: Fido2Device, dev_path: Path, password: Password) -> None:
@classmethod
def fido2_enroll(
cls,
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():
@ -116,3 +121,5 @@ class Fido2:
if bytes('please enter security token pin', 'UTF-8') in worker._trace_log.lower():
worker.write(bytes(getpass.getpass(' '), 'UTF-8'))
pin_inputted = True
info('You might need to touch the FIDO2 device to unlock it if no prompt comes up after 3 seconds')

View File

@ -1,20 +1,15 @@
from __future__ import annotations
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 (
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import Tui
from ..interactions.general_conf import ask_abort
from ..luks import Luks2
from ..models.device import (
DiskEncryption,
DiskLayoutConfiguration,
DiskLayoutType,
@ -28,6 +23,8 @@ from archinstall.lib.models.device import (
Size,
Unit,
)
from ..output import debug, info
from .device_handler import device_handler
class FilesystemHandler:
@ -35,7 +32,7 @@ class FilesystemHandler:
self._disk_config = disk_config
self._enc_config = disk_config.disk_encryption
def perform_filesystem_operations(self) -> None:
def perform_filesystem_operations(self, show_countdown: bool = True) -> None:
if self._disk_config.config_type == DiskLayoutType.Pre_mount:
debug('Disk layout configuration is set to pre-mount, not performing any operations')
return
@ -46,6 +43,9 @@ class FilesystemHandler:
debug('No modifications required')
return
if show_countdown:
self._final_warning()
# Setup the blockdevice, filesystem (and optionally encryption).
# Once that's done, we'll hand over to perform_installation()
@ -56,7 +56,7 @@ class FilesystemHandler:
for mod in device_mods:
device_handler.partition(mod)
udev_sync()
device_handler.udev_sync()
if self._disk_config.lvm_config:
for mod in device_mods:
@ -70,7 +70,7 @@ class FilesystemHandler:
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():
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(
@ -100,7 +100,7 @@ class FilesystemHandler:
device_handler.format(part_mod.safe_fs_type, part_mod.safe_dev_path)
# synchronize with udev before using lsblk
udev_sync()
device_handler.udev_sync()
lsblk_info = device_handler.fetch_part_info(part_mod.safe_dev_path)
@ -113,7 +113,7 @@ class FilesystemHandler:
# 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'),
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'),
}
@ -139,25 +139,34 @@ class FilesystemHandler:
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:
if enc_config.encryption_type == EncryptionType.LvmOnLuks:
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:
# export the lvm group safely otherwise the Luks cannot be closed
self._safely_close_lvm(lvm_config)
for luks in enc_mods.values():
luks.lock()
elif enc_config.encryption_type == EncryptionType.LuksOnLvm:
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()
self._safely_close_lvm(lvm_config)
def _safely_close_lvm(self, lvm_config: LvmConfiguration) -> None:
for vg in lvm_config.vol_groups:
for vol in vg.volumes:
device_handler.lvm_vol_change(vol, False)
device_handler.lvm_export_vg(vg)
def _setup_lvm(
self,
lvm_config: LvmConfiguration,
@ -168,10 +177,10 @@ class FilesystemHandler:
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)
device_handler.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)
vg_info = device_handler.lvm_group_info(vg.name)
if not vg_info:
raise ValueError('Unable to fetch VG info')
@ -201,11 +210,11 @@ class FilesystemHandler:
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)
device_handler.lvm_vol_create(vg.name, lv, offset)
while True:
debug('Fetching LVM volume info')
lv_info = lvm_vol_info(lv.name)
lv_info = device_handler.lvm_vol_info(lv.name)
if lv_info is not None:
break
@ -230,7 +239,7 @@ class FilesystemHandler:
# find the mapper device yet
device_handler.format(vol.fs_type, path)
if vol.fs_type == FilesystemType.BTRFS:
if vol.fs_type == FilesystemType.Btrfs:
device_handler.create_lvm_btrfs_subvolumes(path, vol.btrfs_subvols, vol.mount_options)
def _lvm_create_pvs(
@ -243,7 +252,7 @@ class FilesystemHandler:
for vg in lvm_config.vol_groups:
pv_paths |= self._get_all_pv_dev_paths(vg.pvs, enc_mods)
lvm_pv_create(pv_paths)
device_handler.lvm_pv_create(pv_paths)
def _get_all_pv_dev_paths(
self,
@ -318,10 +327,27 @@ class FilesystemHandler:
# 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]):
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(
device_handler.lvm_vol_reduce(
largest_vol.safe_dev_path,
Size(256, Unit.MiB, SectorSize.default()),
)
def _final_warning(self) -> bool:
# Issue a final warning before we continue with something un-revertable.
# We mention the drive one last time, and count from 5 to 0.
out = tr('Starting device modifications in ')
Tui.print(out, row=0, endl='', clear_screen=True)
try:
countdown = '\n5...4...3...2...1\n'
for c in countdown:
Tui.print(c, row=0, endl='')
time.sleep(0.25)
except KeyboardInterrupt:
with Tui():
ask_abort()
return True

View File

@ -1,196 +0,0 @@
import json
import time
from collections.abc import Iterable
from pathlib import Path
from typing import Literal, overload
from archinstall.lib.command import SysCommand, SysCommandWorker
from archinstall.lib.disk.utils import udev_sync
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.models.device import (
LvmGroupInfo,
LvmPVInfo,
LvmVolume,
LvmVolumeGroup,
LvmVolumeInfo,
SectorSize,
Size,
Unit,
)
def _lvm_info(
cmd: str,
info_type: Literal['lv', 'vg', 'pvseg'],
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
raw_info = SysCommand(cmd).decode().split('\n')
# for whatever reason the output sometimes contains
# "File descriptor X leaked leaked on vgs invocation
data = '\n'.join(raw for raw in raw_info if 'File descriptor' not in raw)
debug(f'LVM info: {data}')
reports = json.loads(data)
for report in reports['report']:
if len(report[info_type]) != 1:
raise ValueError('Report does not contain any entry')
entry = report[info_type][0]
match info_type:
case 'pvseg':
return LvmPVInfo(
pv_name=Path(entry['pv_name']),
lv_name=entry['lv_name'],
vg_name=entry['vg_name'],
)
case 'lv':
return LvmVolumeInfo(
lv_name=entry['lv_name'],
vg_name=entry['vg_name'],
lv_size=Size(int(entry['lv_size'][:-1]), Unit.B, SectorSize.default()),
)
case 'vg':
return LvmGroupInfo(
vg_uuid=entry['vg_uuid'],
vg_size=Size(int(entry['vg_size'][:-1]), Unit.B, SectorSize.default()),
)
return None
@overload
def _lvm_info_with_retry(cmd: str, info_type: Literal['lv']) -> LvmVolumeInfo | None: ...
@overload
def _lvm_info_with_retry(cmd: str, info_type: Literal['vg']) -> LvmGroupInfo | None: ...
@overload
def _lvm_info_with_retry(cmd: str, info_type: Literal['pvseg']) -> LvmPVInfo | None: ...
def _lvm_info_with_retry(
cmd: str,
info_type: Literal['lv', 'vg', 'pvseg'],
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
# Retry for up to 5 mins
max_retries = 100
for attempt in range(max_retries):
try:
return _lvm_info(cmd, info_type)
except ValueError:
if attempt < max_retries - 1:
debug(f'LVM info query failed (attempt {attempt + 1}/{max_retries}), retrying in 3 seconds...')
time.sleep(3)
debug(f'LVM info query failed after {max_retries} attempts')
return None
def lvm_vol_info(lv_name: str) -> LvmVolumeInfo | None:
cmd = f'lvs --reportformat json --unit B -S lv_name={lv_name}'
return _lvm_info_with_retry(cmd, 'lv')
def lvm_group_info(vg_name: str) -> LvmGroupInfo | None:
cmd = f'vgs --reportformat json --unit B -o vg_name,vg_uuid,vg_size -S vg_name={vg_name}'
return _lvm_info_with_retry(cmd, 'vg')
def lvm_pvseg_info(vg_name: str, lv_name: str) -> LvmPVInfo | None:
cmd = f'pvs --segments -o+lv_name,vg_name -S vg_name={vg_name},lv_name={lv_name} --reportformat json '
return _lvm_info_with_retry(cmd, 'pvseg')
def lvm_vol_change(vol: LvmVolume, activate: bool) -> None:
active_flag = 'y' if activate else 'n'
cmd = f'lvchange -a {active_flag} {vol.safe_dev_path}'
debug(f'lvchange volume: {cmd}')
SysCommand(cmd)
def lvm_export_vg(vg: LvmVolumeGroup) -> None:
cmd = f'vgexport {vg.name}'
debug(f'vgexport: {cmd}')
SysCommand(cmd)
def lvm_import_vg(vg: LvmVolumeGroup) -> None:
# Check if the VG is actually exported before trying to import it
check_cmd = f'vgs --noheadings -o vg_exported {vg.name}'
try:
result = SysCommand(check_cmd)
is_exported = result.decode().strip() == 'exported'
except SysCallError:
# VG might not exist yet, skip import
debug(f'Volume group {vg.name} not found, skipping import')
return
if not is_exported:
debug(f'Volume group {vg.name} is already active (not exported), skipping import')
return
cmd = f'vgimport {vg.name}'
debug(f'vgimport: {cmd}')
SysCommand(cmd)
def lvm_vol_reduce(vol_path: Path, amount: Size) -> None:
val = amount.format_size(Unit.B, include_unit=False)
cmd = f'lvreduce -L -{val}B {vol_path}'
debug(f'Reducing LVM volume size: {cmd}')
SysCommand(cmd)
def lvm_pv_create(pvs: Iterable[Path]) -> None:
pvs_str = ' '.join(str(pv) for pv in pvs)
# Signatures are already wiped by wipefs, -f is just for safety
cmd = f'pvcreate -f --yes {pvs_str}'
# note flags used in scripting
debug(f'Creating LVM PVS: {cmd}')
SysCommand(cmd)
# Sync with udev to ensure the PVs are visible
udev_sync()
def lvm_vg_create(pvs: Iterable[Path], vg_name: str) -> None:
pvs_str = ' '.join(str(pv) for pv in pvs)
cmd = f'vgcreate --yes --force {vg_name} {pvs_str}'
debug(f'Creating LVM group: {cmd}')
SysCommand(cmd)
# Sync with udev to ensure the VG is visible
udev_sync()
def lvm_vol_create(vg_name: str, volume: LvmVolume, offset: Size | None = None) -> None:
if offset is not None:
length = volume.length - offset
else:
length = volume.length
length_str = length.format_size(Unit.B, include_unit=False)
cmd = f'lvcreate --yes -L {length_str}B {vg_name} -n {volume.name}'
debug(f'Creating volume: {cmd}')
worker = SysCommandWorker(cmd)
worker.poll()
worker.write(b'y\n', line_ending=False)
volume.vg_name = vg_name
volume.dev_path = Path(f'/dev/{vg_name}/{volume.name}')

View File

@ -1,11 +1,9 @@
from __future__ import annotations
import re
from pathlib import Path
from typing import override
from archinstall.lib.disk.subvolume_menu import SubvolumeMenu
from archinstall.lib.menu.helpers import Confirmation, Input, Selection
from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.menu.util import prompt_dir
from archinstall.lib.models.device import (
BtrfsMountOption,
DeviceModification,
@ -20,9 +18,15 @@ from archinstall.lib.models.device import (
Unit,
)
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.curses_menu import EditMenu, SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, Orientation
from ..menu.list_manager import ListManager
from ..output import FormattedOutput
from ..utils.util import prompt_dir
from .subvolume_menu import SubvolumeMenu
class FreeSpace:
@ -57,8 +61,8 @@ class DiskSegment:
return self.segment.table_data()
part_mod = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType._UNKNOWN,
status=ModificationStatus.Create,
type=PartitionType._Unknown,
start=self.segment.start,
length=self.segment.length,
)
@ -189,27 +193,23 @@ class PartitioningList(ListManager[DiskSegment]):
def get_part_mods(disk_segments: list[DiskSegment]) -> list[PartitionModification]:
return [s.segment for s in disk_segments if isinstance(s.segment, PartitionModification)]
async def show(self) -> DeviceModification | None:
disk_segments = await super()._run()
if not disk_segments:
return None
def get_device_mod(self) -> DeviceModification:
disk_segments = super().run()
partitions = self.get_part_mods(disk_segments)
return DeviceModification(self._device, self._wipe, partitions)
@override
async def _run_actions_on_entry(self, entry: DiskSegment) -> None:
def _run_actions_on_entry(self, entry: DiskSegment) -> None:
# Do not create a menu when the segment is free space
if isinstance(entry.segment, FreeSpace):
self._data = await self.handle_action('', entry, self._data)
self._data = self.handle_action('', entry, self._data)
else:
await super()._run_actions_on_entry(entry)
super()._run_actions_on_entry(entry)
@override
def selected_action_display(self, selection: DiskSegment) -> str:
if isinstance(selection.segment, PartitionModification):
if selection.segment.status == ModificationStatus.CREATE:
if selection.segment.status == ModificationStatus.Create:
return tr('Partition - New')
elif selection.segment.is_delete() and selection.segment.dev_path:
title = tr('Partition') + '\n\n'
@ -238,7 +238,7 @@ class PartitioningList(ListManager[DiskSegment]):
# was marked as formatting, otherwise we run into issues where
# 1. select a new fs -> potentially mark as wipe now
# 2. Switch back to old filesystem -> should unmark wipe now, but
# how do we know it was the original one?
# how do we know it was the original one?
not_filter += [
self._actions['set_filesystem'],
self._actions['mark_bootable'],
@ -255,7 +255,7 @@ class PartitioningList(ListManager[DiskSegment]):
]
# non btrfs partitions shouldn't get btrfs options
if selection.segment.fs_type != FilesystemType.BTRFS:
if selection.segment.fs_type != FilesystemType.Btrfs:
not_filter += [
self._actions['btrfs_mark_compressed'],
self._actions['btrfs_mark_nodatacow'],
@ -267,7 +267,7 @@ class PartitioningList(ListManager[DiskSegment]):
return [o for o in options if o not in not_filter]
@override
async def handle_action(
def handle_action(
self,
action: str,
entry: DiskSegment | None,
@ -278,20 +278,20 @@ class PartitioningList(ListManager[DiskSegment]):
match action_key:
case 'suggest_partition_layout':
part_mods = self.get_part_mods(data)
device_mod = await self._suggest_partition_layout(part_mods)
device_mod = self._suggest_partition_layout(part_mods)
if device_mod and device_mod.partitions:
data = self.as_segments(device_mod.partitions)
self._wipe = device_mod.wipe
self._prompt = self._info + self.wipe_str()
case 'remove_added_partitions':
if await self._reset_confirmation():
if self._reset_confirmation():
data = [s for s in data if isinstance(s.segment, PartitionModification) and s.segment.is_exists_or_modify()]
elif isinstance(entry.segment, PartitionModification):
partition = entry.segment
action_key = [k for k, v in self._actions.items() if v == action][0]
match action_key:
case 'assign_mountpoint':
new_mountpoint = await self._prompt_mountpoint()
new_mountpoint = self._prompt_mountpoint()
if not partition.is_swap():
if partition.is_home():
partition.invert_flag(PartitionFlag.LINUX_HOME)
@ -307,7 +307,7 @@ class PartitioningList(ListManager[DiskSegment]):
partition.flags = []
partition.set_flag(PartitionFlag.LINUX_HOME)
case 'mark_formatting':
await self._prompt_formatting(partition)
self._prompt_formatting(partition)
case 'mark_bootable':
if not partition.is_swap():
partition.invert_flag(PartitionFlag.BOOT)
@ -322,7 +322,7 @@ class PartitioningList(ListManager[DiskSegment]):
partition.invert_flag(PartitionFlag.ESP)
partition.invert_flag(PartitionFlag.XBOOTLDR)
case 'set_filesystem':
fs_type = await self._prompt_partition_fs_type()
fs_type = self._prompt_partition_fs_type()
if partition.is_swap():
partition.invert_flag(PartitionFlag.SWAP)
@ -332,21 +332,20 @@ class PartitioningList(ListManager[DiskSegment]):
partition.flags = []
partition.set_flag(PartitionFlag.SWAP)
# btrfs subvolumes will define mountpoints
if fs_type == FilesystemType.BTRFS:
if fs_type == FilesystemType.Btrfs:
partition.mountpoint = None
case 'btrfs_mark_compressed':
self._toggle_mount_option(partition, BtrfsMountOption.compress)
case 'btrfs_mark_nodatacow':
self._toggle_mount_option(partition, BtrfsMountOption.nodatacow)
case 'btrfs_set_subvolumes':
await self._set_btrfs_subvolumes(partition)
self._set_btrfs_subvolumes(partition)
case 'delete_partition':
data = self._delete_partition(partition, data)
else:
part_mods = self.get_part_mods(data)
index = data.index(entry)
part = await self._create_new_partition(entry.segment)
part_mods.insert(index, part)
part_mods.insert(index, self._create_new_partition(entry.segment))
data = self.as_segments(part_mods)
return data
@ -357,7 +356,7 @@ class PartitioningList(ListManager[DiskSegment]):
data: list[DiskSegment],
) -> list[DiskSegment]:
if entry.is_exists_or_modify():
entry.status = ModificationStatus.DELETE
entry.status = ModificationStatus.Delete
part_mods = self.get_part_mods(data)
else:
part_mods = [d.segment for d in data if isinstance(d.segment, PartitionModification) and d.segment != entry]
@ -379,53 +378,52 @@ class PartitioningList(ListManager[DiskSegment]):
else:
partition.mount_options = [o for o in partition.mount_options if o != option.value]
async def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None:
subvols = await SubvolumeMenu(
def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None:
partition.btrfs_subvols = SubvolumeMenu(
partition.btrfs_subvols,
None,
).show()
).run()
if subvols is not None:
partition.btrfs_subvols = subvols
async def _prompt_formatting(self, partition: PartitionModification) -> None:
def _prompt_formatting(self, partition: PartitionModification) -> None:
# an existing partition can toggle between Exist or Modify
if partition.is_modify():
partition.status = ModificationStatus.EXIST
partition.status = ModificationStatus.Exist
return
elif partition.exists():
partition.status = ModificationStatus.MODIFY
partition.status = ModificationStatus.Modify
# If we mark a partition for formatting, but the format is CRYPTO LUKS, there's no point in formatting it really
# without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set,
# it's safe to change the filesystem for this partition.
if partition.fs_type == FilesystemType.CRYPTO_LUKS:
if partition.fs_type == FilesystemType.Crypto_luks:
prompt = tr('This partition is currently encrypted, to format it a filesystem has to be specified') + '\n'
fs_type = await self._prompt_partition_fs_type(prompt)
fs_type = self._prompt_partition_fs_type(prompt)
partition.fs_type = fs_type
if fs_type == FilesystemType.BTRFS:
if fs_type == FilesystemType.Btrfs:
partition.mountpoint = None
async def _prompt_mountpoint(self) -> Path:
header = tr('Partition mount-points are relative to inside the installation, the boot would be /boot as an example.') + '\n\n'
header += tr('Enter a mountpoint')
def _prompt_mountpoint(self) -> Path:
header = tr('Partition mount-points are relative to inside the installation, the boot would be /boot as an example.') + '\n'
prompt = tr('Mountpoint')
mountpoint = await prompt_dir(header, validate=False, allow_skip=False)
mountpoint = prompt_dir(prompt, header, validate=False, allow_skip=False)
assert mountpoint
return mountpoint
async def _prompt_partition_fs_type(self, prompt: str | None = None) -> FilesystemType:
fs_types = filter(lambda fs: fs != FilesystemType.CRYPTO_LUKS, FilesystemType)
def _prompt_partition_fs_type(self, prompt: str | None = None) -> FilesystemType:
fs_types = filter(lambda fs: fs != FilesystemType.Crypto_luks, FilesystemType)
items = [MenuItem(fs.value, value=fs) for fs in fs_types]
group = MenuItemGroup(items, sort_items=False)
result = await Selection[FilesystemType](
result = SelectMenu[FilesystemType](
group,
header=prompt,
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Filesystem')),
allow_skip=False,
).show()
).run()
match result.type_:
case ResultType.Selection:
@ -439,7 +437,7 @@ class PartitioningList(ListManager[DiskSegment]):
max_size: Size,
text: str,
) -> Size | None:
match = re.match(r'^\s*([0-9]+)\s*([a-zA-Z%]*)\s*$', text, re.I)
match = re.match(r'([0-9]+)([a-zA-Z|%]*)', text, re.I)
if not match:
return None
@ -465,7 +463,7 @@ class PartitioningList(ListManager[DiskSegment]):
return size
async def _prompt_size(self, free_space: FreeSpace) -> Size:
def _prompt_size(self, free_space: FreeSpace) -> Size:
def validate(value: str | None) -> str | None:
if not value:
return None
@ -479,7 +477,7 @@ class PartitioningList(ListManager[DiskSegment]):
sector_size = device_info.sector_size
text = tr('Selected free space segment on device {}:').format(device_info.path) + '\n\n'
free_space_table = as_table([free_space])
free_space_table = FormattedOutput.as_table([free_space])
prompt = text + free_space_table + '\n'
max_sectors = free_space.length.format_size(Unit.sectors, sector_size)
@ -487,16 +485,18 @@ class PartitioningList(ListManager[DiskSegment]):
prompt += tr('Size: {} / {}').format(max_sectors, max_bytes) + '\n\n'
prompt += tr('All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...') + '\n'
prompt += tr('If no unit is provided, the value is interpreted as sectors') + '\n\n'
prompt += tr('If no unit is provided, the value is interpreted as sectors') + '\n'
max_size = free_space.length
prompt += tr('Enter a size (default: {}): ').format(max_size.format_highest())
result = await Input(
title = tr('Size (default: {}): ').format(max_size.format_highest())
result = EditMenu(
title,
header=f'{prompt}\b',
allow_skip=True,
validator_callback=validate,
).show()
validator=validate,
).input()
size: Size | None = None
@ -504,30 +504,28 @@ class PartitioningList(ListManager[DiskSegment]):
case ResultType.Skip:
size = max_size
case ResultType.Selection:
value = result.get_value()
value = result.text()
if value:
size = self._validate_value(sector_size, max_size, value)
else:
size = max_size
case _:
raise ValueError('Unhandled result type')
assert size
return size
async def _create_new_partition(self, free_space: FreeSpace) -> PartitionModification:
length = await self._prompt_size(free_space)
def _create_new_partition(self, free_space: FreeSpace) -> PartitionModification:
length = self._prompt_size(free_space)
fs_type = await self._prompt_partition_fs_type()
fs_type = self._prompt_partition_fs_type()
mountpoint = None
if fs_type not in (FilesystemType.BTRFS, FilesystemType.LINUX_SWAP):
mountpoint = await self._prompt_mountpoint()
if fs_type not in (FilesystemType.Btrfs, FilesystemType.LinuxSwap):
mountpoint = self._prompt_mountpoint()
partition = PartitionModification(
status=ModificationStatus.CREATE,
type=PartitionType.PRIMARY,
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=free_space.start,
length=length,
fs_type=fs_type,
@ -545,41 +543,42 @@ class PartitioningList(ListManager[DiskSegment]):
return partition
async def _reset_confirmation(self) -> bool:
def _reset_confirmation(self) -> bool:
prompt = tr('This will remove all newly added partitions, continue?') + '\n'
result = await Confirmation(
result = SelectMenu[bool](
MenuItemGroup.yes_no(),
header=prompt,
alignment=Alignment.CENTER,
orientation=Orientation.HORIZONTAL,
columns=2,
reset_warning_msg=prompt,
allow_skip=False,
allow_reset=False,
).show()
).run()
return result.item() == MenuItem.yes()
async def _suggest_partition_layout(
def _suggest_partition_layout(
self,
data: list[PartitionModification],
) -> DeviceModification | None:
# if modifications have been done already, inform the user
# that this operation will erase those modifications
if any([not entry.exists() for entry in data]):
if not await self._reset_confirmation():
if not self._reset_confirmation():
return None
from archinstall.lib.disk.disk_menu import suggest_single_disk_layout
from ..interactions.disk_conf import suggest_single_disk_layout
return await suggest_single_disk_layout(self._device)
return suggest_single_disk_layout(self._device)
async def manual_partitioning(
def manual_partitioning(
device_mod: DeviceModification,
partition_table: PartitionTable,
) -> DeviceModification | None:
menu_list = PartitioningList(device_mod, partition_table)
mod = await menu_list.show()
if not mod:
return None
mod = menu_list.get_device_mod()
if menu_list.is_last_choice_cancel():
return device_mod

View File

@ -1,12 +1,14 @@
from pathlib import Path
from typing import assert_never, override
from archinstall.lib.menu.helpers import Input
from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.menu.util import prompt_dir
from archinstall.lib.models.device import SubvolumeModification
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import EditMenu
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment
from ..menu.list_manager import ListManager
from ..utils.util import prompt_dir
class SubvolumeMenu(ListManager[SubvolumeModification]):
@ -28,40 +30,38 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
prompt,
)
async def show(self) -> list[SubvolumeModification] | None:
return await super()._run()
@override
def selected_action_display(self, selection: SubvolumeModification) -> str:
return str(selection.name)
async def _add_subvolume(self, preset: SubvolumeModification | None = None) -> SubvolumeModification | None:
def _add_subvolume(self, preset: SubvolumeModification | None = None) -> SubvolumeModification | None:
def validate(value: str | None) -> str | None:
if value:
return None
return tr('Value cannot be empty')
result = await Input(
header=tr('Enter subvolume name'),
result = EditMenu(
tr('Subvolume name'),
alignment=Alignment.CENTER,
allow_skip=True,
default_value=str(preset.name) if preset else None,
validator_callback=validate,
).show()
default_text=str(preset.name) if preset else None,
validator=validate,
).input()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
name = result.get_value()
name = result.text()
case ResultType.Reset:
raise ValueError('Unhandled result type')
case _:
assert_never(result.type_)
header = f'{tr("Subvolume name")}: {name}\n\n'
header += tr('Enter subvolume mountpoint')
header = f'{tr("Subvolume name")}: {name}\n'
path = await prompt_dir(
path = prompt_dir(
tr('Subvolume mountpoint'),
header=header,
allow_skip=True,
validate=True,
@ -74,29 +74,29 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
return SubvolumeModification(Path(name), path)
@override
async def handle_action(
def handle_action(
self,
action: str,
entry: SubvolumeModification | None,
data: list[SubvolumeModification],
) -> list[SubvolumeModification]:
if action == self._actions[0]:
new_subvolume = await self._add_subvolume()
if action == self._actions[0]: # add
new_subvolume = self._add_subvolume()
if new_subvolume is not None:
# in case a user with the same username as an existing user
# was created we'll replace the existing one
data = [d for d in data if d.name != new_subvolume.name]
data += [new_subvolume]
elif entry is not None:
if action == self._actions[1]:
new_subvolume = await self._add_subvolume(entry)
elif entry is not None: # edit
if action == self._actions[1]: # edit subvolume
new_subvolume = self._add_subvolume(entry)
if new_subvolume is not None:
# we'll remove the original subvolume and add the modified version
data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name]
data += [new_subvolume]
elif action == self._actions[2]:
elif action == self._actions[2]: # delete
data = [d for d in data if d != entry]
return data

View File

@ -2,10 +2,10 @@ from pathlib import Path
from pydantic import BaseModel
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import DiskError, SysCallError
from archinstall.lib.log import debug, info, warn
from archinstall.lib.general import SysCommand
from archinstall.lib.models.device import LsblkInfo
from archinstall.lib.output import debug, warn
class LsblkOutput(BaseModel):
@ -67,12 +67,12 @@ def get_lsblk_output() -> LsblkOutput:
def find_lsblk_info(
dev_path: Path | str,
info_list: list[LsblkInfo],
info: list[LsblkInfo],
) -> LsblkInfo | None:
if isinstance(dev_path, str):
dev_path = Path(dev_path)
for lsblk_info in info_list:
for lsblk_info in info:
if lsblk_info.path == dev_path:
return lsblk_info
@ -110,69 +110,6 @@ def disk_layouts() -> str:
return lsblk_output.model_dump_json(indent=4)
def get_parent_device_path(dev_path: Path) -> Path:
lsblk = get_lsblk_info(dev_path)
return Path(f'/dev/{lsblk.pkname}')
def get_unique_path_for_device(dev_path: Path) -> Path | None:
paths = Path('/dev/disk/by-id').glob('*')
linked_targets = {p.resolve(): p for p in paths}
linked_wwn_targets = {p: linked_targets[p] for p in linked_targets if p.name.startswith('wwn-') or p.name.startswith('nvme-eui.')}
if dev_path in linked_wwn_targets:
return linked_wwn_targets[dev_path]
if dev_path in linked_targets:
return linked_targets[dev_path]
return None
def udev_sync() -> None:
try:
SysCommand('udevadm settle')
except SysCallError as err:
debug(f'Failed to synchronize with udev: {err}')
def mount(
dev_path: Path,
target_mountpoint: Path,
mount_fs: str | None = None,
create_target_mountpoint: bool = True,
options: list[str] = [],
) -> None:
if create_target_mountpoint and not target_mountpoint.exists():
target_mountpoint.mkdir(parents=True, exist_ok=True)
if not target_mountpoint.exists():
raise ValueError('Target mountpoint does not exist')
lsblk_info = get_lsblk_info(dev_path)
if target_mountpoint in lsblk_info.mountpoints:
info(f'Device already mounted at {target_mountpoint}')
return
cmd = ['mount']
if len(options):
cmd.extend(('-o', ','.join(options)))
if mount_fs:
cmd.extend(('-t', mount_fs))
cmd.extend((str(dev_path), str(target_mountpoint)))
command = ' '.join(cmd)
debug(f'Mounting {dev_path}: {command}')
try:
SysCommand(command)
except SysCallError as err:
raise DiskError(f'Could not mount {dev_path}: {command}\n{err.message}')
def umount(mountpoint: Path, recursive: bool = False) -> None:
lsblk_info = get_lsblk_info(mountpoint)
@ -189,10 +126,3 @@ def umount(mountpoint: Path, recursive: bool = False) -> None:
for path in lsblk_info.mountpoints:
debug(f'Unmounting mountpoint: {path}')
SysCommand(cmd + [str(path)])
def swapon(path: Path) -> None:
try:
SysCommand(['swapon', str(path)])
except SysCallError as err:
raise DiskError(f'Could not enable swap {path}:\n{err.message}')

View File

@ -1,18 +1,101 @@
from __future__ import annotations
import json
import os
import re
import secrets
import shlex
import stat
import string
import subprocess
import sys
import time
from collections.abc import Iterator
from datetime import date, datetime
from enum import Enum
from pathlib import Path
from select import EPOLLHUP, EPOLLIN, epoll
from shutil import which
from types import TracebackType
from typing import Any, Self, override
from typing import Any, 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
from .exceptions import RequirementError, SysCallError
from .output import debug, error, logger
# https://stackoverflow.com/a/43627833/929999
_VT100_ESCAPE_REGEX = r'\x1B\[[?0-9;]*[a-zA-Z]'
_VT100_ESCAPE_REGEX_BYTES = _VT100_ESCAPE_REGEX.encode()
def generate_password(length: int = 64) -> str:
haystack = string.printable # digits, ascii_letters, punctuation (!"#$[] etc) and whitespace
return ''.join(secrets.choice(haystack) for _ in range(length))
def locate_binary(name: str) -> str:
if path := which(name):
return path
raise RequirementError(f'Binary {name} does not exist.')
def clear_vt100_escape_codes(data: bytes) -> bytes:
return re.sub(_VT100_ESCAPE_REGEX_BYTES, b'', data)
def clear_vt100_escape_codes_from_str(data: str) -> str:
return re.sub(_VT100_ESCAPE_REGEX, '', data)
def jsonify(obj: object, safe: bool = True) -> object:
"""
Converts objects into json.dumps() compatible nested dictionaries.
Setting safe to True skips dictionary keys starting with a bang (!)
"""
compatible_types = str, int, float, bool
if isinstance(obj, dict):
return {
key: jsonify(value, safe)
for key, value in obj.items()
if isinstance(key, compatible_types) and not (isinstance(key, str) and key.startswith('!') and safe)
}
if isinstance(obj, Enum):
return obj.value
if hasattr(obj, 'json'):
# json() is a friendly name for json-helper, it should return
# a dictionary representation of the object so that it can be
# processed by the json library.
return jsonify(obj.json(), safe)
if isinstance(obj, datetime | date):
return obj.isoformat()
if isinstance(obj, list | set | tuple):
return [jsonify(item, safe) for item in obj]
if isinstance(obj, Path):
return str(obj)
if hasattr(obj, '__dict__'):
return vars(obj)
return obj
class JSON(json.JSONEncoder, json.JSONDecoder):
"""
A safe JSON encoder that will omit private information in dicts (starting with !)
"""
@override
def encode(self, o: object) -> str:
return super().encode(jsonify(o))
class UNSAFE_JSON(json.JSONEncoder, json.JSONDecoder):
"""
UNSAFE_JSON will call/encode and keep private information in dicts (starting with !)
"""
@override
def encode(self, o: object) -> str:
return super().encode(jsonify(o, safe=False))
class SysCommandWorker:
@ -44,8 +127,8 @@ class SysCommandWorker:
self._trace_log_pos = 0
self.poll_object = epoll()
self.child_fd: int | None = None
self.started = False
self.ended = False
self.started: float | None = None
self.ended: float | None = None
self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines
def __contains__(self, key: bytes) -> bool:
@ -85,7 +168,7 @@ class SysCommandWorker:
except UnicodeDecodeError:
return str(self._trace_log)
def __enter__(self) -> Self:
def __enter__(self) -> 'SysCommandWorker':
return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
@ -117,7 +200,7 @@ class SysCommandWorker:
def is_alive(self) -> bool:
self.poll()
if self.started and not self.ended:
if self.started and self.ended is None:
return True
return False
@ -173,11 +256,11 @@ class SysCommandWorker:
self.peak(output)
self._trace_log += output
except OSError:
self.ended = True
self.ended = time.time()
break
if self.ended or (not got_output and not _pid_exists(self.pid)):
self.ended = True
self.ended = time.time()
try:
wait_status = os.waitpid(self.pid, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status)
@ -195,9 +278,9 @@ class SysCommandWorker:
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.
# 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()
@ -215,7 +298,7 @@ class SysCommandWorker:
# Only parent process moves back to the original working directory
os.chdir(old_dir)
self.started = True
self.started = time.time()
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
return True
@ -331,6 +414,31 @@ class SysCommand:
return None
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
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 run(
cmd: list[str],
input_data: bytes | None = None,
@ -346,39 +454,8 @@ def run(
)
def locate_binary(name: str) -> str:
if path := which(name):
return path
raise RequirementError(f'Binary {name} does not exist.')
def _pid_exists(pid: int) -> bool:
try:
return any(subprocess.check_output(['ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip())
except subprocess.CalledProcessError:
return False
def _cmd_history(cmd: list[str]) -> None:
content = f'{time.time()} {cmd}\n'
_append_log('cmd_history.txt', content)
def _cmd_output(output: str) -> None:
_append_log('cmd_output.txt', output)
def _append_log(file: str, content: str) -> None:
path = logger.directory / file
change_perm = not path.exists()
try:
with path.open('a') as f:
f.write(content)
if change_perm:
path.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
except PermissionError, FileNotFoundError:
# If the file does not exist, ignore the error
pass

View File

@ -1,17 +0,0 @@
from archinstall.lib.general.general_menu import (
select_archinstall_language,
select_hostname,
select_ntp,
select_timezone,
)
from archinstall.lib.general.system_menu import select_driver, select_kernel, select_swap
__all__ = [
'select_archinstall_language',
'select_driver',
'select_hostname',
'select_kernel',
'select_ntp',
'select_swap',
'select_timezone',
]

View File

@ -1,149 +0,0 @@
from enum import Enum
from archinstall.lib.locale.utils import list_timezones
from archinstall.lib.log import warn
from archinstall.lib.menu.helpers import Confirmation, Input, Selection
from archinstall.lib.translationhandler import Language, tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class PostInstallationAction(Enum):
EXIT = tr('Exit archinstall')
REBOOT = tr('Reboot system')
CHROOT = tr('chroot into installation for post-installation configurations')
async def select_ntp(preset: bool = True) -> bool:
header = tr('Would you like to use automatic time synchronization (NTP) with the default time servers?\n') + '\n'
header += (
tr(
'Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki',
)
+ '\n'
)
result = await Confirmation(
header=header,
allow_skip=True,
preset=preset,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.item() == MenuItem.yes()
case _:
raise ValueError('Unhandled return type')
async def select_hostname(preset: str | None = None) -> str | None:
result = await Input(
header=tr('Enter a hostname'),
allow_skip=True,
default_value=preset,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
hostname = result.get_value()
if len(hostname) < 1:
return None
return hostname
case ResultType.Reset:
raise ValueError('Unhandled result type')
async def select_timezone(preset: str | None = None) -> str | None:
default = 'UTC'
timezones = list_timezones()
items = [MenuItem(tz, value=tz) for tz in timezones]
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(preset)
group.set_default_by_value(default)
result = await Selection[str](
group,
header=tr('Select timezone'),
allow_reset=True,
allow_skip=True,
enable_filter=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return default
case ResultType.Selection:
return result.get_value()
async def select_language(preset: str | None = None) -> str | None:
from archinstall.lib.locale.locale_menu import select_kb_layout
# We'll raise an exception in an upcoming version.
# from ..exceptions import Deprecated
# raise Deprecated("select_language() has been deprecated, use select_kb_layout() instead.")
# No need to translate this i feel, as it's a short lived message.
warn('select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version')
return await select_kb_layout(preset)
async def select_archinstall_language(languages: list[Language], preset: Language) -> Language:
# these are the displayed language names which can either be
# the english name of a language or, if present, the
# name of the language in its own language
items = [MenuItem(lang.display_name, lang) for lang in languages]
group = MenuItemGroup(items, sort_items=True)
group.set_focus_by_value(preset)
title = 'NOTE: Console font will be set automatically for supported languages.\n'
title += 'For other languages, fonts can be found in "/usr/share/kbd/consolefonts"\n'
title += 'and set manually with: setfont <fontname>\n'
result = await Selection[Language](
header=title,
group=group,
allow_reset=False,
allow_skip=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
case ResultType.Reset:
raise ValueError('Language selection not handled')
async def select_post_installation(elapsed_time: float | None = None) -> PostInstallationAction:
header = 'Installation completed'
if elapsed_time is not None:
minutes = int(elapsed_time // 60)
seconds = int(elapsed_time % 60)
header += f' in {minutes}m{seconds}s' + '\n'
header += tr('What would you like to do next?') + '\n'
items = [MenuItem(action.value, value=action) for action in PostInstallationAction]
group = MenuItemGroup(items)
result = await Selection[PostInstallationAction](
group,
header=header,
allow_skip=False,
).show()
match result.type_:
case ResultType.Selection:
return result.get_value()
case _:
raise ValueError('Post installation action not handled')

View File

@ -1,149 +0,0 @@
from typing import assert_never
from archinstall.lib.hardware import GfxDriver, SysInfo
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.application import ZramAlgorithm, ZramConfiguration
from archinstall.lib.models.package_types import DEFAULT_KERNEL, Kernel
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
async def select_kernel(preset: list[Kernel] = []) -> list[Kernel]:
"""
Asks the user to select a kernel for system.
:return: The string as a selected kernel
:rtype: string
"""
group = MenuItemGroup.from_enum(Kernel, sort_items=True, preset=preset)
group.set_default_by_value(DEFAULT_KERNEL)
group.set_focus_by_value(DEFAULT_KERNEL)
result = await Selection[Kernel](
group,
header=tr('Select which kernel(s) to install'),
allow_skip=True,
allow_reset=True,
multi=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return []
case ResultType.Selection:
return result.get_values()
async def select_uki(preset: bool = True) -> 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.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
async def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None) -> GfxDriver | None:
"""
Somewhat convoluted function, whose job is simple.
Select a graphics driver from a pre-defined set of popular options.
(The template xorg is for beginner users, not advanced, and should
there for appeal to the general public first and edge cases later)
"""
if not options:
options = [driver for driver in GfxDriver]
items = [
MenuItem(
o.value,
value=o,
preview_action=lambda x: x.value.packages_text() if x.value else None,
)
for o in options
]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(GfxDriver.AllOpenSource)
if preset is not None:
group.set_focus_by_value(preset)
header = ''
if SysInfo.has_amd_graphics():
header += tr('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.') + '\n'
if SysInfo.has_intel_graphics():
header += tr('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n')
if SysInfo.has_nvidia_graphics():
header += tr('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')
result = await Selection[GfxDriver](
group,
header=header,
allow_skip=True,
allow_reset=True,
preview_location='right',
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return None
case ResultType.Selection:
return result.get_value()
async def select_swap(preset: ZramConfiguration = ZramConfiguration(enabled=True)) -> ZramConfiguration:
prompt = tr('Would you like to use swap on zram?') + '\n'
group = MenuItemGroup.yes_no()
group.set_default_by_value(True)
group.set_focus_by_value(preset.enabled)
result = await Confirmation(
header=prompt,
allow_skip=True,
preset=preset.enabled,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
enabled = result.item() == MenuItem.yes()
if not enabled:
return ZramConfiguration(enabled=False)
# Ask for compression algorithm
algo_group = MenuItemGroup.from_enum(ZramAlgorithm, sort_items=False)
algo_group.set_default_by_value(ZramAlgorithm.ZSTD)
algo_group.set_focus_by_value(preset.algorithm)
algo_result = await Selection[ZramAlgorithm](
algo_group,
header=tr('Select zram compression algorithm:') + '\n',
allow_skip=True,
).show()
match algo_result.type_:
case ResultType.Skip:
algo = preset.algorithm
case ResultType.Selection:
algo = algo_result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
case _:
assert_never(algo_result.type_)
return ZramConfiguration(enabled=True, algorithm=algo)
case ResultType.Reset:
raise ValueError('Unhandled result type')

View File

@ -1,76 +1,65 @@
from __future__ import annotations
from typing import override
from archinstall.default_profiles.profile import GreeterType
from archinstall.lib.applications.application_menu import ApplicationMenu
from archinstall.lib.args import ArchConfig
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.configuration import ConfigurationOutput, save_config
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone
from archinstall.lib.general.system_menu import select_kernel, select_swap
from archinstall.lib.hardware import SysInfo
from archinstall.lib.locale.locale_menu import LocaleMenu
from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.mirror.mirror_menu import MirrorMenu
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.application import ApplicationConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, PartitionModification
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration
from archinstall.lib.models.network import NetworkConfiguration, NicType
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.network.network_menu import select_network
from archinstall.lib.packages.packages import list_available_packages, select_additional_packages
from archinstall.lib.pacman.config import PacmanConfig
from archinstall.lib.pacman.pacman_menu import PacmanMenu
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_table
from archinstall.tui.components import tui
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, EncryptionType, FilesystemType, PartitionModification
from archinstall.lib.packages import list_available_packages
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from .applications.application_menu import ApplicationMenu
from .args import ArchConfig
from .authentication.authentication_menu import AuthenticationMenu
from .configuration import save_config
from .hardware import SysInfo
from .interactions.general_conf import (
add_number_of_parallel_downloads,
ask_additional_packages_to_install,
ask_for_a_timezone,
ask_hostname,
ask_ntp,
)
from .interactions.network_menu import ask_to_configure_network
from .interactions.system_conf import ask_for_bootloader, ask_for_swap, ask_for_uki, select_kernel
from .locale.locale_menu import LocaleMenu
from .menu.abstract_menu import CONFIG_KEY, AbstractMenu
from .mirrors import MirrorMenu
from .models.bootloader import Bootloader
from .models.locale import LocaleConfiguration
from .models.mirrors import MirrorConfiguration
from .models.network import NetworkConfiguration, NicType
from .models.packages import Repository
from .models.profile import ProfileConfiguration
from .output import FormattedOutput
from .pacman.config import PacmanConfig
from .translationhandler import Language, tr, translation_handler
class GlobalMenu(AbstractMenu[None]):
def __init__(
self,
arch_config: ArchConfig,
mirror_list_handler: MirrorListHandler | None = None,
skip_boot: bool = False,
advanced: bool = False,
title: str | None = None,
) -> None:
def __init__(self, arch_config: ArchConfig) -> None:
self._arch_config = arch_config
self._mirror_list_handler = mirror_list_handler
self._skip_boot = skip_boot
self._advanced = advanced
self._uefi = SysInfo.has_uefi()
menu_options = self._get_menu_options()
menu_optioons = self._get_menu_options()
self._item_group = MenuItemGroup(
menu_options,
menu_optioons,
sort_items=False,
checkmarks=True,
)
super().__init__(self._item_group, config=arch_config, title=title)
super().__init__(self._item_group, config=arch_config)
def _get_menu_options(self) -> list[MenuItem]:
menu_options = [
MenuItem(
text=tr('Archinstall language'),
action=self._select_archinstall_language,
preview_action=self._prev_archinstall_language,
display_action=lambda x: x.display_name if x else '',
key='archinstall_language',
),
MenuItem(
text=tr('Locales'),
value=LocaleConfiguration.default(),
action=self._locale_selection,
preview_action=self._prev_locale,
key='locale_config',
@ -90,30 +79,31 @@ class GlobalMenu(AbstractMenu[None]):
),
MenuItem(
text=tr('Swap'),
value=ZramConfiguration(enabled=True),
action=select_swap,
value=True,
action=ask_for_swap,
preview_action=self._prev_swap,
key='swap',
),
MenuItem(
text=tr('Bootloader'),
value=BootloaderConfiguration.get_default(self._uefi, self._skip_boot),
action=self._select_bootloader_config,
preview_action=self._prev_bootloader_config,
key='bootloader_config',
value=Bootloader.get_default(),
action=self._select_bootloader,
preview_action=self._prev_bootloader,
mandatory=True,
key='bootloader',
),
MenuItem(
text=tr('Kernels'),
value=[DEFAULT_KERNEL],
action=select_kernel,
preview_action=self._prev_kernel,
mandatory=True,
key='kernels',
text=tr('Unified kernel images'),
value=False,
enabled=SysInfo.has_uefi(),
action=ask_for_uki,
preview_action=self._prev_uki,
key='uki',
),
MenuItem(
text=tr('Hostname'),
value='archlinux',
action=select_hostname,
action=ask_hostname,
preview_action=self._prev_hostname,
key='hostname',
),
@ -136,19 +126,27 @@ class GlobalMenu(AbstractMenu[None]):
preview_action=self._prev_applications,
key='app_config',
),
MenuItem(
text=tr('Kernels'),
value=['linux'],
action=select_kernel,
preview_action=self._prev_kernel,
mandatory=True,
key='kernels',
),
MenuItem(
text=tr('Network configuration'),
action=select_network,
action=ask_to_configure_network,
value={},
preview_action=self._prev_network_config,
key='network_config',
),
MenuItem(
text=tr('Pacman'),
action=self._pacman_configuration,
value=PacmanConfiguration.default(),
preview_action=self._prev_pacman_config,
key='pacman_config',
text=tr('Parallel Downloads'),
action=add_number_of_parallel_downloads,
value=0,
preview_action=self._prev_parallel_dw,
key='parallel_downloads',
),
MenuItem(
text=tr('Additional packages'),
@ -159,48 +157,48 @@ class GlobalMenu(AbstractMenu[None]):
),
MenuItem(
text=tr('Timezone'),
action=select_timezone,
action=ask_for_a_timezone,
value='UTC',
preview_action=self._prev_tz,
key='timezone',
),
MenuItem(
text=tr('Automatic time sync (NTP)'),
action=select_ntp,
action=ask_ntp,
value=True,
preview_action=self._prev_ntp,
key='ntp',
),
MenuItem(
text='',
read_only=True,
),
MenuItem(
text=tr('Save configuration'),
action=lambda x: self._safe_config(),
key=SpecialMenuKey.SAVE.value,
key=f'{CONFIG_KEY}_save',
),
MenuItem(
text=tr('Install'),
preview_action=self._prev_install_invalid_config,
key=SpecialMenuKey.INSTALL.value,
key=f'{CONFIG_KEY}_install',
),
MenuItem(
text=tr('Abort'),
key=SpecialMenuKey.ABORT.value,
action=lambda x: exit(1),
key=f'{CONFIG_KEY}_abort',
),
]
return menu_options
async def _safe_config(self) -> None:
def _safe_config(self) -> None:
# data: dict[str, Any] = {}
# for item in self._item_group.items:
# if item.key is not None:
# data[item.key] = item.value
# if item.key is not None:
# data[item.key] = item.value
self.sync_all_to_config()
await save_config(self._arch_config)
save_config(self._arch_config)
def _missing_configs(self) -> list[str]:
item: MenuItem = self._item_group.find_by_key('auth_config')
@ -210,25 +208,18 @@ class GlobalMenu(AbstractMenu[None]):
item = self._item_group.find_by_key(s)
return item.has_value()
def has_superuser() -> bool:
if auth_config and auth_config.users:
return any([u.sudo for u in auth_config.users])
return False
missing = set()
if (auth_config is None or auth_config.root_enc_password is None) and not (auth_config and auth_config.has_superuser()):
if (auth_config is None or auth_config.root_enc_password is None) and not has_superuser():
missing.add(
tr('Either root-password or at least 1 user with sudo privileges must be specified'),
)
# These greeters only show users with UID >= 1000 and have no manual login by default
if not (auth_config and auth_config.has_regular_user()):
profile_item: MenuItem = self._item_group.find_by_key('profile_config')
profile_config: ProfileConfiguration | None = profile_item.value
if profile_config and profile_config.profile and profile_config.profile.is_desktop_profile():
problematic_greeters = {GreeterType.Sddm}
if any(p.default_greeter_type in problematic_greeters for p in profile_config.profile.current_selection):
missing.add(
tr('The selected desktop profile requires a regular user to log in via the greeter'),
)
for item in self._item_group.items:
if item.mandatory:
assert item.key is not None
@ -238,7 +229,7 @@ class GlobalMenu(AbstractMenu[None]):
return list(missing)
@override
def is_config_valid(self) -> bool:
def _is_config_valid(self) -> bool:
"""
Checks the validity of the current configuration.
"""
@ -246,29 +237,22 @@ class GlobalMenu(AbstractMenu[None]):
return False
return self._validate_bootloader() is None
async def _select_archinstall_language(self, preset: Language) -> Language:
from archinstall.lib.general.general_menu import select_archinstall_language
def _select_archinstall_language(self, preset: Language) -> Language:
from .interactions.general_conf import select_archinstall_language
language = await select_archinstall_language(translation_handler.translated_languages, preset)
language = select_archinstall_language(translation_handler.translated_languages, preset)
translation_handler.activate(language)
self._update_lang_text()
return language
def _prev_archinstall_language(self, item: MenuItem) -> str | None:
if not item.value:
return None
lang: Language = item.value
return f'{tr("Language")}: {lang.display_name}'
async def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None:
app_config = await ApplicationMenu(preset).show()
def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None:
app_config = ApplicationMenu(preset).run()
return app_config
async def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None:
auth_config = await AuthenticationMenu(preset).show()
def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None:
auth_config = AuthenticationMenu(preset).run()
return auth_config
def _update_lang_text(self) -> None:
@ -282,10 +266,8 @@ class GlobalMenu(AbstractMenu[None]):
if o.key is not None:
self._item_group.find_by_key(o.key).text = o.text
tui.translate_bindings()
async def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration | None:
locale_config = await LocaleMenu(preset).show()
def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration:
locale_config = LocaleMenu(preset).run()
return locale_config
def _prev_locale(self, item: MenuItem) -> str | None:
@ -299,7 +281,7 @@ class GlobalMenu(AbstractMenu[None]):
if item.value:
network_config: NetworkConfiguration = item.value
if network_config.type == NicType.MANUAL:
output = as_table(network_config.nics)
output = FormattedOutput.as_table(network_config.nics)
else:
output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}'
@ -321,7 +303,7 @@ class GlobalMenu(AbstractMenu[None]):
output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'
if auth_config.users:
output += as_table(auth_config.users) + '\n'
output += FormattedOutput.as_table(auth_config.users) + '\n'
if auth_config.u2f_config:
u2f_config = auth_config.u2f_config
@ -350,21 +332,6 @@ class GlobalMenu(AbstractMenu[None]):
output += f'{tr("Audio")}: {audio_config.audio.value}'
output += '\n'
if app_config.print_service_config:
output += f'{tr("Print service")}: '
output += tr('Enabled') if app_config.print_service_config.enabled else tr('Disabled')
output += '\n'
if app_config.power_management_config:
power_management_config = app_config.power_management_config
output += f'{tr("Power management")}: {power_management_config.power_management.value}'
output += '\n'
if app_config.firewall_config:
firewall_config = app_config.firewall_config
output += f'{tr("Firewall")}: {firewall_config.firewall.value}'
output += '\n'
return output
return None
@ -394,7 +361,7 @@ class GlobalMenu(AbstractMenu[None]):
output += '{}: {}'.format(tr('LVM configuration type'), disk_layout_conf.lvm_config.config_type.display_msg()) + '\n'
if disk_layout_conf.disk_encryption:
output += tr('Disk encryption') + ': ' + disk_layout_conf.disk_encryption.encryption_type.type_to_text() + '\n'
output += tr('Disk encryption') + ': ' + EncryptionType.type_to_text(disk_layout_conf.disk_encryption.encryption_type) + '\n'
if disk_layout_conf.btrfs_options:
btrfs_options = disk_layout_conf.btrfs_options
@ -408,9 +375,14 @@ class GlobalMenu(AbstractMenu[None]):
def _prev_swap(self, item: MenuItem) -> str | None:
if item.value is not None:
output = f'{tr("Swap on zram")}: '
output += tr('Enabled') if item.value.enabled else tr('Disabled')
if item.value.enabled:
output += f'\n{tr("Compression algorithm")}: {item.value.algorithm.value}'
output += tr('Enabled') if item.value else tr('Disabled')
return output
return None
def _prev_uki(self, item: MenuItem) -> str | None:
if item.value is not None:
output = f'{tr("Unified kernel images")}: '
output += tr('Enabled') if item.value else tr('Disabled')
return output
return None
@ -419,18 +391,10 @@ class GlobalMenu(AbstractMenu[None]):
return f'{tr("Hostname")}: {item.value}'
return None
async def _pacman_configuration(self, preset: PacmanConfiguration) -> PacmanConfiguration | None:
return await PacmanMenu(preset, advanced=self._advanced).show()
def _prev_pacman_config(self, item: MenuItem) -> str | None:
if not item.value:
return None
config: PacmanConfiguration = item.value
output = ''
if self._advanced:
output += '{}: {}\n'.format(tr('Parallel Downloads'), config.parallel_downloads)
output += '{}: {}'.format(tr('Color'), config.color)
return output
def _prev_parallel_dw(self, item: MenuItem) -> str | None:
if item.value is not None:
return f'{tr("Parallel Downloads")}: {item.value}'
return None
def _prev_kernel(self, item: MenuItem) -> str | None:
if item.value:
@ -438,10 +402,9 @@ class GlobalMenu(AbstractMenu[None]):
return f'{tr("Kernel")}: {kernel}'
return None
def _prev_bootloader_config(self, item: MenuItem) -> str | None:
bootloader_config: BootloaderConfiguration | None = item.value
if bootloader_config:
return bootloader_config.preview(self._uefi)
def _prev_bootloader(self, item: MenuItem) -> str | None:
if item.value is not None:
return f'{tr("Bootloader")}: {item.value.value}'
return None
def _validate_bootloader(self) -> str | None:
@ -451,15 +414,18 @@ class GlobalMenu(AbstractMenu[None]):
Returns [`None`] if the bootloader is valid, otherwise returns a
string with the error message.
XXX: The caller is responsible for wrapping the string with the translation
shim if necessary.
"""
bootloader_config: BootloaderConfiguration | None = None
bootloader: Bootloader | None = None
root_partition: PartitionModification | None = None
boot_partition: PartitionModification | None = None
efi_partition: PartitionModification | None = None
bootloader_config = self._item_group.find_by_key('bootloader_config').value
bootloader = self._item_group.find_by_key('bootloader').value
if not bootloader_config or bootloader_config.bootloader == Bootloader.NO_BOOTLOADER:
if bootloader == Bootloader.NO_BOOTLOADER:
return None
if disk_config := self._item_group.find_by_key('disk_config').value:
@ -469,7 +435,7 @@ class GlobalMenu(AbstractMenu[None]):
for layout in disk_config.device_modifications:
if boot_partition := layout.get_boot_partition():
break
if self._uefi:
if SysInfo.has_uefi():
for layout in disk_config.device_modifications:
if efi_partition := layout.get_efi_partition():
break
@ -482,15 +448,16 @@ class GlobalMenu(AbstractMenu[None]):
if boot_partition is None:
return 'Boot partition not found'
if self._uefi:
if SysInfo.has_uefi():
if efi_partition is None:
return 'EFI system partition (ESP) not found'
if efi_partition.fs_type is None or not efi_partition.fs_type.is_fat():
if efi_partition.fs_type not in [FilesystemType.Fat12, FilesystemType.Fat16, FilesystemType.Fat32]:
return 'ESP must be formatted as a FAT filesystem'
if failure := validate_bootloader_layout(bootloader_config, disk_config):
return failure.description
if bootloader == Bootloader.Limine:
if boot_partition.fs_type not in [FilesystemType.Fat12, FilesystemType.Fat16, FilesystemType.Fat32]:
return 'Limine does not support booting with a non-FAT boot partition'
return None
@ -502,13 +469,9 @@ class GlobalMenu(AbstractMenu[None]):
return text[:-1] # remove last new line
if error := self._validate_bootloader():
return tr('Invalid configuration: {}').format(error)
return tr(f'Invalid configuration: {error}')
self.sync_all_to_config()
summary = ConfigurationOutput(self._arch_config).as_summary()
if summary:
return f'{tr("Ready to install")}\n\n{summary}'
return tr('Ready to install')
return None
def _prev_profile(self, item: MenuItem) -> str | None:
profile_config: ProfileConfiguration | None = item.value
@ -530,51 +493,51 @@ class GlobalMenu(AbstractMenu[None]):
return None
async def _select_disk_config(
def _select_disk_config(
self,
preset: DiskLayoutConfiguration | None = None,
) -> DiskLayoutConfiguration | None:
disk_config = await DiskLayoutConfigurationMenu(preset).show()
disk_config = DiskLayoutConfigurationMenu(preset).run()
return disk_config
async def _select_bootloader_config(
self,
preset: BootloaderConfiguration | None = None,
) -> BootloaderConfiguration | None:
if preset is None:
preset = BootloaderConfiguration.get_default(self._uefi, self._skip_boot)
def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None:
bootloader = ask_for_bootloader(preset)
bootloader_config = await BootloaderMenu(preset, self._uefi, self._skip_boot).show()
if bootloader:
uki = self._item_group.find_by_key('uki')
if not SysInfo.has_uefi() or not bootloader.has_uki_support():
uki.value = False
uki.enabled = False
else:
uki.enabled = True
return bootloader_config
return bootloader
async def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None:
from archinstall.lib.profile.profile_menu import ProfileMenu
def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None:
from .profile.profile_menu import ProfileMenu
profile_config = await ProfileMenu(preset=current_profile).show()
profile_config = ProfileMenu(preset=current_profile).run()
return profile_config
async def _select_additional_packages(self, preset: list[str]) -> list[str]:
def _select_additional_packages(self, preset: list[str]) -> list[str]:
config: MirrorConfiguration | None = self._item_group.find_by_key('mirror_config').value
repositories: set[Repository] = set()
if config:
repositories = set(config.optional_repositories)
packages = await select_additional_packages(
packages = ask_additional_packages_to_install(
preset,
repositories=repositories,
)
return packages
async def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration | None:
if self._mirror_list_handler is None:
self._mirror_list_handler = MirrorListHandler()
def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration:
mirror_configuration = MirrorMenu(preset=preset).run()
mirror_configuration = await MirrorMenu(self._mirror_list_handler, preset=preset).run()
if mirror_configuration and mirror_configuration.optional_repositories:
if mirror_configuration.optional_repositories:
# reset the package list cache in case the repository selection has changed
list_available_packages.cache_clear()
@ -607,12 +570,12 @@ class GlobalMenu(AbstractMenu[None]):
if mirror_config.optional_repositories:
title = tr('Optional repositories')
divider = '-' * len(title)
repos = ', '.join(r.value for r in mirror_config.optional_repositories)
repos = ', '.join([r.value for r in mirror_config.optional_repositories])
output += f'{title}\n{divider}\n{repos}\n\n'
if mirror_config.custom_repositories:
title = tr('Custom repositories')
table = as_table(mirror_config.custom_repositories)
table = FormattedOutput.as_table(mirror_config.custom_repositories)
output += f'{title}:\n\n{table}'
return output.strip()

View File

@ -2,13 +2,12 @@ import os
from enum import Enum
from functools import cached_property
from pathlib import Path
from typing import Self
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.networking import enrich_iface_types, list_interfaces
from archinstall.lib.translationhandler import tr
from .exceptions import SysCallError
from .general import SysCommand
from .networking import enrich_iface_types, list_interfaces
from .output import debug
from .translationhandler import tr
class CpuVendor(Enum):
@ -17,7 +16,7 @@ class CpuVendor(Enum):
_Unknown = 'unknown'
@classmethod
def get_vendor(cls, name: str) -> Self:
def get_vendor(cls, name: str) -> 'CpuVendor':
if vendor := getattr(cls, name, None):
return vendor
else:
@ -41,9 +40,10 @@ class GfxPackage(Enum):
Dkms = 'dkms'
IntelMediaDriver = 'intel-media-driver'
LibvaIntelDriver = 'libva-intel-driver'
LibvaMesaDriver = 'libva-mesa-driver'
LibvaNvidiaDriver = 'libva-nvidia-driver'
Mesa = 'mesa'
NvidiaOpen = 'nvidia-open'
NvidiaDkms = 'nvidia-dkms'
NvidiaOpenDkms = 'nvidia-open-dkms'
VulkanIntel = 'vulkan-intel'
VulkanRadeon = 'vulkan-radeon'
@ -51,6 +51,8 @@ class GfxPackage(Enum):
Xf86VideoAmdgpu = 'xf86-video-amdgpu'
Xf86VideoAti = 'xf86-video-ati'
Xf86VideoNouveau = 'xf86-video-nouveau'
XorgServer = 'xorg-server'
XorgXinit = 'xorg-xinit'
class GfxDriver(Enum):
@ -59,24 +61,12 @@ class GfxDriver(Enum):
IntelOpenSource = 'Intel (open-source)'
NvidiaOpenKernel = 'Nvidia (open kernel module for newer GPUs, Turing+)'
NvidiaOpenSource = 'Nvidia (open-source nouveau driver)'
NvidiaProprietary = 'Nvidia (proprietary)'
VMOpenSource = 'VirtualBox (open-source)'
def is_nvidia(self) -> bool:
match self:
case GfxDriver.NvidiaOpenSource | GfxDriver.NvidiaOpenKernel:
return True
case _:
return False
def is_nvidia_proprietary(self) -> bool:
"""
True for Nvidia drivers that ship proprietary userspace components.
Currently only NvidiaOpenKernel (nvidia-open-dkms): open kernel module
paired with proprietary userspace. NvidiaOpenSource (nouveau) is fully
open and works with Sway, so it is excluded.
"""
match self:
case GfxDriver.NvidiaOpenKernel:
case GfxDriver.NvidiaProprietary | GfxDriver.NvidiaOpenSource | GfxDriver.NvidiaOpenKernel:
return True
case _:
return False
@ -86,12 +76,12 @@ class GfxDriver(Enum):
text = tr('Installed packages') + ':\n'
for p in sorted(pkg_names):
text += f' - {p}\n'
text += f'\t- {p}\n'
return text
def gfx_packages(self) -> list[GfxPackage]:
packages: list[GfxPackage] = []
packages = [GfxPackage.XorgServer, GfxPackage.XorgXinit]
match self:
case GfxDriver.AllOpenSource:
@ -100,6 +90,7 @@ class GfxDriver(Enum):
GfxPackage.Xf86VideoAmdgpu,
GfxPackage.Xf86VideoAti,
GfxPackage.Xf86VideoNouveau,
GfxPackage.LibvaMesaDriver,
GfxPackage.LibvaIntelDriver,
GfxPackage.IntelMediaDriver,
GfxPackage.VulkanRadeon,
@ -111,6 +102,7 @@ class GfxDriver(Enum):
GfxPackage.Mesa,
GfxPackage.Xf86VideoAmdgpu,
GfxPackage.Xf86VideoAti,
GfxPackage.LibvaMesaDriver,
GfxPackage.VulkanRadeon,
]
case GfxDriver.IntelOpenSource:
@ -130,8 +122,15 @@ class GfxDriver(Enum):
packages += [
GfxPackage.Mesa,
GfxPackage.Xf86VideoNouveau,
GfxPackage.LibvaMesaDriver,
GfxPackage.VulkanNouveau,
]
case GfxDriver.NvidiaProprietary:
packages += [
GfxPackage.NvidiaDkms,
GfxPackage.Dkms,
GfxPackage.LibvaNvidiaDriver,
]
case GfxDriver.VMOpenSource:
packages += [
GfxPackage.Mesa,
@ -144,18 +143,6 @@ class _SysInfo:
def __init__(self) -> None:
pass
@cached_property
def has_battery(self) -> bool:
for type_path in Path('/sys/class/power_supply/').glob('*/type'):
try:
with open(type_path) as f:
if f.read().strip() == 'Battery':
return True
except OSError:
continue
return False
@cached_property
def cpu_info(self) -> dict[str, str]:
"""
@ -206,27 +193,11 @@ class _SysInfo:
return modules
@cached_property
def graphics_devices(self) -> dict[str, str]:
"""
Returns detected graphics devices (cached)
"""
cards: dict[str, str] = {}
for line in SysCommand('lspci'):
if b' VGA ' in line or b' 3D ' in line:
_, identifier = line.split(b': ', 1)
cards[identifier.strip().decode('UTF-8')] = str(line)
return cards
_sys_info = _SysInfo()
class SysInfo:
@staticmethod
def has_battery() -> bool:
return _sys_info.has_battery
@staticmethod
def has_wifi() -> bool:
ifaces = list(list_interfaces().values())
@ -238,19 +209,24 @@ class SysInfo:
@staticmethod
def _graphics_devices() -> dict[str, str]:
return _sys_info.graphics_devices
cards: dict[str, str] = {}
for line in SysCommand('lspci'):
if b' VGA ' in line or b' 3D ' in line:
_, identifier = line.split(b': ', 1)
cards[identifier.strip().decode('UTF-8')] = str(line)
return cards
@staticmethod
def has_nvidia_graphics() -> bool:
return any('nvidia' in x.lower() for x in _sys_info.graphics_devices)
return any('nvidia' in x.lower() for x in SysInfo._graphics_devices())
@staticmethod
def has_amd_graphics() -> bool:
return any('amd' in x.lower() for x in _sys_info.graphics_devices)
return any('amd' in x.lower() for x in SysInfo._graphics_devices())
@staticmethod
def has_intel_graphics() -> bool:
return any('intel' in x.lower() for x in _sys_info.graphics_devices)
return any('intel' in x.lower() for x in SysInfo._graphics_devices())
@staticmethod
def cpu_vendor() -> CpuVendor | None:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
from .disk_conf import (
get_default_partition_layout,
select_devices,
select_disk_config,
select_main_filesystem_format,
suggest_multi_disk_layout,
suggest_single_disk_layout,
)
from .general_conf import (
add_number_of_parallel_downloads,
ask_additional_packages_to_install,
ask_for_a_timezone,
ask_hostname,
ask_ntp,
select_archinstall_language,
)
from .manage_users_conf import UserList, ask_for_additional_users
from .network_menu import ManualNetworkConfig, ask_to_configure_network
from .system_conf import ask_for_bootloader, ask_for_swap, ask_for_uki, select_driver, select_kernel
__all__ = [
'ManualNetworkConfig',
'UserList',
'add_number_of_parallel_downloads',
'ask_additional_packages_to_install',
'ask_for_a_timezone',
'ask_for_additional_users',
'ask_for_bootloader',
'ask_for_swap',
'ask_for_uki',
'ask_hostname',
'ask_ntp',
'ask_to_configure_network',
'get_default_partition_layout',
'select_archinstall_language',
'select_devices',
'select_disk_config',
'select_driver',
'select_kernel',
'select_main_filesystem_format',
'suggest_multi_disk_layout',
'suggest_single_disk_layout',
]

View File

@ -0,0 +1,633 @@
from pathlib import Path
from archinstall.lib.args import arch_config_handler
from archinstall.lib.disk.device_handler import device_handler
from archinstall.lib.disk.partitioning_menu import manual_partitioning
from archinstall.lib.menu.menu_helper import MenuHelper
from archinstall.lib.models.device import (
BDevice,
BtrfsMountOption,
DeviceModification,
DiskLayoutConfiguration,
DiskLayoutType,
FilesystemType,
LvmConfiguration,
LvmLayoutType,
LvmVolume,
LvmVolumeGroup,
LvmVolumeStatus,
ModificationStatus,
PartitionFlag,
PartitionModification,
PartitionType,
SectorSize,
Size,
SubvolumeModification,
Unit,
_DeviceInfo,
)
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
from ..output import FormattedOutput
from ..utils.util import prompt_dir
def select_devices(preset: list[BDevice] | None = []) -> list[BDevice]:
def _preview_device_selection(item: MenuItem) -> str | None:
device = item.get_value()
dev = device_handler.get_device(device.path)
if dev and dev.partition_infos:
return FormattedOutput.as_table(dev.partition_infos)
return None
if preset is None:
preset = []
devices = device_handler.devices
options = [d.device_info for d in devices]
presets = [p.device_info for p in preset]
group = MenuHelper(options).create_menu_group()
group.set_selected_by_value(presets)
group.set_preview_for_all(_preview_device_selection)
result = SelectMenu[_DeviceInfo](
group,
alignment=Alignment.CENTER,
search_enabled=False,
multi=True,
preview_style=PreviewStyle.BOTTOM,
preview_size='auto',
preview_frame=FrameProperties.max('Partitions'),
allow_skip=True,
).run()
match result.type_:
case ResultType.Reset:
return []
case ResultType.Skip:
return preset
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
def get_default_partition_layout(
devices: list[BDevice],
filesystem_type: FilesystemType | None = None,
) -> list[DeviceModification]:
if len(devices) == 1:
device_modification = suggest_single_disk_layout(
devices[0],
filesystem_type=filesystem_type,
)
return [device_modification]
else:
return suggest_multi_disk_layout(
devices,
filesystem_type=filesystem_type,
)
def _manual_partitioning(
preset: list[DeviceModification],
devices: list[BDevice],
) -> list[DeviceModification]:
modifications = []
for device in devices:
mod = next(filter(lambda x: x.device == device, preset), None)
if not mod:
mod = DeviceModification(device, wipe=False)
if device_mod := manual_partitioning(mod, device_handler.partition_table):
modifications.append(device_mod)
return modifications
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 = SelectMenu[str](
group,
allow_skip=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Disk configuration type')),
allow_reset=True,
).run()
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 = 'You will use whatever drive-setup is mounted at the specified directory\n'
output += "WARNING: Archinstall won't check the suitability of this setup\n"
path = prompt_dir(tr('Root mount directory'), 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 = select_devices(preset_devices)
if not devices:
return None
if result.get_value() == default_layout:
modifications = 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 []
modifications = _manual_partitioning(preset_mods, devices)
if modifications:
return DiskLayoutConfiguration(
config_type=DiskLayoutType.Manual,
device_modifications=modifications,
)
return None
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 = SelectMenu[str](
group,
allow_reset=True,
allow_skip=True,
frame=FrameProperties.min(tr('LVM configuration type')),
alignment=Alignment.CENTER,
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return None
case ResultType.Selection:
if result.get_value() == default_mode:
return 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,
)
def select_main_filesystem_format() -> FilesystemType:
items = [
MenuItem('btrfs', value=FilesystemType.Btrfs),
MenuItem('ext4', value=FilesystemType.Ext4),
MenuItem('xfs', value=FilesystemType.Xfs),
MenuItem('f2fs', value=FilesystemType.F2fs),
]
if arch_config_handler.args.advanced:
items.append(MenuItem('ntfs', value=FilesystemType.Ntfs))
group = MenuItemGroup(items, sort_items=False)
result = SelectMenu[FilesystemType](
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min('Filesystem'),
allow_skip=False,
).run()
match result.type_:
case ResultType.Selection:
return result.get_value()
case _:
raise ValueError('Unhandled result type')
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 = SelectMenu[str](
group,
header=prompt,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
search_enabled=False,
allow_skip=True,
).run()
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')),
]
def suggest_single_disk_layout(
device: BDevice,
filesystem_type: FilesystemType | None = None,
separate_home: bool | None = None,
) -> DeviceModification:
if not filesystem_type:
filesystem_type = 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'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(MenuItem.yes().value)
result = SelectMenu[bool](
group,
header=prompt,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
allow_skip=False,
).run()
using_subvolumes = result.item() == MenuItem.yes()
mount_options = 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'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(MenuItem.yes().value)
result = SelectMenu(
group,
header=prompt,
orientation=Orientation.HORIZONTAL,
columns=2,
alignment=Alignment.CENTER,
allow_skip=False,
).run()
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
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 = 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))
items = [MenuItem(tr('Continue'))]
group = MenuItemGroup(items)
SelectMenu(group).run()
return []
if filesystem_type == FilesystemType.Btrfs:
mount_options = 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]
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 = select_main_filesystem_format()
if filesystem_type == FilesystemType.Btrfs:
prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(MenuItem.yes().value)
result = SelectMenu[bool](
group,
header=prompt,
search_enabled=False,
allow_skip=False,
orientation=Orientation.HORIZONTAL,
columns=2,
alignment=Alignment.CENTER,
).run()
using_subvolumes = MenuItem.yes() == result.item()
mount_options = 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 = Size(20, Unit.GiB, 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=LvmVolumeStatus.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=LvmVolumeStatus.Create,
name='home',
fs_type=filesystem_type,
length=home_vol_size,
mountpoint=Path('/home'),
)
lvm_vol_group.volumes.append(home_vol)
return LvmConfiguration(LvmLayoutType.Default, [lvm_vol_group])

View File

@ -0,0 +1,305 @@
from __future__ import annotations
from enum import Enum
from pathlib import Path
from typing import assert_never
from archinstall.lib.models.packages import Repository
from archinstall.lib.packages.packages import list_available_packages
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import EditMenu, SelectMenu, Tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
from ..locale.utils import list_timezones
from ..models.packages import AvailablePackage, PackageGroup
from ..output import warn
from ..translationhandler import Language
class PostInstallationAction(Enum):
EXIT = tr('Exit archinstall')
REBOOT = tr('Reboot system')
CHROOT = tr('chroot into installation for post-installation configurations')
def ask_ntp(preset: bool = True) -> bool:
header = tr('Would you like to use automatic time synchronization (NTP) with the default time servers?\n') + '\n'
header += (
tr(
'Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki',
)
+ '\n'
)
preset_val = MenuItem.yes() if preset else MenuItem.no()
group = MenuItemGroup.yes_no()
group.focus_item = preset_val
result = SelectMenu[bool](
group,
header=header,
allow_skip=True,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.item() == MenuItem.yes()
case _:
raise ValueError('Unhandled return type')
def ask_hostname(preset: str | None = None) -> str | None:
result = EditMenu(
tr('Hostname'),
alignment=Alignment.CENTER,
allow_skip=True,
default_text=preset,
).input()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
hostname = result.text()
if len(hostname) < 1:
return None
return hostname
case ResultType.Reset:
raise ValueError('Unhandled result type')
def ask_for_a_timezone(preset: str | None = None) -> str | None:
default = 'UTC'
timezones = list_timezones()
items = [MenuItem(tz, value=tz) for tz in timezones]
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(preset)
group.set_default_by_value(default)
result = SelectMenu[str](
group,
allow_reset=True,
allow_skip=True,
frame=FrameProperties.min(tr('Timezone')),
alignment=Alignment.CENTER,
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return default
case ResultType.Selection:
return result.get_value()
def select_language(preset: str | None = None) -> str | None:
from ..locale.locale_menu import select_kb_layout
# We'll raise an exception in an upcoming version.
# from ..exceptions import Deprecated
# raise Deprecated("select_language() has been deprecated, use select_kb_layout() instead.")
# No need to translate this i feel, as it's a short lived message.
warn('select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version')
return select_kb_layout(preset)
def select_archinstall_language(languages: list[Language], preset: Language) -> Language:
# these are the displayed language names which can either be
# the english name of a language or, if present, the
# name of the language in its own language
items = [MenuItem(lang.display_name, lang) for lang in languages]
group = MenuItemGroup(items, sort_items=True)
group.set_focus_by_value(preset)
title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n'
title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n'
title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n'
result = SelectMenu[Language](
group,
header=title,
allow_skip=True,
allow_reset=False,
alignment=Alignment.CENTER,
frame=FrameProperties.min(header=tr('Select language')),
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
case ResultType.Reset:
raise ValueError('Language selection not handled')
def ask_additional_packages_to_install(
preset: list[str] = [],
repositories: set[Repository] = set(),
) -> list[str]:
repositories |= {Repository.Core, Repository.Extra}
respos_text = ', '.join([r.value for r in repositories])
output = tr('Repositories: {}').format(respos_text) + '\n'
output += tr('Loading packages...')
Tui.print(output, clear_screen=True)
packages = list_available_packages(tuple(repositories))
package_groups = PackageGroup.from_available_packages(packages)
# Additional packages (with some light weight error handling for invalid package names)
header = tr('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.') + '\n'
header += tr('Select any packages from the below list that should be installed additionally') + '\n'
# there are over 15k packages so this needs to be quick
preset_packages: list[AvailablePackage | PackageGroup] = []
for p in preset:
if p in packages:
preset_packages.append(packages[p])
elif p in package_groups:
preset_packages.append(package_groups[p])
items = [
MenuItem(
name,
value=pkg,
preview_action=lambda x: x.value.info(),
)
for name, pkg in packages.items()
]
items += [
MenuItem(
name,
value=group,
preview_action=lambda x: x.value.info(),
)
for name, group in package_groups.items()
]
menu_group = MenuItemGroup(items, sort_items=True)
menu_group.set_selected_by_value(preset_packages)
result = SelectMenu[AvailablePackage | PackageGroup](
menu_group,
header=header,
alignment=Alignment.LEFT,
allow_reset=True,
allow_skip=True,
multi=True,
preview_frame=FrameProperties.max('Package info'),
preview_style=PreviewStyle.RIGHT,
preview_size='auto',
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return []
case ResultType.Selection:
selected_pacakges = result.get_values()
return [pkg.name for pkg in selected_pacakges]
def add_number_of_parallel_downloads(preset: int | None = None) -> int | None:
max_recommended = 5
header = tr('This option enables the number of parallel downloads that can occur during package downloads') + '\n'
header += tr('Enter the number of parallel downloads to be enabled.\n\nNote:\n')
header += tr(' - Maximum recommended value : {} ( Allows {} parallel downloads at a time )').format(max_recommended, max_recommended) + '\n'
header += tr(' - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n')
def validator(s: str | None) -> str | None:
if s is not None:
try:
value = int(s)
if value >= 0:
return None
except Exception:
pass
return tr('Invalid download number')
result = EditMenu(
tr('Number downloads'),
header=header,
allow_skip=True,
allow_reset=True,
validator=validator,
default_text=str(preset) if preset is not None else None,
).input()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return 0
case ResultType.Selection:
downloads: int = int(result.text())
case _:
assert_never(result.type_)
pacman_conf_path = Path('/etc/pacman.conf')
with pacman_conf_path.open() as f:
pacman_conf = f.read().split('\n')
with pacman_conf_path.open('w') as fwrite:
for line in pacman_conf:
if 'ParallelDownloads' in line:
fwrite.write(f'ParallelDownloads = {downloads}\n')
else:
fwrite.write(f'{line}\n')
return downloads
def ask_post_installation() -> PostInstallationAction:
header = tr('Installation completed') + '\n\n'
header += tr('What would you like to do next?') + '\n'
items = [MenuItem(action.value, value=action) for action in PostInstallationAction]
group = MenuItemGroup(items)
result = SelectMenu[PostInstallationAction](
group,
header=header,
allow_skip=False,
alignment=Alignment.CENTER,
).run()
match result.type_:
case ResultType.Selection:
return result.get_value()
case _:
raise ValueError('Post installation action not handled')
def ask_abort() -> None:
prompt = tr('Do you really want to abort?') + '\n'
group = MenuItemGroup.yes_no()
result = SelectMenu[bool](
group,
header=prompt,
allow_skip=False,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
).run()
if result.item() == MenuItem.yes():
exit(0)

View File

@ -1,13 +1,17 @@
from __future__ import annotations
import re
from typing import override
from archinstall.lib.menu.helpers import Confirmation, Input
from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.users import User
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem
from archinstall.tui.curses_menu import EditMenu, SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, Orientation
from ..menu.list_manager import ListManager
from ..models.users import User
from ..utils.util import get_password
class UserList(ListManager[User]):
@ -26,17 +30,14 @@ class UserList(ListManager[User]):
prompt,
)
async def show(self) -> list[User] | None:
return await super()._run()
@override
def selected_action_display(self, selection: User) -> str:
return selection.username
@override
async def handle_action(self, action: str, entry: User | None, data: list[User]) -> list[User]:
def handle_action(self, action: str, entry: User | None, data: list[User]) -> list[User]:
if action == self._actions[0]: # add
new_user = await self._add_user()
new_user = self._add_user()
if new_user is not None:
# in case a user with the same username as an existing user
# was created we'll replace the existing one
@ -44,8 +45,7 @@ class UserList(ListManager[User]):
data += [new_user]
elif action == self._actions[1] and entry: # change password
header = f'{tr("User")}: {entry.username}\n'
header += tr('Enter new password')
new_password = await get_password(header=header, allow_skip=True)
new_password = get_password(tr('Password'), header=header)
if new_password:
user = next(filter(lambda x: x == entry, data))
@ -64,18 +64,18 @@ class UserList(ListManager[User]):
return None
return tr('The username you entered is invalid')
async def _add_user(self) -> User | None:
editResult = await Input(
tr('Enter a username'),
def _add_user(self) -> User | None:
editResult = EditMenu(
tr('Username'),
allow_skip=True,
validator_callback=self._check_for_correct_username,
).show()
validator=self._check_for_correct_username,
).input()
match editResult.type_:
case ResultType.Skip:
return None
case ResultType.Selection:
username = editResult.get_value()
username = editResult.text()
case _:
raise ValueError('Unhandled result type')
@ -83,21 +83,27 @@ class UserList(ListManager[User]):
return None
header = f'{tr("Username")}: {username}\n'
prompt = f'{header}\n' + tr('Enter a password')
password = await get_password(header=prompt, allow_skip=True)
password = get_password(tr('Password'), header=header, allow_skip=True)
if not password:
return None
header += f'{tr("Password")}: {password.hidden()}\n'
prompt = f'{header}\n' + tr('Should "{}" be a superuser (sudo)?\n').format(username)
header += f'{tr("Password")}: {password.hidden()}\n\n'
header += str(tr('Should "{}" be a superuser (sudo)?\n')).format(username)
result = await Confirmation(
header=prompt,
group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.yes()
result = SelectMenu[bool](
group,
header=header,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
search_enabled=False,
allow_skip=False,
preset=True,
).show()
).run()
match result.type_:
case ResultType.Selection:
@ -108,10 +114,6 @@ class UserList(ListManager[User]):
return User(username, password, sudo)
async def select_users(prompt: str = '', preset: list[User] = []) -> list[User]:
users = await UserList(prompt, preset).show()
if users is None:
return preset
def ask_for_additional_users(prompt: str = '', defined_users: list[User] = []) -> list[User]:
users = UserList(prompt, defined_users).run()
return users

View File

@ -1,13 +1,17 @@
from __future__ import annotations
import ipaddress
from typing import assert_never, override
from archinstall.lib.menu.helpers import Input, Selection
from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.models.network import NetworkConfiguration, Nic, NicType
from archinstall.lib.networking import list_interfaces
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import EditMenu, SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties
from ..menu.list_manager import ListManager
from ..models.network import NetworkConfiguration, Nic, NicType
from ..networking import list_interfaces
class ManualNetworkConfig(ListManager[Nic]):
@ -25,32 +29,28 @@ class ManualNetworkConfig(ListManager[Nic]):
prompt,
)
async def show(self) -> list[Nic] | None:
return await super()._run()
@override
def selected_action_display(self, selection: Nic) -> str:
return selection.iface if selection.iface else ''
@override
async def handle_action(self, action: str, entry: Nic | None, data: list[Nic]) -> list[Nic]:
def handle_action(self, action: str, entry: Nic | None, data: list[Nic]) -> list[Nic]:
if action == self._actions[0]: # add
iface = await self._select_iface(data)
iface = self._select_iface(data)
if iface:
nic = Nic(iface=iface)
nic = await self._edit_iface(nic)
nic = self._edit_iface(nic)
data += [nic]
elif entry:
if action == self._actions[1]: # edit interface
data = [d for d in data if d.iface != entry.iface]
nic = await self._edit_iface(entry)
data.append(nic)
data.append(self._edit_iface(entry))
elif action == self._actions[2]: # delete
data = [d for d in data if d != entry]
return data
async def _select_iface(self, data: list[Nic]) -> str | None:
def _select_iface(self, data: list[Nic]) -> str | None:
all_ifaces = list_interfaces().values()
existing_ifaces = [d.iface for d in data]
available = set(all_ifaces) - set(existing_ifaces)
@ -64,11 +64,12 @@ class ManualNetworkConfig(ListManager[Nic]):
items = [MenuItem(i, value=i) for i in available]
group = MenuItemGroup(items, sort_items=True)
result = await Selection[str](
result = SelectMenu[str](
group,
header=tr('Select an interface'),
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Interfaces')),
allow_skip=True,
).show()
).run()
match result.type_:
case ResultType.Skip:
@ -78,13 +79,18 @@ class ManualNetworkConfig(ListManager[Nic]):
case ResultType.Reset:
raise ValueError('Unhandled result type')
async def _get_ip_address(self, header: str, allow_skip: bool, multi: bool, preset: str | None = None, allow_empty: bool = False) -> str | None:
def _get_ip_address(
self,
title: str,
header: str,
allow_skip: bool,
multi: bool,
preset: str | None = None,
) -> str | None:
def validator(ip: str | None) -> str | None:
failure = tr('You need to enter a valid IP in IP-config mode')
if not ip:
if allow_empty:
return None
return failure
if multi:
@ -99,37 +105,39 @@ class ManualNetworkConfig(ListManager[Nic]):
except ValueError:
return failure
result = await Input(
result = EditMenu(
title,
header=header,
validator_callback=validator,
validator=validator,
allow_skip=allow_skip,
default_value=preset,
).show()
default_text=preset,
).input()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
return result.text()
case ResultType.Reset:
raise ValueError('Unhandled result type')
async def _edit_iface(self, edit_nic: Nic) -> Nic:
def _edit_iface(self, edit_nic: Nic) -> Nic:
iface_name = edit_nic.iface
modes = ['DHCP (auto detect)', 'IP (static)']
default_mode = 'DHCP (auto detect)'
header = tr('Select which mode to configure for "{}"').format(iface_name)
header = tr('Select which mode to configure for "{}"').format(iface_name) + '\n'
items = [MenuItem(m, value=m) for m in modes]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(default_mode)
result = await Selection[str](
result = SelectMenu[str](
group,
header=header,
allow_skip=False,
).show()
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Modes')),
).run()
match result.type_:
case ResultType.Selection:
@ -143,10 +151,10 @@ class ManualNetworkConfig(ListManager[Nic]):
if mode == 'IP (static)':
header = tr('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) + '\n'
ip = await self._get_ip_address(header, False, False)
ip = self._get_ip_address(tr('IP address'), header, False, False)
header = tr('Enter your gateway (router) IP address (leave blank for none)') + '\n'
gateway = await self._get_ip_address(header, True, False, allow_empty=True)
gateway = self._get_ip_address(tr('Gateway address'), header, True, False)
if edit_nic.dns:
display_dns = ' '.join(edit_nic.dns)
@ -154,7 +162,13 @@ class ManualNetworkConfig(ListManager[Nic]):
display_dns = None
header = tr('Enter your DNS servers with space separated (leave blank for none)') + '\n'
dns_servers = await self._get_ip_address(header, True, True, display_dns, allow_empty=True)
dns_servers = self._get_ip_address(
tr('DNS servers'),
header,
True,
True,
display_dns,
)
dns = []
if dns_servers is not None:
@ -166,26 +180,24 @@ class ManualNetworkConfig(ListManager[Nic]):
return Nic(iface=iface_name)
async def select_network(preset: NetworkConfiguration | None) -> NetworkConfiguration | None:
def ask_to_configure_network(preset: NetworkConfiguration | None) -> NetworkConfiguration | None:
"""
Configure the network on the newly installed system
"""
items = [MenuItem(n.display_msg(), value=n) for n in NicType]
group = MenuItemGroup(items, sort_items=False)
group = MenuItemGroup(items, sort_items=True)
if preset:
group.set_selected_by_value(preset.type)
header = tr('Choose network configuration') + '\n'
header += tr('Recommended: Network Manager for desktop, Manual for server') + '\n'
result = await Selection[NicType](
result = SelectMenu[NicType](
group,
header=header,
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Network configuration')),
allow_reset=True,
allow_skip=True,
).show()
).run()
match result.type_:
case ResultType.Skip:
@ -200,13 +212,9 @@ async def select_network(preset: NetworkConfiguration | None) -> NetworkConfigur
return NetworkConfiguration(NicType.ISO)
case NicType.NM:
return NetworkConfiguration(NicType.NM)
case NicType.NM_IWD:
return NetworkConfiguration(NicType.NM_IWD)
case NicType.IWD:
return NetworkConfiguration(NicType.IWD)
case NicType.MANUAL:
preset_nics = preset.nics if preset else []
nics = await ManualNetworkConfig(tr('Configure interfaces'), preset_nics).show()
nics = ManualNetworkConfig(tr('Configure interfaces'), preset_nics).run()
if nics:
return NetworkConfiguration(NicType.MANUAL, nics)

View File

@ -0,0 +1,188 @@
from __future__ import annotations
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, FrameStyle, Orientation, PreviewStyle
from ..args import arch_config_handler
from ..hardware import GfxDriver, SysInfo
from ..models.bootloader import Bootloader
def select_kernel(preset: list[str] = []) -> list[str]:
"""
Asks the user to select a kernel for system.
:return: The string as a selected kernel
:rtype: string
"""
kernels = ['linux', 'linux-lts', 'linux-zen', 'linux-hardened']
default_kernel = 'linux'
items = [MenuItem(k, value=k) for k in kernels]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(default_kernel)
group.set_focus_by_value(default_kernel)
group.set_selected_by_value(preset)
result = SelectMenu[str](
group,
allow_skip=True,
allow_reset=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Kernel')),
multi=True,
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return []
case ResultType.Selection:
return result.get_values()
def ask_for_bootloader(preset: Bootloader | None) -> Bootloader | None:
# Systemd is UEFI only
options = []
hidden_options = []
default = None
header = None
if arch_config_handler.args.skip_boot:
default = Bootloader.NO_BOOTLOADER
else:
hidden_options += [Bootloader.NO_BOOTLOADER]
if not SysInfo.has_uefi():
options += [Bootloader.Grub, Bootloader.Limine]
if not default:
default = Bootloader.Grub
header = tr('UEFI is not detected and some options are disabled')
else:
options += [b for b in Bootloader if b not in hidden_options]
if not default:
default = Bootloader.Systemd
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 = SelectMenu[Bootloader](
group,
header=header,
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Bootloader')),
allow_skip=True,
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
def ask_for_uki(preset: bool = True) -> bool:
prompt = tr('Would you like to use unified kernel images?') + '\n'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(preset)
result = SelectMenu[bool](
group,
header=prompt,
columns=2,
orientation=Orientation.HORIZONTAL,
alignment=Alignment.CENTER,
allow_skip=True,
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.item() == MenuItem.yes()
case ResultType.Reset:
raise ValueError('Unhandled result type')
def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None) -> GfxDriver | None:
"""
Some what convoluted function, whose job is simple.
Select a graphics driver from a pre-defined set of popular options.
(The template xorg is for beginner users, not advanced, and should
there for appeal to the general public first and edge cases later)
"""
if not options:
options = [driver for driver in GfxDriver]
items = [MenuItem(o.value, value=o, preview_action=lambda x: x.value.packages_text()) for o in options]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(GfxDriver.AllOpenSource)
if preset is not None:
group.set_focus_by_value(preset)
header = ''
if SysInfo.has_amd_graphics():
header += tr('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.') + '\n'
if SysInfo.has_intel_graphics():
header += tr('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n')
if SysInfo.has_nvidia_graphics():
header += tr('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')
result = SelectMenu[GfxDriver](
group,
header=header,
allow_skip=True,
allow_reset=True,
preview_size='auto',
preview_style=PreviewStyle.BOTTOM,
preview_frame=FrameProperties(tr('Info'), h_frame_style=FrameStyle.MIN),
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return None
case ResultType.Selection:
return result.get_value()
def ask_for_swap(preset: bool = True) -> bool:
if preset:
default_item = MenuItem.yes()
else:
default_item = MenuItem.no()
prompt = tr('Would you like to use swap on zram?') + '\n'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(default_item)
result = SelectMenu[bool](
group,
header=prompt,
columns=2,
orientation=Orientation.HORIZONTAL,
alignment=Alignment.CENTER,
allow_skip=True,
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return result.item() == MenuItem.yes()
case ResultType.Reset:
raise ValueError('Unhandled result type')

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