Compare commits

...

493 Commits

Author SHA1 Message Date
renovate[bot] b96afcc579
Update dependency arch/python-textual to v8.2.7 (#4556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 21:58:49 +10:00
BB 3bc248b47f
add niri DankMaterialShell profile (#4554) 2026-05-25 21:13:56 +10:00
Anton Hvornum af2120c0e9
mkosi for test image builds (#4539)
Added a `mkosi` profile as well as organized test tooling a bit.

---------

Co-authored-by: 0xdeadd <clintdotphillips@gmail.com>
2026-05-23 13:49:54 +02:00
renovate[bot] d92a98d3bf
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.14 (#4552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 12:39:28 +10:00
renovate[bot] af58671e88
Update dependency ruff to v0.15.14 (#4551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 12:39:11 +10:00
utuhiro78 e17e036f54
Update Japanese translation (#4550) 2026-05-21 07:25:23 +10:00
Daniel Girtler ac4bc442de
Refactor logging and formatting logic (#4542)
* Refactor logging and formatting

* Refactor logging and formatting

* Refactor logging and formatting

* Updaet
2026-05-19 17:48:03 +02:00
Clinton Phillips 5fd2f9b627
fix: restrict EFI partition permissions with fmask/dmask=0077 (#4506)
* fix: restrict EFI partition permissions with fmask/dmask=0077

Mount the ESP with fmask=0077 and dmask=0077 to prevent world-readable
files like /efi/loader/random-seed.

Closes #4241

* fix(efi): collapse fmask/dmask dedup to dict.fromkeys one-liner

Per @Torxed's review feedback. Same semantics as the previous loop
(dedupe by exact-string match) but shorter. dict.fromkeys preserves
insertion order, where set() would not.

* fix(efi): drop defensive list wrap per review

The list() copy on line 378 was load-bearing only if options were
mutated downstream, but the EFI branch reassigns options via
dict.fromkeys() (line 381) and the non-EFI branch passes through
to mount() without mutating. Drop the copy.
2026-05-19 13:36:28 +10:00
Softer dccd0c8ccf
Add translation CI validation (#4519)
* Fix broken localization: tr(f-string) never matches translation catalog

tr(f'Invalid configuration: {error}') evaluates the f-string before
tr() runs, so xgettext extracts the literal placeholder as the msgid
while runtime passes the formatted string - the two never match.
Switch to tr('...{}').format(...) and update msgid in base.pot.

* Add CI validation for translations and pot_tools dev utility

Add translation-check workflow with two jobs:
- validate-po: msgfmt --check on changed .po files, .mo sync warning,
  tr(f-string) anti-pattern grep on changed .py files
- validate-pot: verify all tr() strings exist in base.pot when .py
  files change

Workflow only triggers on .py/.po/.pot file changes.

Add scripts/pot_tools.py developer utility (stats, list, add_missing)
for managing base.pot.

* Fix code style: use tabs and reformat xgettext arguments

Align check_pot_freshness.py and pot_tools.py with project
indentation (tabs) and ruff format requirements.

Sorry :-)

* Replace custom PO parser with msgcmp, drop pot_tools.py

Address review feedback: use standard gettext msgcmp instead of
hand-rolled parser for base.pot freshness check. Remove pot_tools.py
that duplicated locales_generator.sh functionality.

* Move translation checks into locales_generator.sh, simplify CI workflow

Use msgcmp instead of diff for base.pot validation to avoid failing on
legacy stale entries - the same cascading breakage that killed the
original workflow (disabled 2023, removed in #4483).

* Fix broken .po files: duplicate msgid in Hindi, missing format args in Finnish
2026-05-18 20:10:56 +10:00
renovate[bot] 22bf6e3c35
Update dependency arch/python-textual to v8.2.6 (#4546)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 15:36:33 +10:00
Matteo b5d323762b
Update Italian translation + fix a string (#4545)
* Update Italian translation

* Added new strings
* Improved existing translations

* Fix wrong string + Italian translation

* Mispelled "respository" -> "repository"
2026-05-17 11:26:30 +10:00
utuhiro78 42d9113611
Update Japanese translation (#4544) 2026-05-16 09:29:27 +10:00
Daniel Girtler 516a61d8af
Enhance log sharing capability (#4526) 2026-05-15 21:38:17 +10:00
renovate[bot] e48ca45b0b
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.13 (#4541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 16:30:33 +10:00
renovate[bot] 74a1066661
Update dependency ruff to v0.15.13 (#4540)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 13:58:00 +10:00
HADEON b81fe955f0
Add IWD standalone option to network configuration + fix NM_IWD (#4528)
* Add iwd standalone option to network configuration

Adds NicType.IWD as a third option alongside NM and NM_IWD: install
iwd, write /etc/iwd/main.conf with EnableNetworkConfiguration=true and
NameResolvingService=systemd, enable iwd.service + systemd-resolved.service.
iwd handles DHCP itself and resolved picks up its DNS via the symlink, so
no NetworkManager pulled in.

Also fills in parse_arg cases for NM_IWD and IWD so config files
round-trip both nic types.

Assisted-By: Flint

* wire up resolv stub and networkd service

* exclude virtual devices, dont harcode iface name instead match 'ether'
similar pattern to .network files shipped in /etc/systemd/network

* use dedent and rename menu option
2026-05-15 13:57:31 +10:00
renovate[bot] 13944f3cca
Update pre-commit hook pre-commit/mirrors-mypy to v2.1.0 (#4538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-13 08:54:05 +10:00
renovate[bot] b95321d38c
Update dependency mypy to v2.1.0 (#4535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-13 08:33:42 +10:00
renovate[bot] 3dec5025c3
Update dependency arch/python-pydantic to v2.13.4 (#4534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-13 08:21:44 +10:00
stinga11 f7a6f70fc8
Replace terminal and file manager in Budgie profile (#4533)
I think it was a mistake to have made the previous changes to KDE Apps. In retrospect, it seemed like a good idea since the Budgie developer had done it that way in Fedora, which is the distro the developer uses as a reference for Budgie. When you start using it, you realize there's no bridge between the desktop and the KDE Apps, and things like Dolphin recognizes icons and themes, requiring some rather annoying manual configurations. Then, if you want to change them again, you have to change those configurations again. Files don't open by default with the apps either; you have to configure them for that to work.

At first, I thought the Budgie packager for Arch had forgotten some stray dependency, but with some free time, I tested it with Fedora 44, and to my surprise, it has exactly the same problems, which is completely unacceptable for a final stable release. I suppose he'll make the necessary changes in the near future, but right now, it's a disaster.
2026-05-13 08:21:15 +10:00
Daniel Girtler 7fc33c2507
Enhance config types and summary (#4532)
* Enhance config types and summary

* Update
2026-05-10 22:35:53 +02:00
Lena Pastwa 2de7254b21
Update Polish translation (#4529) 2026-05-08 11:28:36 +10:00
renovate[bot] af106eb238
Update dependency mypy to v2 (#4523)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 21:57:10 +10:00
renovate[bot] 4265be6e43
Update pre-commit hook pre-commit/mirrors-mypy to v2 (#4525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 21:56:30 +10:00
Softer b0b7983af2
Fix bspwm black screen: add provision() delegation and default configs (#4518)
* Fix bspwm black screen: add provision() delegation and default configs

* Move TYPE_CHECKING imports to top level in bspwm profile
2026-05-07 10:20:09 +10:00
Softer ef1cfe7b56
Add share-log subcommand to upload install.log to paste.rs (#4511)
* Add --share-log flag to upload install.log to paste.rs

* Apply ruff-format to share_log.py

* Rework share-log per review: subcommand, TUI confirmation, truncate large logs

* Replace curl/SysCommand with urllib.request, remove defensive try/except

Per review: use stdlib urllib instead of shelling out to curl,
drop unnecessary try/except around TUI confirmation,
remove tempfile (content is passed directly as bytes).

* Fix TUI imports after ui module was pulled one level up (#4515)

* Decouple share_install_log from TUI module

* Add unit tests for share_install_log function

* Update docs to use share-log subcommand syntax
2026-05-07 10:18:56 +10:00
Softer 7b5dddf34b
Fix truncated package metadata in additional packages preview (#3580) (#4510)
* Fix truncated package metadata in additional packages preview (#3580)

* Simplify package info fix: use rstrip and CSS wrap instead of env var plumbing

* Apply preview text wrap conditionally via wrap_preview parameter

* Add missing wrap-preview CSS to OptionListScreen and pass parameter through SelectMenu
2026-05-07 10:17:13 +10:00
renovate[bot] dcc38fe9aa
Update dependency arch/python-cryptography to v48 (#4520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 15:25:45 +10:00
Softer 11607851c4
Keep standalone initramfs for grub-btrfs when UKI is enabled (#4513)
* Keep standalone initramfs for grub-btrfs when UKI is enabled

Fixes #4505

* Move keep_initramfs logic into add_bootloader()

The grub-btrfs snapshot detection was in guided.py, forcing custom
script authors to replicate it. Since Installer already holds
disk_config, the check belongs inside add_bootloader().
2026-05-06 14:26:59 +10:00
Daniel Girtler ba7dbeadfc
Refactor profile seat access selection (#4457)
* Refactor profile seat access selection

* Update
2026-05-04 14:45:43 +10:00
aronmr-1 b5501d9507
Improved Finnish translation further (#4516) 2026-05-04 08:16:16 +10:00
renovate[bot] 528c27fbe1
Update dependency arch/python-textual to v8.2.5 (#4517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 08:14:56 +10:00
Daniel Girtler b18cc57216
Pull `ui` module one level up (#4515)
* Pull the UI module one level up

* Update
2026-05-03 23:54:42 +02:00
Softer dd34954011
Remove dead Cutefish desktop profile (#4514)
The cutefish package was never in the official Arch repos (AUR only)
and the upstream project is abandoned. Installing this profile would
always fail with a pacman "not found" error.
2026-05-03 10:33:48 +10:00
utuhiro78 76629ecc15
Update Japanese translation (#4504) 2026-04-30 17:37:37 +10:00
Softer dc7d9cf0a2
Improve Ukrainian translation (#4502) 2026-04-30 09:23:08 +10:00
correctmost b936fa11e3
Fix some inconsistent-return-statements Pylint warnings (#4503) 2026-04-30 09:22:45 +10:00
aronmr-1 6fefa3b6f5
Improved Finnish language (#4501) 2026-04-30 07:43:01 +10:00
Softer 3b026cb603
Fix broken localization in U2F, LUKS, and gfx driver preview (#4500) 2026-04-30 07:33:21 +10:00
correctmost 05560d29af
Add basic Pyrefly configuration to pyproject.toml (#4494)
This makes it easier to use the Pyrefly type checker with various
IDEs.
2026-04-29 21:54:59 +10:00
correctmost 3223ae4212
Remove the old curses-based TUI code (#4497)
The new Textual-based TUI shipped in the April .iso.
2026-04-29 17:08:24 +10:00
Steen Rabol 468e33d54f
Danish translation (#4493)
* Danish translation

* updated base.po and  created base.mo

---------

Co-authored-by: Steen Rabol <steen@rabol.dev>
2026-04-29 16:06:10 +10:00
utuhiro78 ccbc53d576
Update Japanese translation (#4498) 2026-04-29 13:44:11 +10:00
correctmost c4d68855cb
Replace time.time with time.monotonic to avoid clock drift issues (#4492)
Using time.time() can be inaccurate if the system clock gets
updated in between calls.
2026-04-28 13:34:33 +02:00
Softer 8a929f603f
Fix sway+nvidia confirmation dialog (#4481) (#4485)
* Fix sway+nvidia confirmation dialog (#4481)

Two bugs in the Sway+Nvidia driver confirmation:

1. The boolean was inverted - confirming "yes, I'm okay with issues"
   reverted the driver to the previous choice instead of keeping it.

2. The warning triggered for any Nvidia driver, including the
   open-source nouveau driver which is officially supported by Sway.

Add GfxDriver.is_nvidia_proprietary() and is_nvidia_nouveau() methods
so the warning fires only for nvidia-open-dkms (proprietary userspace).

* Address review feedback (#4485)

- Drop is_nvidia_nouveau() helper. It is not called anywhere yet; can
  be re-added when a consumer lands.
- Collapse the Sway+Nvidia confirmation result handling into a single
  expression now that allow_skip=False guarantees a boolean answer.
2026-04-28 21:02:32 +10:00
Softer e1efa34d8a
Install mkinitcpio explicitly in base packages (#4484)
Prevents pacstrap from picking a different initramfs provider
(dracut, booster) when running from a non-Arch host.

Fixes #4368
2026-04-28 21:01:05 +10:00
Softer cb0f3a6eba
Warn when no network configuration is selected (#4408)
* Make network configuration mandatory with explicit "No network" option

* Add network configuration recommendation hint and disable sorting

* Warn when network selection was skipped instead of making it mandatory

Replace the mandatory network_config menu requirement with a yellow
warning on the final confirmation screen, shown only when the user
skipped the network menu entirely. Explicitly picking "No network
configuration" is treated as a conscious choice and does not trigger
the warning.

- Drop mandatory=True from the network_config global menu item
- Rename NicType.NONE menu label from "No network" to
  "No network configuration" (an installed system still has an NIC;
  only the install-time configuration is absent)
- Add ConfigurationOutput.get_install_warnings() and render them in
  confirm_config when show_install_warnings=True
- guided.py and minimal.py enable the warning on the confirm screen
- Reuse the existing "No network configuration" msgid in .pot; add the
  warning string; update Ukrainian translations accordingly

* Drop NicType.NONE from this PR

The NONE option introduced earlier in this branch is being removed per
review feedback (#4408). Whether to add explicit "None" options across
multiple menus (greeter, gfx driver, network, bootloader) is now being
discussed in #4464 and should land as a separate change there.

This PR keeps only the warning-on-confirm behaviour: when the user
skipped the network menu entirely (network_config is None), a yellow
warning is shown on the final confirmation screen.
2026-04-28 20:17:40 +10:00
Hassaan Amin 1b7a32a3b3
Fix typos in known issues documentation (#4488)
First Edit :
- "effort" is singular so use has not have

Second Edit :
- we use "an" before vowel sounds (an old )


Third Edit :  
- use Sometimes because sometimes means occasionally and Some times means  some period or era 

Fourth Edit :
- Plural of ISO is ISOs not ISO's
2026-04-28 20:03:49 +10:00
Softer 08cba236f6
Show install summary when configuration is valid (#4475)
* Show install summary when configuration is valid

Previously the Install menu preview was empty when everything was valid,
leaving the user with no "ready" signal. Now show "Ready to install"
plus a two-column summary of the current configuration.

- New _install_summary() composes an aligned key/value table with rows
  for disks+FS+LUKS, bootloader, kernel, profile, greeter, package
  count, network, locale and timezone. Column width adapts to the
  longest translated label so translations keep the alignment.
- Rows whose underlying config is not set are skipped rather than
  rendered as empty.
- base.pot / uk base.po: add new msgids for summary labels.

* Move install summary into ConfigurationOutput

The summary helper that renders the install preview's two-column
configuration overview lived in GlobalMenu. Move it to
ConfigurationOutput.as_summary() so it sits alongside the JSON
output methods that share the same role. The preview now syncs
menu state to ArchConfig and delegates rendering.
2026-04-28 19:22:01 +10:00
Franco Castillo 8cc35f41d8
Update Spanish translation (#4490)
Signed-off-by: Franco Castillo <castillofrancodamian@gmail.com>
2026-04-28 17:08:39 +10:00
codefiles 9010ccf9eb
Add kernel enum (#4489) 2026-04-28 17:08:19 +10:00
Anton Hvornum 074dfbb178
Adding a reference to why AUR is not bundled as an option in archinstall (#4468) 2026-04-27 08:44:14 +02:00
Softer 008d303aaf
Remove disabled translation-check workflow (#4483)
The workflow has been fully commented out since #2119 (2023-09-28) when
translation handling was reworked. Because the file has no `on:` triggers,
GitHub Actions creates a failed workflow run for every push, polluting
the Actions tab without affecting PR check-runs.
2026-04-27 09:35:34 +10:00
utuhiro78 3795bfc21a
Update Japanese translation (#4482) 2026-04-27 09:32:10 +10:00
Softer 210329ed80
Add console font selection to Locales menu (#4469)
* Add console font selection to Locales menu

Add a 4th menu item "Console font" to the Locales configuration,
allowing users to select a console font for the target system.
The selected font is written to /etc/vconsole.conf.

If a terminus font (ter-*) is selected, the terminus-font package
is automatically installed on the target system.

* Switch list_console_fonts to pathlib and add @lru_cache

Address svartkanin's review on #4469. Replace os.listdir +
chained removesuffix with Path.glob('*.gz') + split('.')[0],
and cache the result via lru_cache - the kbd consolefonts
directory is static at runtime so re-scanning on every menu
reopen was wasted I/O.
2026-04-27 09:30:20 +10:00
Softer 9e05260df5
Fix argv injection in _create_user and gpasswd loop (#4473)
* Fix argv injection in _create_user and gpasswd loop

Use argv list with run() instead of f-string interpolation into
SysCommand, add debug logging on failure.

* Extract _chroot_argv helper and harden user/file ops

Address svartkanin's review on #4473: factor the
['arch-chroot', '-S', str(self.target), ...] boilerplate into a
private Installer._chroot_argv() helper, and migrate the seven
existing argv-form call sites to it (useradd, gpasswd, chpasswd,
chsh, chown, snapper-create-config, grub-install).

Two related hardening tweaks while in the area:

- Raise gpasswd failure log from debug() to warn(). The group-add
  loop has no return-False feedback channel for the caller, so a
  silent debug() means a half-configured user looks like a
  successful install.

- Add `--` end-of-options separator for useradd and chown so a
  username or path starting with `-` cannot smuggle flags. The TUI
  validates usernames, but parse_arguments() in models/users.py
  does not, so config.json is the residual hole; this closes it
  for these two sites at zero cost.
2026-04-27 09:24:51 +10:00
Softer d836ab0a66
Extend validate_bootloader_layout with UEFI-dependent checks (#4474)
* Extend validate_bootloader_layout with UEFI-dependent checks

Add is_uefi parameter and three new validations: Systemd-boot, Efistub
and rEFInd require UEFI; Efistub additionally requires a FAT boot
partition. Move the rEFInd UEFI-only check out of GlobalMenu so
guided.py and Installer silent-install paths get the same coverage.

* Encapsulate UEFI-only flag in Bootloader enum

Replace module-level _UEFI_ONLY_BOOTLOADERS tuple with an
is_uefi_only() method on the Bootloader enum, mirroring the
existing has_uki_support() / has_removable_support() pattern.

* Drop is_uefi parameter from validate_bootloader_layout

The UEFI flag is a constant system fact for the run, so the
validator retrieves it via SysInfo.has_uefi() directly instead
of having every caller pass it in. Updates all three call sites
in global_menu.py, installer.py and guided.py, and removes the
now-unused SysInfo import from guided.py.
2026-04-26 19:14:23 +10:00
renovate[bot] 3c4c87bdd6
Update dependency arch/python-cryptography to v47 (#4478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-26 19:03:16 +10:00
renovate[bot] efda78cad5
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.12 (#4477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-26 18:36:10 +10:00
renovate[bot] c8e5c85e20
Update dependency ruff to v0.15.12 (#4476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-26 18:35:31 +10:00
Softer bb46e295d1
Fix OVMF paths in README QEMU examples (#4472)
All three qemu-system-x86_64 examples in README pointed both -drive
if=pflash entries at the same file (/usr/share/ovmf/x64/OVMF.4m.fd).
OVMF needs CODE for the first pflash and VARS for the second; as
written, EFI NVRAM is not initialized correctly.

The path /usr/share/ovmf/x64/ is also stale - the ovmf package has
been replaced by edk2-ovmf, which installs under /usr/share/edk2/x64/.

Fix both in the three examples (testimage loop section, base Boot ISO
block, espeakup variant).
2026-04-24 12:19:01 +02:00
Thierry M de43019094
Update base.po (#4467) 2026-04-23 20:07:54 +10:00
renovate[bot] 6e222bcdd8
Update dependency pre-commit to v4.6.0 (#4459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 11:08:51 +10:00
renovate[bot] 6652197e35
Update pre-commit hook pre-commit/mirrors-mypy to v1.20.2 (#4463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 11:06:09 +10:00
renovate[bot] 80ae85cc8b
Update dependency mypy to v1.20.2 (#4458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-22 21:41:09 +10:00
correctmost 00834f9c6e
Use bools instead of time values for SysCommandWorker attributes (#4462)
Only the truthiness of the time values was being evaluated. This
also reduces the usage of time.time(), which makes the codebase
more resilient against clock drift issues.
2026-04-22 21:33:35 +10:00
Softer e7d38d0e82
Fix Limine install with ESP mounted outside /boot (#4442)
* Fix Limine install with ESP mounted outside /boot

Place limine.conf next to the EFI binary on the ESP so it is found
regardless of ESP mountpoint, and block unbootable layouts (non-UKI
Limine with ESP not at /boot and no separate /boot partition) in
GlobalMenu validation, guided.main() and _add_limine_bootloader().

Fixes #4333

* Extract bootloader layout validation into lib/bootloader/utils

* Consolidate Limine layout validation in bootloader utils

Move the boot-partition FAT check from GlobalMenu into
validate_bootloader_layout so all three call sites (GlobalMenu,
guided.py, Installer._add_limine_bootloader) share one function.

Return a BootloaderValidationFailure dataclass (kind + description)
instead of str | None, so callers can match on the failure kind and
the description is built where partition context is in scope.

* Encapsulate FAT filesystem detection in FilesystemType.is_fat()
2026-04-22 21:32:41 +10:00
correctmost d1765323d8
Use staticmethod for PasswordStrength methods (#4461)
The methods don't need access to the class, so they don't need to
be classmethods. This change reduces the number of 'bad return'
warnings seen when running Pyright, Pyrefly, and ty.
2026-04-22 21:31:06 +10:00
javier-anton-ordonez 094798b496
ES Language updated (#4456)
* ES langauje updated and added diferent translations

* Update archinstall/locales/es/LC_MESSAGES/base.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Cerrar -> Ocultar

Github copilot suggested

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix Spanish translations and accents in base.po

Github suggestions

* ./locales_generator.sh generated

* Accent updated and ./locales_generator.sh generated

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-22 10:34:41 +10:00
stinga11 fa54c9d6c9
Change Budgie display server and update packages (#4450)
Some changes consistent with other distros.
2026-04-22 10:23:09 +10:00
renovate[bot] b842e54091
Update dependency arch/python-textual to v8.2.4 (#4448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 19:52:17 +10:00
Softer 03b245a77a
Set console font automatically when selecting language (#4356)
* Set console font automatically when selecting language

Add console_font field to languages.json and Language dataclass.
When a language is activated, setfont is called automatically,
falling back to default8x16 on error or for languages without
a custom font. Also activate translation when loading language
from config file.

* Support FONT environment variable for console font override

When FONT env var is set, use it as the console font instead of the language-specific font mapping from languages.json. The font is applied at startup and preserved across language switches.

On exit (success or failure), restore the console font to default8x16.

* CI checks fix

* Try to restore original console font with setfont -O

* Fix for pylint

* Restore console font before Textual exits application mode

Move font set/restore into Textual lifecycle to prevent color
artifacts from 256/512 glyph transitions. Apply FONT env var
and language font in on_mount, restore in _on_exit_app.

Skip font change when loading language from config (set_font=False)
to defer it until TUI starts.

* Fall back to language font mapping when FONT env var is invalid

Add _using_env_font flag to skip mapping only when FONT was
successfully applied. Show info message after TUI exits if FONT
could not be set.

* Use tempfile for console font backup files

* Fix linter errors: use mkstemp, close fds, add @override

* Fix ruff formatting

* Move font state from module singleton into TranslationHandler

* Refactor font handling per review feedback

* Make font methods members of TranslationHandler, skip on non-ISO

* ci: trigger tests

* Move running_from_iso import to module level

No circular dep, simpler than per-method local imports.

* Add explicit ISO guard to restore_console_font

Matches the existing guards in _set_font and save_console_font.
Behaviour was already safe implicitly via _font_backup=None, but
the explicit check makes intent obvious at the call site.

* Skip apply_console_font off-ISO

* Use list form for setfont SysCommand calls
2026-04-21 18:36:29 +10:00
Dylan M. Taylor 7d10f9e08b
ci: Strip Arch pkgrel from Repology versions in Renovate config (#4453)
* ci: Strip Arch pkgrel from Repology versions in Renovate config

* ci: Use .+ instead of .* in extractVersionTemplate

Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>

---------

Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>
2026-04-21 15:18:25 +10:00
Anton Hvornum d80bdb4fad
Bumping version to: 4.3 (#4452) 2026-04-20 22:31:22 +02:00
Anton Hvornum 68d137cdce
Bumping version to: 4.3 2026-04-20 22:28:35 +02:00
Softer 82a46c66ca
Fix shell injection in user_set_shell and chown (#4443)
Use argv list with run() instead of sh -c with f-string interpolation,
fix mutable default argument in chown, add debug logging on failure.
2026-04-20 18:36:49 +10:00
Dhruv 86dc1bbc12
locale(hindi): Completed the hindi translation locale (#4446)
* locale: ran generate_locale before making change

* locale: first 1000 confirmed everything is fine

* locale(hi): fixed rest of the fuzzy and empty strings

* locale(hi): ran locale generator
2026-04-20 18:29:26 +10:00
codefiles cd62eff4a7
Fix copying into target directory (#4441) 2026-04-18 12:05:00 +10:00
Softer e8ea33c41c
Enable power management services after package installation (#4440) 2026-04-18 12:04:14 +10:00
Softer 4fcef35af0
Add optional "Additional fonts" selection to Applications menu (#4420)
* Add Fonts application with multi-select for emoji and CJK packages

* Rename to Additional fonts and add package descriptions

* Add noto-fonts to font package selection

* Move font descriptions into FontPackage enum method

* ci: trigger tests

* Add ttf-liberation and ttf-dejavu to font selection
2026-04-18 12:02:47 +10:00
codefiles 9fdd7eb12e
Refactor EncryptionType (#4438)
* Use UPPER_CASE for EncryptionType

* Use StrEnum for EncryptionType
2026-04-17 10:08:23 +10:00
codefiles 8fe8d4e35f
Pin linkify-it-py to Arch Linux package version (#4437) 2026-04-17 10:07:57 +10:00
renovate[bot] 02faf9fbb9
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.11 (#4436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 10:07:09 +10:00
renovate[bot] 9b9f9d6ac0
Update dependency ruff to v0.15.11 (#4435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 10:06:51 +10:00
Dylan M. Taylor 98d72a6b57
ci: Update renovate to track Arch Linux repos using repology (#4434) 2026-04-16 19:17:07 +10:00
Daniel Girtler 06488dfb9a
Fix table column error (#4406) 2026-04-16 07:30:38 +02:00
Daniel Girtler 29588fadee
Fix encrypted partition selection (#4407) 2026-04-16 07:29:53 +02:00
codefiles 3cabe860ae
Refactor LsblkInfo field validators (#4428) 2026-04-16 14:06:23 +10:00
codefiles 7ea171369a
Refactor PartitionType (#4432)
* Use UPPER_CASE for PartitionType

* Use StrEnum for PartitionType
2026-04-16 14:05:08 +10:00
Anton Hvornum 8b3be842ca
Bumping version to: 4.2 (#4433) 2026-04-15 20:47:13 +02:00
codefiles 30f50d9719
Revert pydantic update due to upstream bug (#4427) 2026-04-15 10:16:34 +10:00
tom c1771cf615
locale(german): extend and fix translation (#4421)
* chore(locale): Fix and improve German translation strings

* chore(locale): edit incorrect translations
2026-04-14 17:40:15 +10:00
Ian Dieb H. Rebouças c0d3c14b6e
Update Brazilian Portuguese translation (#4426)
Co-authored-by: Ian Dieb <idbreboucas@gmail.com>
2026-04-14 17:39:23 +10:00
correctmost 113cc7fd94
Fix urllib.parse import to avoid warnings with ty and Pyright (#4425) 2026-04-14 15:39:08 +10:00
renovate[bot] 88366ba575
Update dependency pydantic to v2.13.0 (#4424)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 15:38:14 +10:00
renovate[bot] 728bb450c7
Update pre-commit hook pre-commit/mirrors-mypy to v1.20.1 (#4423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 15:37:56 +10:00
codefiles 2b27e565ae
Refactor ModificationStatus (#4422)
* Use UPPER_CASE for ModificationStatus member names

* Use StrEnum for ModificationStatus

* Replace LvmVolumeStatus with ModificationStatus
2026-04-14 15:37:14 +10:00
renovate[bot] a52bfc3446
Update dependency mypy to v1.20.1 (#4419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 18:49:04 +10:00
renovate[bot] 5dd94e80d2
Update astral-sh/ruff-action action to v4 (#4416)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 18:48:17 +10:00
codefiles 335a7b792c
Refactor FilesystemType (#4417)
* Use UPPER_CASE for FilesystemType member names

* Use StrEnum for FilesystemType

* Use FilesystemType member values
2026-04-13 18:41:59 +10:00
Matteo 5553cb9eae
locale: Update Italian translation (#4415)
-Translated new strings
-Fixed a couple of old translations
2026-04-13 18:38:24 +10:00
Softer 80da3f14a1
Add Pacman settings submenu with Color and ParallelDownloads (#4404)
* Add Pacman settings submenu with Color and ParallelDownloads

Replace the standalone Parallel Downloads menu item with a Pacman
submenu containing ParallelDownloads (default 5) and Color (default on).
Settings are applied to both live and target system pacman.conf.
Hidden behind --advanced flag.

Backward compatible with old configs using "parallel_downloads" key.

* Skip _apply_to_live when user exits Pacman menu without changes

* Use TypedDict for PacmanConfiguration serialization

* Show Pacman menu by default, keep ParallelDownloads behind --advanced
2026-04-13 18:37:51 +10:00
codefiles bf9b9cb7c1
Remove invalid container PATH lookup (#4413) 2026-04-12 14:48:51 +10:00
Softer 9a2d546882
Add translation support for TUI help groups and binding descriptions (#4363)
* Add translation support for TUI help groups and binding descriptions

Help group names shown via F1 (General, Navigation, Selection, Search) and key binding descriptions in the Textual footer (Down, Up, Cancel, Confirm, etc.) were hardcoded in English and never went through the translation system.

* ruff_format_check and mypy fixes

* Refactor _translate_bindings to accept BindingsMap instead of Any

Add TApp.translate_bindings() to avoid exporting private functions
across modules.

* Revert deprecated curses help.py change

* Move TApp import to module level in global_menu

* Change translate_bindings from staticmethod to member method
2026-04-12 14:24:46 +10:00
codefiles d6de03ab76
Use auto() for eligible StrEnum members (#4411) 2026-04-11 22:51:43 +10:00
Koutheir Attouchi 51600ecd2a
Update Arabic translation (#4410) 2026-04-11 18:14:14 +10:00
Softer 60bcded743
Replace tab with spaces in preview package lists (#4409) 2026-04-11 17:48:26 +10:00
codefiles 934407414e
Add constant for mirrorlist (#4403) 2026-04-11 10:17:29 +10:00
renovate[bot] 1a4eedf868
Update actions/upload-artifact digest to 043fb46 (#4402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-11 10:16:58 +10:00
Daniel Girtler b2f413124b
Allow granular plasma configuration (#4389)
* Flexible plasma profile selection

* Update

* Update

* Update

* Update
2026-04-10 07:07:28 +02:00
renovate[bot] 101f647319
Update dependency ruff to v0.15.10 (#4400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 07:24:30 +10:00
renovate[bot] 9059a18dfe
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.10 (#4401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 07:24:01 +10:00
correctmost d6987b4e9d
Fix del-attr-with-constant ruff warnings (#4399) 2026-04-09 10:19:02 +10:00
codefiles 1bf87c6f4a
Use Path.copy() (#4398) 2026-04-08 15:18:09 +02:00
codefiles 6505e17f34
Refactor sync_log_to_install_medium() (#4397) 2026-04-08 14:35:35 +02:00
Daniel Girtler 78987d75fe
Revert enter behavior on multi-select (#4386) 2026-04-08 09:17:38 +02:00
renovate[bot] b906a34a5d
fix(deps): update dependency pytest to v9.0.3 (#4391)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 17:05:33 +10:00
correctmost 18c77b94f2
Remove unnecessary 'unused-ignore' mypy directives (#4395)
The directives were added because a PR branch accidentally lagged
behind master and did not contain the mypy python_version bump from
commit 0175949ca.

This fixes commit d70e03fa3.
2026-04-08 08:42:49 +02:00
codefiles 3ef0848ffe
Refactor copy_iso_network_config() (#4394) 2026-04-08 08:13:14 +02:00
codefiles a92cd50aec
Remove deprecated __future__ imports (#4393) 2026-04-08 08:11:05 +02:00
Crystal eda9ee338b
Update German translation for base.po (#4334)
* Update German translations in base.po

Updated German translations for various prompts and messages in the base.po file.

* Apply suggestions from code review

Co-authored-by: Luca Zeuch <l-zeuch@email.de>

* Removed the # fuzzy´s from the already translated sentences.

---------

Co-authored-by: Luca Zeuch <l-zeuch@email.de>
2026-04-07 23:23:22 +10:00
Ghosted Owl caf285a6dc
Czech translation (#4370)
* Complete Czech translation

* Complete Czech translation
2026-04-07 23:21:52 +10:00
codefiles 7609a1634e
Bump ruff target-version to Python 3.14 (#4390)
* Bump ruff target-version to Python 3.14

* Update exception syntax to pass ruff format
2026-04-07 23:09:14 +10:00
Daniel Girtler f37ae8b282
Handle empty mountpoint input (#4388) 2026-04-07 13:54:18 +02:00
codefiles d57709cb2f
Remove quotes from type annotation (#4384) 2026-04-07 12:21:51 +10:00
Lena Pastwa 9e8cbd0181
Update Polish translation (#4383) 2026-04-07 12:21:19 +10:00
HADEON 3ba29f1193
Patch plasma profile (#4358)
* add essentials #4336

* rem unrelated change
2026-04-06 22:43:06 +10:00
codefiles a8d265184c
Add constant for archiso mountpoint (#4377) 2026-04-06 22:41:33 +10:00
codefiles 37159dcb6d
Add LPath execute permissions methods (#4378) 2026-04-06 09:16:21 +02:00
codefiles e2bd7b3405
Remove is_subpath() and use Path.is_relative_to() (#4372) 2026-04-06 16:42:45 +10:00
codefiles 56bec01979
Add constant for /etc/pacman.conf (#4375) 2026-04-05 23:18:22 +02:00
Ghosted Owl 41858db832
Russian translation (#4367)
* Сomplete russian translation

* Сomplete russian translation
2026-04-03 23:41:28 +11:00
renovate[bot] 5a4b6131c1
fix(deps): update dependency ruff to v0.15.9 (#4366)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-03 09:50:16 +11:00
renovate[bot] c91e19e534
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.15.9 (#4365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-03 09:41:45 +11:00
Dylan M. Taylor 6c6c8d8000
Stop installing xorg packages for Wayland profiles (#4348)
* Add WaylandProfile to avoid installing xorg packages for Wayland compositors

* Refactor: use composition (is_wayland) instead of WaylandProfile inheritance

* Fix X11 profiles to inherit xorg packages from XorgProfile

* Style: use consistent multi-line super().__init__ for Wayland profiles

* Refactor: replace is_wayland with DisplayServerType enum

Add DisplayServerType enum (Xorg/Wayland) to Profile. All profiles
now inherit Profile directly with an explicit display_server param.
desktop.py installs xorg-server and xorg-xinit for Xorg profiles.
XorgProfile remains for standalone Xorg selection.

* Remove unnecessary super().packages from desktop profiles
2026-04-03 09:38:02 +11:00
Julio Napurí 6c9f66265a
i18n: Updated Spanish translation (#4357) 2026-04-02 18:21:49 +11:00
Softer 5544abd380
Improved translation into Ukrainian (#4355) 2026-04-02 14:40:40 +11:00
renovate[bot] deb8080322
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.20.0 (#4351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-02 07:52:30 +11:00
correctmost d70e03fa3b
Update mypy to 1.20.0 (#4352)
* Update mypy to 1.20.0

This commit also removes a cast that is no longer needed after
https://github.com/python/mypy/pull/20602

* Ignore os.system deprecation warnings from mypy to fix CI
2026-04-02 07:51:47 +11:00
Dylan M. Taylor 29b73302ae
Don't write encryption keyfiles to an unencrypted root partition (#4349) 2026-04-01 15:35:33 +11:00
Dylan M. Taylor a3c6bd5d45
Use nvidia-open instead of nvidia-open-dkms for mainline kernels (#4347) 2026-04-01 15:24:11 +11:00
Anton Hvornum 0175949cab
Bumping version to: 4.1 (#4346) 2026-03-31 23:02:14 +02:00
Dylan M. Taylor 179d9c7b48
Remove Nvidia proprietary driver option (nvidia-dkms no longer in repos) (#4343)
* Remove Nvidia proprietary driver option (nvidia-dkms no longer in repos)

* Remove libva-mesa-driver (now provided by mesa)
2026-03-31 22:51:57 +02:00
Odyssey bdf6f92991
Update catalan locales (#4344) 2026-04-01 07:33:34 +11:00
Dylan M. Taylor ef4d7cfc3a
Move auth config helpers to AuthenticationConfiguration class (#4340) 2026-03-31 11:29:26 +11:00
Dylan M. Taylor 603c432e18
Warn when a desktop profile's greeter can't log in without a regular user (#4331) 2026-03-31 10:48:01 +11:00
Anton Hvornum e3e9563aa6
Bumping version to: 4.0 2026-03-30 23:35:51 +02:00
renovate[bot] 398d6cfdbe
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.15.8 (#4329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 14:33:13 +11:00
renovate[bot] c76fd90074
fix(deps): update dependency ruff to v0.15.8 (#4328)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 14:32:55 +11:00
Daniel Girtler 4b9087e2f4
Fix AwaitComplete runtime error (#4325) 2026-03-26 13:04:43 +01:00
Daniel Girtler dc64e15327
Handle enter on filter confirm (#4324) 2026-03-26 13:04:09 +01:00
Daniel Girtler dd0da34ed9
Move luks into disk module (#4296) 2026-03-21 18:36:00 +11:00
Daniel Girtler e763c1109c
Refactor interactive module (#4315)
* Refactor interactive module

* Refactor interactive module

* Update
2026-03-21 18:35:44 +11:00
Daniel Girtler 1764b4971d
Refactor mirrors (#4274) 2026-03-21 17:57:33 +11:00
Daniel Girtler 8d6c56ca2b
Allow skip on password confirmation (#4273) 2026-03-21 17:57:17 +11:00
Daniel Girtler e06dd6299f
Zram - remove custom size in config (#4272) 2026-03-21 17:56:59 +11:00
Daniel Girtler 8e34303175
Fix iwd network installation and setup (#4271)
* Fix 4223 - IWD

* Update

* Update
2026-03-21 17:56:41 +11:00
renovate[bot] cfdcebcacd
fix(deps): update dependency ruff to v0.15.7 (#4312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-21 12:35:25 +11:00
renovate[bot] b21ac742c2
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.15.7 (#4311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-21 12:34:40 +11:00
Rosy 594cf54c8a
Update Turkish translations and refresh locale template (#4310) 2026-03-19 08:36:23 +11:00
Piyush Singh e74a72f482
fix: reintegrate PasswordStrength into password prompt (#4111) (#4291) 2026-03-18 22:48:05 +11:00
Daniel Girtler b186fb3f64
Async menu (#4308)
* Move to async TUI

* Update

* Update
2026-03-16 10:39:36 +11:00
renovate[bot] ffa130fe99
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.15.6 (#4303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 15:51:48 +11:00
amode e718a12b42
Removed unnecessary signal for multi-line comment. (#4298) 2026-03-13 15:01:52 +11:00
renovate[bot] 28f4904a3c
fix(deps): update dependency ruff to v0.15.6 (#4304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 15:01:20 +11:00
Valerii bace59aef8
better Ukrainian 1.4.2 (#4301)
* Add files via upload

* Add files via upload
2026-03-13 14:59:26 +11:00
Atharv Singh Negi 8b65bd9a6a
Add nepali translation (#4295)
* Improve Hindi translations (base.po)

* Improve all Fuzzy and more hindi translations

* Update base.po

* Update base.po

* Update Hindi translations in base.po

* Update translation for Network Manager iwd backend

* Create base.po

* Update base.po

* Update base.po

* Update languages.json

* Update base.po

* Complete Nepali translation and generated .mo file

* Update languages.json

* Update languages.json

* Update Nepali translations with system and user strings

* Add translations for Partitioning, Bootloader, and Network

* Add disk and configuration translations

* Reach 500+ lines: User management, NTP, and BTRFS subvolumes

* Reached 700+ strings: Desktop profiles, BTRFS setup, and final installation prompts

* Fix syntax error in base.po and update translations
2026-03-09 21:22:53 +11:00
Atharv Singh Negi a4bfd379f5
Add nepali translation (#4294)
* Improve Hindi translations (base.po)

* Improve all Fuzzy and more hindi translations

* Update base.po

* Update base.po

* Update Hindi translations in base.po

* Update translation for Network Manager iwd backend

* Create base.po

* Update base.po

* Update base.po

* Update languages.json

* Update base.po

* Complete Nepali translation and generated .mo file

* Update languages.json

* Update languages.json

* Update Nepali translations with system and user strings

* Add translations for Partitioning, Bootloader, and Network

* Add disk and configuration translations

* Reach 500+ lines: User management, NTP, and BTRFS subvolumes

* Reached 700+ strings: Desktop profiles, BTRFS setup, and final installation prompts
2026-03-09 12:31:36 +11:00
Atharv Singh Negi 2b63d573af
Locales: Add Nepali (ne) translation (Initial 300+ strings) (#4288)
* Improve Hindi translations (base.po)

* Improve all Fuzzy and more hindi translations

* Update base.po

* Update base.po

* Update Hindi translations in base.po

* Update translation for Network Manager iwd backend

* Create base.po

* Update base.po

* Update base.po

* Update languages.json

* Update base.po

* Complete Nepali translation and generated .mo file

* Update languages.json

* Update languages.json

* Update Nepali translations with system and user strings

* Add translations for Partitioning, Bootloader, and Network

* Add disk and configuration translations

* Reach 500+ lines: User management, NTP, and BTRFS subvolumes
2026-03-07 16:57:37 +11:00
renovate[bot] 33b0131e4e
fix(deps): update dependency ruff to v0.15.5 (#4287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 12:06:53 +11:00
renovate[bot] 8638726e7a
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.15.5 (#4286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 12:06:33 +11:00
codefiles 6d87a062fd
Move all LVM helpers to dedicated module (#4283) 2026-03-05 09:13:04 +11:00
Atharv Singh Negi e133cb0d28
Add nepali translation (#4282)
* Improve Hindi translations (base.po)

* Improve all Fuzzy and more hindi translations

* Update base.po

* Update base.po

* Update Hindi translations in base.po

* Update translation for Network Manager iwd backend

* Create base.po

* Update base.po

* Update base.po

* Update languages.json

* Update base.po

* Complete Nepali translation and generated .mo file

* Update languages.json

* Update languages.json

* Update Nepali translations with system and user strings

* Add translations for Partitioning, Bootloader, and Network
2026-03-05 09:11:45 +11:00
codefiles 452abe4277
Move udev_sync() to disk.utils (#4281) 2026-03-04 12:43:47 +11:00
codefiles 813b9b34b3
Refactor arch_config_handler to use DI (#4280) 2026-03-03 20:11:22 +11:00
codefiles 64567a748a
Remove support for NTFS root file system (#4279)
* Remove support for NTFS root file system

* Fix rootfstype value
2026-03-03 12:21:31 +11:00
codefiles b809c84ad5
Use os.sync() instead of sync command (#4278) 2026-03-02 07:48:22 +11:00
codefiles 501759d979
Remove obsolete GRUB package install (#4277) 2026-03-02 07:04:32 +11:00
codefiles 839e6bfc55
Move get_version() to version module (#4270) 2026-03-01 11:31:13 +11:00
codefiles 74a230dae9
Enable if-key-in-dict-del ruff rule (#4269) 2026-03-01 08:06:49 +11:00
codefiles f19f8d195f
Replace Installer with path parameter in Boot (#4268) 2026-03-01 08:06:15 +11:00
codefiles 76d6f08841
Remove unnecessary post-init (#4267) 2026-02-28 17:30:59 +11:00
codefiles 390b4959bd
Refactor ISO check (#4264) 2026-02-28 11:20:02 +11:00
correctmost d413c01fde
Fix a reportUnnecessaryIsInstance Pyright warning (#4263) 2026-02-27 17:39:46 +11:00
renovate[bot] d781c9d857
fix(deps): update dependency ruff to v0.15.4 (#4259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 15:39:03 +11:00
renovate[bot] 33f3709c31
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.15.4 (#4261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 15:38:49 +11:00
renovate[bot] ec8bd01ced
chore(deps): update actions/upload-artifact action to v7 (#4260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 12:14:25 +11:00
codefiles 3cd85c023e
Remove unneeded chmod (#4262) 2026-02-27 12:13:59 +11:00
renovate[bot] b6ce255b36
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.15.3 (#4258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 06:59:38 +11:00
Daniel Girtler e4faba2de0
Convert imports to absolute imports (#4196)
* Enable ruff absolute import check

* Convert all relative imports to absolute

* Update

* Update
2026-02-26 20:17:48 +11:00
Anton Hvornum c92a457142
Revert "Locale timezone python chmod (#4210)" (#4257)
This reverts commit 01d3af9fbb.
2026-02-25 22:48:03 +01:00
Favilances 01d3af9fbb
Locale timezone python chmod (#4210)
* Set timezone during minimal install

* Clean up conflicting boot entries

* Revert "Clean up conflicting boot entries"

This reverts commit e673301b99.
2026-02-25 22:47:25 +01:00
Daniel Girtler 683996c44f
Add missing linkify dependency (#4208)
* Add missing linkify dependency

* Update PKGBUILD
2026-02-25 22:45:28 +01:00
Daniel Girtler 4f87ccba28
Refactor general utility functions (#4212) 2026-02-25 22:45:21 +01:00
renovate[bot] 3447d2f47d
fix(deps): update dependency pylint to v4.0.5 (#4253)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 08:45:24 +11:00
codefiles d86daa531a
Add provision() to configure users for profiles (#4254) 2026-02-22 07:53:58 +11:00
renovate[bot] a9c939cb86
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.15.2 (#4251)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 17:34:04 +11:00
renovate[bot] 415fefef26
fix(deps): update dependency ruff to v0.15.2 (#4252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 17:33:41 +11:00
codefiles 2d18b55fa7
Remove unused advanced member from Profile (#4248) 2026-02-19 12:32:57 +11:00
nubprogrammer bb76f96bd5
locales: Add Nepali (ne) translation (#4222)
* Improve Hindi translations (base.po)

* Improve all Fuzzy and more hindi translations

* Update base.po

* Update base.po

* Update Hindi translations in base.po

* Update translation for Network Manager iwd backend

* Create base.po

* Update base.po

* Update base.po

* Update languages.json

* Update base.po

* Complete Nepali translation and generated .mo file

* Update languages.json

* Update languages.json

* Update Nepali translations with system and user strings
2026-02-19 07:22:43 +11:00
Mário Victor Ribeiro Silva 377b3bee97
feat: add `plasma-login-manager` (#4247) 2026-02-19 07:20:13 +11:00
codefiles 618c0bd5dc
Remove arch_config_handler from installer (#4246) 2026-02-19 07:18:53 +11:00
codefiles bd35473b5d
Move LVM helpers to dedicated module (#4245) 2026-02-18 15:57:10 +11:00
codefiles f2c17c6341
Move get_parent_device_path() to disk.utils (#4243) 2026-02-18 09:12:47 +11:00
HADEON f736b8c4ec
install gfx before DE/WM/Server (#4238) 2026-02-17 13:19:11 +11:00
codefiles 8edab9fafd
Move get_unique_path_for_device() to disk.utils (#4242) 2026-02-17 13:15:07 +11:00
codefiles 083a73eab1
Move unlock_luks2_dev() (#4234) 2026-02-17 07:35:25 +11:00
codefiles 35c2ff3ef5
Move swapon() to disk.utils (#4233) 2026-02-16 14:43:10 +11:00
HADEON 8148b1d9bf
fix efistub bootloader to use backslashes (#4231)
* fix efistub to use backslashes

* comment

* cf1

Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>

* cf2

Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>

---------

Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>
2026-02-16 13:10:52 +11:00
codefiles 3b6f7db942
Move mount() to disk.utils (#4232) 2026-02-16 13:08:59 +11:00
codefiles 9bd2131792
Refactor SysInfo.has_uefi() to DI for bootloader (#4230) 2026-02-15 12:34:40 +11:00
renovate[bot] ec9ee80062
fix(deps): update dependency ruff to v0.15.1 (#4224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-15 09:58:02 +11:00
renovate[bot] cfe61c0993
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.15.1 (#4225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-15 09:57:47 +11:00
Franco Castillo 6ce0556c2c
Update Spanish translation (#4228) 2026-02-15 08:40:19 +11:00
correctmost a61d4e5314
Revert "Handle whole-disk LUKS root params (#4205)" (#4229)
This reverts commit 0dddc7308d.
2026-02-15 08:38:55 +11:00
Mário Victor Ribeiro Silva 21358f77dd
feat: update `pt_BR` translations (#4221) 2026-02-12 12:30:14 +11:00
Matteo a7901d172d
Translation: Update Italian (#4219)
- Added new strings
- Improved old translation
2026-02-06 07:49:01 +11:00
utuhiro78 c0d47db75d
Update Japanese translation (#4217) 2026-02-04 19:34:31 +11:00
renovate[bot] 66e0dd119f
Update dependency ruff to v0.15.0 (#4214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 20:49:30 +00:00
renovate[bot] 13b7c7194a
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.0 (#4215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 07:48:52 +11:00
renovate[bot] 3dab440cbf
Update actions/checkout digest to de0fac2 (#4213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 07:06:48 +11:00
Daniel Girtler 1d8352b466
Use TypeAdapter for Json serialization (#4183) 2026-02-03 22:27:51 +11:00
Favilances 0dddc7308d
Handle whole-disk LUKS root params (#4205)
Co-authored-by: favilances <favilances@proton.me>
2026-02-03 22:03:22 +11:00
Daniel Girtler 4a8604ac88
Make list manager run private (#4192) 2026-02-03 11:22:56 +01:00
Daniel Girtler aac6c896d9
Add missing galician mo file (#4209) 2026-02-03 11:18:11 +01:00
gaelgnz f8304b0bd1
Added Galician language support (#4202)
* Added Galician language support

* Delete archinstall/locales/gl/LC_MESSAGES/base.mo
2026-02-03 11:35:33 +11:00
codefiles 0394a735a7
Fix typo locale -> local (#4201) 2026-02-03 11:34:53 +11:00
codefiles 12bd83ca8a
Refactor skip_boot to use dependency injection (#4200) 2026-02-03 11:34:27 +11:00
correctmost b5f710425f
Remove an unused milliseconds member from Installer (#4199) 2026-02-01 22:48:30 +11:00
correctmost 331634fd80
Measure install time with monotonic clock instead of system clock (#4198)
Using time.time() can be inaccurate if the system clock gets
updated in between calls.
2026-02-01 22:48:09 +11:00
correctmost 173c64c847
Avoid union syntax in an isinstance check (#4197)
This is a partial revert of commit 44f4bc86. The UP038 ruff rule
was removed and using union syntax is no longer recommended.
2026-02-01 22:47:39 +11:00
renovate[bot] 47a4caf163
Update astral-sh/ruff-action action to v3.6.1 (#4195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 09:40:51 +11:00
Daniel Girtler 449c1bfb36
Rename ask* functions to select* (#4191) 2026-01-31 08:17:11 +11:00
renovate[bot] e2ba6cbc54
Update astral-sh/ruff-action action to v3.6.0 (#4193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 21:47:52 +11:00
Federico Berrueta 97dd099c0e
Fix: Resolve fuzzy strings and add note for 'Seat access translation' (#4186) 2026-01-29 09:49:16 +11:00
HADEON 26014aa092
Add sponsors button (#4056)
* Add archlinux funding file

* Fix formatting of custom funding URL
2026-01-28 22:01:56 +01:00
nubprogrammer 6d89770e7a
Update Hindi translations for archinstall (#4185)
* Improve Hindi translations (base.po)

* Improve all Fuzzy and more hindi translations

* Update base.po

* Update base.po

* Update Hindi translations in base.po

* Update translation for Network Manager iwd backend
2026-01-29 07:56:59 +11:00
codefiles 2cb81d8758
Reuse MirrorListHandler instance (#4184) 2026-01-29 07:54:18 +11:00
Daniel Girtler 5612325dc3
Refactor code to reduce circular dep (#4175)
* Refactor to reduce circular dep

* Update

* Update
2026-01-28 13:43:57 +01:00
correctmost 8f104bc829
Remove archinstall.svg from project root (#4180)
The file was accidentally added in commit bde544ca.
2026-01-28 11:43:28 +11:00
codefiles 18105fff22
Use main() as script entrypoint (#4179) 2026-01-28 11:42:51 +11:00
codefiles 76284b601b
Remove superfluous __future__ import (#4178) 2026-01-28 07:23:25 +11:00
codefiles 8ffc20b6c1
Refactor script list (#4177) 2026-01-27 17:47:36 +01:00
Daniel Girtler bde544caca
Refactor syscommand (#4176)
* Refactor SysCommand

* Refactor syscommand
2026-01-27 13:31:33 +01:00
Daniel Girtler 316251f6e0
Refactor network config installer (#4143)
* Refactor network config installer

* Update

* Move network menu

* Remove singleton
2026-01-27 19:39:45 +11:00
Daniel Girtler 5fe42bd2f5
Refactor user menu (#4141) 2026-01-27 18:50:09 +11:00
correctmost 15460e4a88
Fix useless-return warnings reported by Pylint (#4173) 2026-01-27 16:40:38 +11:00
codefiles d7bcd431a7
Refactor application_handler to use DI (#4172) 2026-01-27 16:39:42 +11:00
codefiles ef3b6ab853
Refactor auth_handler to use dependency injection (#4171) 2026-01-27 12:46:31 +11:00
Daniel Girtler 2421e538a8
Move Pacman out of init (#4150)
* Move pacman out of init

* Update archinstall/lib/pacman/pacman.py

Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>

* Update archinstall/lib/pacman/pacman.py

Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>

---------

Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>
2026-01-27 12:34:48 +11:00
codefiles 28f7aec2af
Refactor mirror_list_handler to use DI (#4170) 2026-01-27 11:23:01 +11:00
Daniel Girtler a025c79ac3
Delete dead code (#4144) 2026-01-27 11:22:03 +11:00
nubprogrammer 93736349a2
Improve Hindi translations and resolve all fuzzy entries (#4167)
* Improve Hindi translations (base.po)

* Improve all Fuzzy and more hindi translations

* Update base.po

* Update base.po
2026-01-26 23:35:32 +01:00
Daniel Girtler 9bd16e998c
Fix - Handle cancel from list manger properly (#4160) 2026-01-26 23:34:58 +01:00
Daniel Girtler 7a0b4c2a30
Fix - disk partitioning menu (#4158) 2026-01-26 23:34:23 +01:00
Daniel Girtler 63e3825756
Remove obsolete package functions (#4145) 2026-01-26 23:33:21 +01:00
Daniel Girtler a57bb801c0
[README update] Clarify archinstall upgrade steps (#4138)
* Clarify upgrade steps on Live ISO

* Update

* Update

* Update

* Update README.md

Spelling fix

---------

Co-authored-by: Anton Hvornum <torxed@archlinux.org>
2026-01-26 23:30:27 +01:00
codefiles 2ed6182575
Refactor wifi_handler to use dependency injection (#4161) 2026-01-23 14:40:08 +11:00
renovate[bot] b07bbeecd3
Update pre-commit hook astral-sh/ruff-pre-commit to v0.14.14 (#4166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 13:13:08 +11:00
renovate[bot] 2c2b17b8a6
Update dependency ruff to v0.14.14 (#4165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 13:12:47 +11:00
nubprogrammer 7fb8fb57ee
Improve Hindi translations (base.po) (#4163) 2026-01-23 06:44:23 +11:00
renovate[bot] 3500bc3b41
Update actions/setup-python digest to a309ff8 (#4157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 18:32:02 +11:00
codefiles 192aff0b94
Replace sys.exit calls with return values (#4156) 2026-01-22 13:58:40 +11:00
codefiles 4d2864ba76
Refactor error message (#4153) 2026-01-21 16:39:49 +11:00
codefiles 78969c58c7
Move script entry point (#4151) 2026-01-21 12:10:19 +11:00
nubprogrammer 7cf1566902
Improve Hindi translations in base.po (#4152)
* Improve Hindi translations in base.po

This PR improves and fixes Hindi translations in base.po.
It also corrects previously incorrect fuzzy translations.
No functional code changes.

* Update base.po

* removed fuzzy from the things i fixed in translation
2026-01-21 12:08:43 +11:00
codefiles c7dbf0105e
Rework has_default_btrfs_vols (#4147) 2026-01-20 11:18:22 +11:00
codefiles 38462db2db
Enable GRUB UKI menu entries (#4139) 2026-01-19 15:46:02 +11:00
Daniel Girtler 0aca992ac5
Migrate UI to textual (#3997)
* Footer textual

* Linting

* Revert pre-commit for pylint

* Add missing textual

* Reinstate example

* Linting
2026-01-19 07:01:40 +11:00
codefiles 9a38b73baf
Enable quoted-annotation ruff rule and fixes (#4137) 2026-01-18 08:19:07 +11:00
codefiles a150a8d9f7
Remove quotes from Installer type annotations (#4136) 2026-01-17 18:52:18 +11:00
codefiles 7b45613996
Use Self for remaining occurrences (#4135) 2026-01-17 18:51:44 +11:00
renovate[bot] e1d9935f30
Update pre-commit hook astral-sh/ruff-pre-commit to v0.14.13 (#4133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 10:50:45 +11:00
renovate[bot] 65190a253e
Update dependency ruff to v0.14.13 (#4131)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 10:50:19 +11:00
Kenraaliskuutteri 7995d6e815
Updated Finnish translation in base.po (#4129)
Updated Finnish translations for various messages in base.po.
2026-01-16 06:36:36 +11:00
codefiles 2c154245cb
Use Self for Tui (#4130) 2026-01-16 06:36:11 +11:00
codefiles 2c85b5eab0
Remove storage module (#4128) 2026-01-15 14:09:35 +11:00
codefiles 972e278555
Remove leftover code from unattended script (#4127) 2026-01-15 12:18:47 +11:00
codefiles e5a14c0cfe
Use Self in tui (#4126) 2026-01-15 07:05:43 +11:00
Hannan c4de093122
Add header and update urdu translations (#4122)
* add header

* update urdu translations
2026-01-14 19:54:33 +11:00
codefiles 90d1b08628
Remove unused installation_hooks property (#4118) 2026-01-14 19:53:43 +11:00
codefiles 20460e1c29
Use Self in default_profiles (#4125) 2026-01-14 16:03:32 +11:00
codefiles 2e9d5e4829
Refactor Boot (#4124) 2026-01-14 13:29:22 +11:00
codefiles 5811f81e59
Use Self for parameters to dunder methods (#4123) 2026-01-14 13:24:56 +11:00
codefiles 8f7d59b718
Replace __enter__ return type with Self (#4121) 2026-01-13 15:45:21 +01:00
Mattyan89 b779345a5b
fixes (#4093) 2026-01-13 20:59:02 +11:00
codefiles f6eca309ac
Use sys.exit instead of exit (#4120) 2026-01-13 07:51:08 +11:00
codefiles 82512eed0e
Remove superfluous list creation in join calls (#4119) 2026-01-12 12:56:55 +11:00
codefiles db6c11345a
Remove superfluous __future__ imports (#4117) 2026-01-12 10:20:07 +11:00
codefiles 3374b47d50
Use Self for return instances of cls (#4116) 2026-01-12 09:06:19 +11:00
codefiles bde3b0ed6e
Change staticmethods to classmethods (#4115) 2026-01-12 07:17:21 +11:00
codefiles b1290672bb
Change classmethods to staticmethods (#4114) 2026-01-11 17:40:43 +11:00
codefiles 43509d8ce1
Use instance method for type_to_text (#4113) 2026-01-11 11:29:39 +11:00
codefiles efd57870d0
Use instance method for _load_config (#4112) 2026-01-11 11:29:01 +11:00
codefiles cb4b7e3db0
Update classmethods to use cls (#4110) 2026-01-11 10:29:32 +11:00
codefiles 01bee60cd1
Fix typos (#4109) 2026-01-11 10:28:53 +11:00
Tobias Stoeckmann e0c1b869a6
Fix typos in comments (#4107)
Typos found with codespell.

No functional change.
2026-01-10 10:17:50 +11:00
correctmost d07a9b4a92
Disable the non-empty-init-module ruff rule (#4106)
This allows ruff check --preview to pass again.
2026-01-10 10:17:00 +11:00
HADEON ef9f704761
hotfix: Firewall (#4100)
* Needed to have default files + enabled status

* Modify conf file directly

* nl

* more thnx to codefiles

* Fix previews in menu
2026-01-10 10:02:35 +11:00
Benjamin Smith 5fbe1deea4
Add missing spanish translations (#4098)
* added some missing Spanish translations

* added missing translations, fixed typos

* added a bunch of missing translations, fixed some too
2026-01-09 15:54:56 +11:00
renovate[bot] 7292a9e449
chore(deps): update dependency ruff to v0.14.11 (#4095)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 15:54:10 +11:00
renovate[bot] 85d38f063b
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.11 (#4096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 15:53:52 +11:00
HADEON 62edc56a52
hotfix: Revert Grub changes (#4099)
* Hotfix grub

* revert changes
2026-01-09 11:30:12 +11:00
correctmost 84c36adb1f
Avoid a reportPossiblyUnboundVariable warning with Pyright (#4092) 2026-01-07 18:49:07 +11:00
correctmost e2e158973f
Remove unused pytest-mock dependency (#4091) 2026-01-07 12:01:10 +11:00
correctmost ef3369b1c5
Add node_modules to .gitignore and Pylint's ignore list (#4089)
This makes it easier to use Pyright locally.
2026-01-07 11:54:54 +11:00
correctmost bb13139339
Fix an invalid-type-form error reported by ty (#4088)
The 'bytes' annotation for the 'bytes' property was actually a
forward reference to the property instead of builtins.bytes:
https://github.com/astral-sh/ty/issues/1747#issuecomment-3609042917
2026-01-07 11:54:15 +11:00
correctmost 0470495cb1
Migrate pytest configs to native TOML and enable strict mode (#4087) 2026-01-07 11:52:46 +11:00
correctmost 8e0ff2d2a9
Update comment in _final_warning (#4086)
Device paths are no longer mentioned as of adbadbf6.
2026-01-07 10:05:58 +11:00
correctmost cec29d123b
Use serialization TypedDicts to reduce Any instances (#4085) 2026-01-07 10:05:13 +11:00
Valerii f2e40742bb
Better Ukrainian v1.4 (#4084) 2026-01-07 09:57:35 +11:00
HADEON 0188459917
fix: bootloader changes (#4073)
* Bootloader changes:

	-> GRUB: Support for UKI
		- Disable 10_linux or breaks at kernel updates
		- Create 09_custom entry
	-> rEFInd:
		- Remove fallback entry similar to other bootloaders
		- With UKI still one dead entry can be hidden with DEL key
	-> All bootloaders:
		- Default to UKI on if supported, if using no UKI and /efi
		Causes systemd boot to not load, because it needs a XTLDRBOOT part
		Safer default for modern setups and simpler sec boot compat

* Add new models

* Modify based on grub-2.14-rc1 -> No need to use chainload
Thanks to codefiles for the heads-up

* Simplify has_uki_support

* Tab

* checks
2026-01-06 20:04:01 +11:00
HADEON cc6e247dcf
State libfido2 dependency (#4082) 2026-01-06 19:55:58 +11:00
HADEON 450664cdc4
feat: firewalls (#4074)
* Firewall Init

* Enable service
2026-01-05 20:04:43 +11:00
HADEON e590277e69
Another small readme change for clarity (#4077) 2026-01-05 12:35:42 +11:00
summoner 9488233a5a
Translation: Update hungarian po/mo (#4078)
Fix typos and mistakes
2026-01-05 12:35:08 +11:00
HADEON 146a2352bc
Change plasma-meta -> -desktop. (#4076)
This avoids to pull in sddm-kcm (to respect user's choice of login manager)
And to not pull in discover and related tools that should also be up to the user.
2026-01-04 19:37:58 +11:00
summoner 5eb3307efd
Translation: Update hu.po (#4071)
Translate new strings
fix typos
fix wording
2026-01-04 19:22:08 +11:00
HADEON d1a74edf9d
fix pre-commit hook (#4070) 2026-01-02 13:33:24 +11:00
walken fd143af05b
Czech localization update (#4068) 2026-01-02 13:29:11 +11:00
scrypt-kitty 4582d60f13
Do not mount btrfs partitions unless required, tested working and solves issue #3689 (#3992) 2026-01-02 13:27:53 +11:00
Luna Jernberg 7889a5417f
Update Arch Installer translation before Jan 2026 release (#4065)
* Update base.po

Update Swedish for January 2026 iso

* Update Swedish translations before release today

Updated Swedish translations for various messages in base.po.
2026-01-01 20:21:49 +11:00
Daniel Girtler bff715ddab
Add instructions for booting Arch ISO in a VM (#4041)
* Add instructions to boot ISO in VM

* Update

---------

Co-authored-by: Daniel Girtler <dgirtler@atlassian.com>
2026-01-01 10:10:24 +01:00
utuhiro78 051352e218
Update Japanese translation (#4064) 2026-01-01 10:14:31 +11:00
Abdullah Koyuncu 9433f15573
Update Turkish translation (#4062) 2026-01-01 10:13:28 +11:00
Matteo d5b554be1e
Update Italian translation (#4060)
- Translated new strings
- Improved some string
2026-01-01 10:12:37 +11:00
Anton Hvornum aabf6ae19e
Bumping version to: 3.0.15 (#4058)
* Bumping version to: 3.0.15

* ruff formatting
2025-12-31 13:02:10 +01:00
justbispo cb6fe6b34b
Add support for rEFInd boot manager (#3707)
* Add support for rEFInd boot manager

* Fix ruff formatting complaints

* Added support for different mountpoints for /efi and /boot

Also fixed issue where if /boot is located in a BTRFS root partition, the initrd path wasn't including the subvol name.

* Fix ruff formatting complaints

* Replace SysCommand with self.arch_chroot call

* Fix ruff formatting complaints

---------

Co-authored-by: Diogo Bispo <gpg.jta36@slmail.me>
2025-12-31 15:24:22 +11:00
Mariya 446d23c59d
feat(applications): add support for power-profiles-daemon/tuned as a power management daemon (#4015)
* fix(profiles): install power-profiles-daemon by default in the desktop
profile

* fix: only install power-profiles-daemon if a battery is detected

* chore: clean up has_battery method

* fix: make power management daemon a configurable application

* fix: make linter happy after merge

* fix: fix merge issues

* fix: give has_battery a return type to make linter happy

* chore: add locale msgids for power management related strings

* fix: changes requested in review

* fix: cache has_battery result

* fix: changes requested in review

* fix: just return none directly

* fix: add selected power management daemon to applications menu preview
2025-12-31 13:22:27 +11:00
HADEON 83c9bf06b2
Modify archinstall language display to be handled like other sections. (#4048)
* Modify archinstall language display to be handled like other sections.
This fixes a bug where language names that were too long would break curses menu.

* Revert types change
2025-12-31 10:46:46 +11:00
HADEON eb5e88f317
Mirrors sort #4046 (#4050)
* Mirrors sort #4046

* Change logic to be just sorted on bitrate
2025-12-30 15:20:45 +01:00
Vincent Dahmen 9f4f29bd29
lib/network: adds symlink to configure systemd-resovled properly (#4052)
* lib/network: adds symlink to configure systemd-resovled properly

* lib/network: adds overwrite mechanism to enforce resolved symlink

---------

Co-authored-by: Vincent Dahmen <wahrwolf@wolfpit.net>
2025-12-30 16:01:01 +11:00
HADEON 2954e4397b
Feat: Zram algorithm config (#4042)
* Add configuration for swap algorithm. Backward compatible implementation

* Fix interaction to default to Yes and show (default)

* Fix mypy error

* Any -> str, str

* feedback Enums

* test file

* line length warning

* Renames

* Fix default values in TUI menu for display

* Address feedback

* More feedback, really appreciate it.

* Adapt to use same | None = None pattern

* Pytests

* Add missing import for Zram
2025-12-30 15:52:35 +11:00
HADEON 747385a883
Lvm2/LUKS fixes/Mirror Logic (#4047)
* Lvm hotfix attempt

* Use --force and --yes flags

* Changed mirror behavior and more lvm testing

* Handle properly LvmOnLuks to only export in one fo the scenarios

* Idek

* Remove dead block

* Use -f flag and wipefs to remove any existing headers avoid "has signatures"

* oops

* Revert mirror change
2025-12-29 17:35:24 +01:00
HADEON ac984b7622
This commit changes the order of the main menu. (#4043)
Since in the next NVIDIA update, the kernel choice will have an impact on profiles.
The order this way makes it more logical: Bootloader => Kernel => Hw drivers
2025-12-28 22:20:17 +11:00
HADEON 4e52dc7493
README clarifications/corrections (#4038)
* A smaller readme tweak to stop answering the same questions :D

* rem
2025-12-28 11:28:40 +11:00
HADEON 79313c4942
Fix mirrors hang when /status endpoint is down (#4031)
* add explicit _fetched_remote bool

* Attempt 2

* Adds about 15 seconds time-out to fetch_data_from_url with 3 retries (4, 5, 6)
Then fallsback to fully local list

* Feedbacks: 20 -> 30
Do not return early
Add debug
Remove new flag
60 second timeout for reflector

* Clean up install logs by hiding mirror scores behind --verbose
2025-12-28 11:10:03 +11:00
HADEON c1eae10e93
Enable IWD to be used as back-end in network selection (#4025)
* feedback

* feedback2

* Refactor for less duplicate code and more conscise logic
Group NM types and handle configurations appropriatly
	- For IWD -> Copy from ISO + Disable standalone and configure back-end
	- For standard -> Install wpa_supplicant
	- For both install applet only when Desktop profile

Added comments for clearer logic

* Rem comments

* Rem copy to ISO

* the one commit to rule them all
2025-12-28 11:09:37 +11:00
Mintsuki 42a4ee8472
Make removable location the default for bootloader installation (#4030)
Also update the option's description to make it clear that it being enabled is the sane default.
2025-12-27 14:38:14 +01:00
Daniel Girtler 6968a33508
Fix kurdish translation files (#4004)
Co-authored-by: Daniel Girtler <dgirtler@atlassian.com>
2025-12-27 12:41:02 +01:00
utuhiro78 4d01d1dbbb
Update Japanese translation (#4040) 2025-12-27 22:37:11 +11:00
Daniel Girtler 7635474772
Explicitly allow selecting any additional repository (#3973)
Co-authored-by: Daniel Girtler <dgirtler@atlassian.com>
2025-12-27 09:25:15 +01:00
HADEON a4ad1b3724
Remove whitespace in timestamp (#4039) 2025-12-27 17:38:03 +11:00
HADEON 5fcea379b9
Fix LVM creation/ info (#4024)
* use pull and udev sync

* 30s -> 5min
2025-12-25 13:07:26 +11:00
HADEON e5ccdb0c1c
Adds a timer to post install screen (#4028)
* Add timer to end screen of guided

* ruff check
2025-12-25 11:06:03 +11:00
HADEON 9e7a5f6931
Do not install base-devel by default (#4022)
* replace base-devel with sudo and add disclaimer texts

* revert basepot and add rem comments
2025-12-25 11:00:34 +11:00
HADEON 9412f97771
Set up Zram dynamically based on best practice (#4027)
* Use the total available RAM / 2 for swap size dynamically.

* ws

* - Systems with 8 GB RAM or less will get 4096 MB zram
- Systems with more than 8 GB RAM will get half their RAM as zram

* Add debug print
2025-12-25 10:59:03 +11:00
Luis Antonio 1faac77c0d
Update Portuguese translations (pt and pt_BR) and metadata. (#4021)
* Revise Portuguese translations in base.po

Updated translations for various messages in Portuguese.

* Update Brazilian Portuguese translations in base.po

Updated translations and metadata in the Brazilian Portuguese locale file.

* Update Last-Translator in Brazilian Portuguese locale
2025-12-22 22:08:02 +11:00
HADEON 1227babd8c
Change LVM /root def to adapt dynamically (#4005)
* Change def size from 20 -> 32
Rest is still calculated from available - root.

* Use the existing process_root_partition_size():
For LVM just like regular disk best-effort.
This is only for single disk layouts.
2025-12-22 22:05:30 +11:00
Mariya ba5f924540
fix: enable the cosmic-greeter service (#4023) 2025-12-22 09:07:47 +11:00
HADEON f639290c20
ly.service -> ly@tty1.service (#4006)
* ly -> ly@tty1

* ly -> ly@tty1 with disable getty

* ws

* ws2

* ws3

* ws4

* conditionally disable and define
2025-12-21 21:37:06 +11:00
Luna Jernberg 043cd68a7c
Update base.po (#4020)
Update Swedish for January 2026 iso
2025-12-21 21:36:02 +11:00
Mariya 9876558f74
chore: cosmic is stable now, don't hide it behind --advanced flag (#4017)
* chore: cosmic is stable now, don't hide it behind --advanced flag

* fix: remove --advanced check in GreeterType

* chore: remove unused import to make linter happy
2025-12-21 14:45:51 +11:00
Mariya d7b559c67e
feat(applications): add CUPS installation support (#4013)
* feat(applications): add CUPS installation support

* fix: use translation for print service preview_action

* fix: incorrect action for print service menu item

* chore: refactor naming, printer -> print service

* fix: commit untracked file

* chore: fix formatting to make linter happy
2025-12-21 14:30:11 +11:00
Mariya cf31148e46
fix: fix fido warning message being spammed (#4016) 2025-12-21 14:16:06 +11:00
Mariya dee2a84057
fix: fix translation for bluetooth preview_action (#4014) 2025-12-21 13:57:29 +11:00
HADEON 17dc001857
Cache property of graphics_devices (#4007)
* On horrible hardware this makes it so that the "Graphics drivers" section loads directly.
By initializing it with launch instead of on the fly.

* Init after logs

* This cache the property of graphics drivers properly.
Instead of trying to hack early init.
2025-12-21 12:22:59 +11:00
renovate[bot] 6f768ea87c
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.10 (#4012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 18:36:04 +11:00
renovate[bot] e2cdf42690
chore(deps): update dependency ruff to v0.14.10 (#4011)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 18:35:50 +11:00
Odyssey b579773421
Update catalan locales (#4010) 2025-12-19 15:51:21 +11:00
renovate[bot] d13882fca1
chore(deps): update dependency pre-commit to v4.5.1 (#4003)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 15:50:37 +11:00
HADEON 6b23eff422
H2T (Host-to-target) installs and prevent host pollution (#3978)
* Add host-to-target (H2T) installation mode detection

- Add running_from_host() function to detect if running from installed system vs ISO
- Function checks for /run/archiso existence (ISO mode) vs host mode
- Add clear logging of installation mode on startup
- Skip keyboard layout changes when running from host system
- Fix Pyright type error in jsonify() by using Any instead of object
- Update README to mention installation from existing system

This enables archinstall to be run from an existing Arch installation
to perform host-to-target installs on other disks/partitions.

* match existing style

* rem debug

* info -> debug
2025-12-16 10:59:04 +11:00
renovate[bot] 810a50e46c
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.19.1 (#3998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 08:26:52 +11:00
renovate[bot] 582c54f4e2
chore(deps): update dependency mypy to v1.19.1 (#3996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 16:54:57 +11:00
CYAXXX bc738a48be
Add Kurdish translation (#3994)
* Add Kurdish translation

* Update base.po
2025-12-15 16:34:12 +11:00
CYAXXX 0a6fd6a07e
Delete archinstall/locales/ku (#3995) 2025-12-15 14:23:48 +11:00
scrypt-kitty 20718ead79
Remove /tmp/archlive before building. Otherwise latest changes are not built. (#3993) 2025-12-15 11:23:01 +11:00
CYAXXX fff783ed93
Add Kurdish language (#3991)
* Add Kurdish language

This pull request introduces full Kurdish language integration into archinstall, allowing Kurdish speaking users to navigate and use the installer entirely in their native language.

* Fix translation for timezone selection prompt
2025-12-15 11:20:51 +11:00
renovate[bot] f65cc5f6c7
chore(deps): update actions/upload-artifact action to v6 (#3985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 11:18:54 +11:00
Jesper Otten cffe47a369
Adding new Dutch translations (#3987) 2025-12-15 11:14:26 +11:00
renovate[bot] 0117205f73
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.9 (#3983)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 11:12:21 +11:00
renovate[bot] 1f1daf1afc
chore(deps): update dependency ruff to v0.14.9 (#3982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 11:11:46 +11:00
walken c9b0b171b3
Czech localization update (#3980) 2025-12-10 22:46:58 +11:00
HADEON 326232098a
Full disclosure for bootloaders/keymaps (#3979)
* Clarify LUKS2 encryption and keyboard layout context in locales README

* ws

* clarity

* final
2025-12-10 22:46:21 +11:00
HADEON 6b50815eb6
Vconsole.conf KEYMAP= FONT=
_base mkinitcpio.conf v40 error (#3928)

* pr1

* pr2

* pr2-2

* pr2-3

* Revert genfstab and pacstrap command changes

* mistake
2025-12-08 19:25:20 +11:00
renovate[bot] 57bd613679
chore(deps): update dependency pytest to v9.0.2 (#3972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-07 17:07:39 +11:00
renovate[bot] 8872f98c8d
chore(deps): update dependency mypy to v1.19.0 (#3956)
* chore(deps): update dependency mypy to v1.19.0

* Update

* Update

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Girtler <dgirtler@atlassian.com>
2025-12-06 21:40:37 +11:00
renovate[bot] 601e033188
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.8 (#3969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 20:23:52 +11:00
Gabriel A Hernandez 07ab6bf4a0
Menu now filters items and sorts using priority, improving UX. (#3967)
* Menu now filters and sorts using priority, improving UX.

* Refactor: improve logic, removed redundancy

* Refactor: improve logic, removed redundancy

* Improved logic when getting view items in menuItems

* Improved logic when getting view items in menuItems
2025-12-06 20:10:01 +11:00
Gabriel A Hernandez 7398e2785d
Typo and grammar fixes (#3970)
* Menu now filters and sorts using priority, improving UX.

* Refactor: improve logic, removed redundancy

* Refactor: improve logic, removed redundancy

* Typo and grammar fixes

* Typo and grammar fixes

* Fix comment formatting in .gitlab-ci.yml

* Fix comment

* Removed code from separate pull request

* Update menu_item.py

* removed white space

* Remove unnecessary blank lines in menu_item.py

>:(
2025-12-06 20:06:07 +11:00
Mintsuki d176b9514c
Remove GRUB removable fallback path (#3971)
As mentioned by @svartkanin on #3950, given we now have a way for the user to
explicitly specify if they want to install to the removable location, having a
fallback like this seems undesirable.

On top of that, as mentioned by @correctmost on the same PR, the code that said
PR introduced was bugged and would always raise an exception anyways.
2025-12-05 20:05:24 +11:00
renovate[bot] 66fb49617a
chore(deps): update dependency ruff to v0.14.8 (#3968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-05 20:03:05 +11:00
renovate[bot] dbfa60bfa8
chore(deps): update actions/checkout digest to 8e8c483 (#3965)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-05 20:02:24 +11:00
Abdullah Koyuncu e7442bf5c6
Translation: Update the Turkish base.po/base.mo (#3964) 2025-12-03 12:26:09 +11:00
renovate[bot] 627530fd82
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.19.0 (#3959)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 12:24:06 +01:00
renovate[bot] 94f22fcc55
chore(deps): update dependency pylint to v4.0.4 (#3962)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:58:26 +11:00
summoner 8387c8f589
Translation: Update hungarian base.po/base.mo (#3961)
Translate new strings
Fix translation
2025-12-01 18:57:59 +11:00
Anton Hvornum 3290175084
Bumping version to: 3.0.14 (#3960) 2025-11-29 22:19:59 +01:00
renovate[bot] 0221099398
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.7 (#3958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-29 11:55:23 +11:00
renovate[bot] 29cb454fda
chore(deps): update dependency ruff to v0.14.7 (#3957)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-29 11:55:05 +11:00
Mintsuki 6fc0bc3671
Fix GRUB fallback-from-removable logic introduced in c095eb56d8 (#3950)
Commit c095eb56d8 was supposed to introduce logic
such that if the `grub-install` command failed with a `--removable` flag, then
another attempt would be made with such flag removed.

This was broken because the `--removable` flag was kept in both cases (likely a
copy-paste mistake). This has been an issue since, in all future iterations of
the code.

What this commit does is fix this logic, but also invert the cases tested:
first test without `--removable`, then add it should that case fail, as this is
the most sensible thing to do.
2025-11-29 11:10:04 +11:00
Gabriel A Hernandez 7732d50016
Improved regex in _validate_value() that checks user input for partition value and unit. (#3952)
- Allows for white space in between groups, aligning better with displayed example.
- Removed unneeded | symbol, which was checking as literal rather than working as "or %"
2025-11-29 11:04:39 +11:00
Mintsuki d7e5dc3692
Do not unconditionally set `prompt` to `None` in list_manager.py (#3955)
May address issue #3954.
2025-11-29 11:04:12 +11:00
Mintsuki 70a6c3499a
Do not create BLS and Limine entries for fallback initramfs (#3949)
* Do not create BLS and Limine entries for fallback initramfs

Fallback initramfs seem to no longer be built by default.

* Remove initramfs variant logic altogether
2025-11-29 11:00:43 +11:00
renovate[bot] b751ad5dab
chore(deps): update dependency pydantic to v2.12.5 (#3951)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 21:23:38 +11:00
renovate[bot] 147447face
chore(deps): update actions/setup-python digest to 83679a8 (#3946)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 10:31:55 +11:00
renovate[bot] 60302060e3
chore(deps): update dependency pre-commit to v4.5.0 (#3944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 09:33:20 +11:00
correctmost 5f0742a87a
Use dummy variables instead of unused-awaitable suppressions (#3943) 2025-11-22 21:16:11 +11:00
correctmost 73ceb0c99d
Use ClassVar to avoid mutable-class-default (RUF012) warnings (#3942) 2025-11-22 21:15:41 +11:00
renovate[bot] 2ee66a059e
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.6 (#3941)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 08:36:34 +11:00
renovate[bot] a94d94a8e1
chore(deps): update dependency ruff to v0.14.6 (#3940)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 08:36:14 +11:00
Mintsuki eb815d817f
Add dialog to install EFI bootloader to removable location (#3932)
* Add dialog to install EFI bootloader to removable location

This is just for GRUB and Limine for now.

* Move bootloader removable and UKI selections to bootloader submenu

* Update ask_for_bootloader_removable() prompt for ease of translation

* Fix issue where removable and UKI options were always enabled at first

* Minor cosmetic fixes to bootloader removable code

* Add has_removable_support to Bootloader

* Validate UKI and removable options in installer

* Use has_removable_support() where appropriate

* Fix potential AttributeError when bootloader_config is None

* Set default value for bootloader configuration menu item

* Update documentation after EFI removable/Limine changes

* Update limine.conf and non-removable location paths (as per Wiki)

* Do not create fallback boot menu entries when using UKIs on Limine

* Remove useless ask_* wrappers in bootloader_menu

* Improve bootloader menu previews

* Make bootloader menu __init__.py empty
2025-11-21 11:41:24 +11:00
renovate[bot] 840c1c0f7e
chore(deps): update actions/checkout action to v6 (#3939)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-21 07:41:57 +11:00
renovate[bot] bf3784fc0c
chore(deps): update actions/checkout digest to 93cb6ef (#3937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 21:14:16 +11:00
utuhiro78 4c8fe60a28
Update Japanese translation (#3938) 2025-11-20 21:13:51 +11:00
HADEON 46072132a1
Readme Tweak (#3929)
* pr1

* pr2

* pr3

* pr3-2

* pr3-3

* pr3-4

* pr4

* Revert hardware.py to original state on snapshots branch

* readme

* Revert installer.py to original state on readme branch

* match base branch

* Revert genfstab and pacstrap command changes

* readme tweaks2
2025-11-17 08:50:35 +11:00
HADEON c3aba06a01
Fix f-string in snapshot debug installation (#3934)
* pr1

* pr2

* pr3

* pr3-2

* pr3-3

* pr3-4

* pr4

* Revert hardware.py to original state on snapshots branch

* Revert genfstab and pacstrap command changes

* f string
2025-11-17 06:51:35 +11:00
HADEON 375d64a600
Snapshots Fix Snapper-Grub (#3930)
* pr1

* pr2

* pr3

* pr3-2

* pr3-3

* pr3-4

* pr4

* Revert hardware.py to original state on snapshots branch

* Revert genfstab and pacstrap command changes
2025-11-16 21:40:38 +11:00
HADEON 6bdb756650
Add --needed to pacstrap to prevent re-installs 2025-11-16 21:30:47 +11:00
renovate[bot] 803ff4236e
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.5 (#3927)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 12:54:53 +11:00
renovate[bot] 117b9d9a5d
chore(deps): update dependency pylint to v4.0.3 (#3923)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 11:03:01 +11:00
Benjamin Smith e6bbd05121
added some missing Spanish translations (#3925) 2025-11-15 10:40:42 +11:00
renovate[bot] 3da27570f7
chore(deps): update dependency pytest to v9.0.1 (#3922)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 10:38:53 +11:00
renovate[bot] 566dcb51c3
chore(deps): update dependency pytest to v9 (#3912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 08:36:02 +01:00
renovate[bot] b77f03e94a
chore(deps): update dependency pre-commit to v4.4.0 (#3911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-09 13:55:03 +11:00
Luna Jernberg 811c1d95b5
Update Swedish translations for wifi (#3910)
Update Swedish translations for wifi
2025-11-09 13:54:40 +11:00
Matteo 165636b2e6
Update Italian translation (#3904)
- Translated new strings
2025-11-07 20:56:41 +01:00
summoner e7553c114c
Translation: Update hungarian base.po/base.mo (#3908)
Translate new strings
2025-11-07 15:45:32 +01:00
renovate[bot] b2fccb0b3b
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.4 (#3907)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 09:09:59 +01:00
renovate[bot] 83aab9cd4f
chore(deps): update dependency ruff to v0.14.4 (#3906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 09:09:23 +01:00
damachine a996670a60
Fix plural form in device selection message (#3905)
Corrected the plural form in the German translation for device selection.
2025-11-07 09:03:12 +01:00
renovate[bot] 240919826f
chore(deps): update dependency pydantic to v2.12.4 (#3900)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 20:23:08 +00:00
Anton Hvornum c131269c08
Bumping version to: 3.0.13 (#3903) 2025-11-06 20:02:49 +01:00
Anton Hvornum a3d85c1c58
Fixed an issue where re-writing a smaller mkinitcpio.conf than previously existed, the 'no' part at the end became a trailing 'o' causing syntax issues. (#3902) 2025-11-06 15:26:44 +01:00
Rachel bf8f72cc54
Add missing --script option in documentation for pre-programmed scripts (#3895) 2025-11-03 11:21:26 +11:00
summoner 996566291d
Translation: Update Hungarian transaltion (#3894)
* Translation: Update Hungarian transaltion

Translate new string
Fixing translation

* Translation: Fixing typos in hungarian translation

Translation: Fixing typos and wording in hungarian translation
2025-11-02 10:50:33 +01:00
Luna Jernberg 2808442f8b
Update with November 2025 Swedish translations (#3893)
Update with November 2025 Swedish translations
2025-11-01 14:44:55 +01:00
Daniel Girtler 76ab9482e9
Wifi connection menu with textual (#3879)
* Wifi connector

* Update

---------

Co-authored-by: Daniel Girtler <dgirtler@atlassian.com>
2025-11-01 13:55:58 +01:00
Anton Hvornum 7af94c8fe5
Bumping version to: 3.0.12 (#3892) 2025-11-01 13:08:48 +01:00
Anton Hvornum c2d3daeb25
Added the new -S flag for arch-chroot (#3891)
* Added the new -S flag for arch-chroot which does: Run in systemd mode.

* Fixed some formatting issues, and removed unused *args and **kwargs for run_command()

* Formatting issue

* Formatting issue
2025-11-01 12:48:20 +01:00
Klaus Zipfel 1174800cca
Skip bootloader config check when 'No Bootloader' is selected as bootloader (#3875)
* Allow installation via TUI when 'No Bootloader' is selected as bootloader (--skip-boot)

* Update global_menu.py

Fixed ruff formatting issue

---------

Co-authored-by: Anton Hvornum <anton.feeds+github@gmail.com>
2025-11-01 11:59:24 +01:00
Daniel Girtler adbadbf606
Fix 3863 (#3880)
* Modify formatting warning text to be less scary

* Update

---------

Co-authored-by: Daniel Girtler <dgirtler@atlassian.com>
2025-11-01 11:55:54 +01:00
Tertle950 ed67e9fd67
replace 'leafpad' with 'l3afpad' (#3890)
seemed like the most obvious replacement but maybe another one would suffice
2025-11-01 11:55:02 +01:00
Daniel Girtler bf6245815b
Save and load UKI setting (#3783)
Co-authored-by: Daniel Girtler <dgirtler@atlassian.com>
2025-11-01 11:54:31 +01:00
Daniel Girtler 4ceddacb4e
Only install applications when enabled (#3861)
Co-authored-by: Daniel Girtler <dgirtler@atlassian.com>
2025-11-01 11:54:09 +01:00
renovate[bot] 00c90d9e45
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.3 (#3888)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-01 21:46:16 +11:00
renovate[bot] a03e9bcdc6
chore(deps): update dependency ruff to v0.14.3 (#3887)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-01 21:45:56 +11:00
renovate[bot] 552c7b7dd9
chore(deps): update actions/upload-artifact action to v5 (#3884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-25 16:21:30 +11:00
renovate[bot] 772652bc69
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.2 (#3883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-25 15:38:12 +11:00
renovate[bot] b0679974d5
chore(deps): update dependency ruff to v0.14.2 (#3882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-25 15:37:52 +11:00
renovate[bot] 90bf24b3ce
chore(deps): update dependency pylint to v4.0.2 (#3877)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 18:22:59 +11:00
renovate[bot] e0ea072032
chore(deps): update dependency pydantic to v2.12.3 (#3874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-18 13:04:32 +11:00
correctmost 1fa8f2013b
Remove an unneeded Pylint suppression (#3873)
The related bug has been fixed in Pylint 4.0.1.
2025-10-18 13:04:09 +11:00
renovate[bot] 6505c073c9
chore(deps): update dependency pylint to v4.0.1 (#3864)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 17:13:45 +11:00
renovate[bot] a84a97fdc7
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.1 (#3872)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 15:31:20 +11:00
renovate[bot] 0a032b04d5
chore(deps): update dependency ruff to v0.14.1 (#3871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 15:30:59 +11:00
renovate[bot] 46380f5da3
chore(deps): update dependency pydantic to v2.12.2 (#3869)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 07:34:22 +11:00
renovate[bot] 6c43a8c6d3
chore(deps): update dependency pydantic to v2.12.1 (#3868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 16:34:18 +11:00
Vasiliy Stelmachenok 53e8cb3a8c
Do not install Btrfs module and binary in mkinitcpio (#3865)
* Do not install Btrfs module and binary in mkinitcpio

This is what btrfs hook already does.

Signed-off-by: Vasiliy Stelmachenok <ventureo@cachyos.org>

* Remove unused properties from FilesystemType

They were only needed for Btrfs

Signed-off-by: Vasiliy Stelmachenok <ventureo@cachyos.org>

---------

Signed-off-by: Vasiliy Stelmachenok <ventureo@cachyos.org>
2025-10-14 07:33:45 +11:00
Vasiliy Stelmachenok daab8e6ff6
Do not re-order amdgpu and radeon modules in mkinitcpio (#3866)
This is not necessary as kms hook for mkinitcpio already takes care
of adding amdgpu and radeon modules.

Signed-off-by: Vasiliy Stelmachenok <ventureo@cachyos.org>
2025-10-13 09:12:41 +00:00
correctmost 8fb83a7beb
Upgrade Pylint to 4.0.0 (#3867)
This commit also removes the pylint-pydantic plug-in, which is not
yet compatible with Pylint v4.
2025-10-13 09:03:00 +00:00
renovate[bot] b0b87400c2
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.0 (#3859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 07:58:29 +00:00
renovate[bot] 97790145c4
chore(deps): update dependency ruff to v0.14.0 (#3858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 16:48:13 +11:00
renovate[bot] d7f74b4b32
chore(deps): update dependency pydantic to v2.12.0 (#3856)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 16:47:57 +11:00
Muhammadyoqub 472ffd4e6a
Add Uzbek (uz) language support (#3854)
* Add Uzbek (uz) language support

* Add Uzbek (uz) language support
2025-10-07 21:11:42 +11:00
Pierre Ambroise 68d8af3df3
Update french translation (#3853)
* Update french translation

* Add mo file
2025-10-07 09:56:48 +11:00
renovate[bot] 88266f5c74
chore(deps): update dependency pylint to v3.3.9 (#3851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 09:19:56 +11:00
renovate[bot] a3e219a2e4
chore(deps): update dependency pydantic to v2.11.10 (#3850)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-05 09:31:15 +11:00
renovate[bot] 5777880d18
chore(deps): update dependency ruff to v0.13.3 (#3848)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 10:25:20 +00:00
renovate[bot] c8429aee23
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.13.3 (#3849)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 10:25:01 +00:00
damachine 60e70f507f
Fix German translations for consistency and clarity (#3844)
* Fix German translations for consistency and clarity

Corrected capitalization and punctuation in German translations.

* Update German translations for disk configuration messages

sounds better

* Update German translations for clarity and consistency

* Update German translations in base.po

* Update German translation for object selection message

much more understandable

* Update German translations in base.po

friendlier

* Refine German translations in base.po

SORRY for the many changes. I only want the best.

* Update German translation for Btrfs subvolumes

* Update German translation for btrfs subvolume message

correct translation o)
2025-10-01 07:25:11 +10:00
Valerii b03948bbfe
Better Ukrainian v1.3.4 (#3843) 2025-10-01 07:24:41 +10:00
damachine 21f9972040
Fix typo in German translation for network interface (#3842)
sorry. another one has sneaked in.
2025-09-30 13:04:02 +10:00
damachine e0ce0bdd45
Fix typo in German translation for bootable partition (#3841)
small typo fix o)
2025-09-30 11:56:43 +10:00
summoner c86b0be79f
Translation: Update hu_HU.po (#3839)
Translate new strings
Fixing translation
2025-09-28 17:50:02 +10:00
renovate[bot] 853309d311
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.13.2 (#3838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-28 17:49:24 +10:00
renovate[bot] 2f1b8cd692
chore(deps): update dependency ruff to v0.13.2 (#3837)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-28 17:49:06 +10:00
Luna Jernberg e1f7a314a7
Bumping version to: 3.0.11 (archlinux#3835) (Swedish Translation) (#3836)
* Update base.po (Swedish)

Update Swedish Translation

* Update base.po (Swedish)

Updated Swedish translation
Uppdaterad svensk översättning

* Bumping version to: 3.0.11 (archlinux#3835)

Bumping version to: 3.0.11 (archlinux#3835)
2025-09-25 17:20:43 +10:00
Anton Hvornum ebeb4c7daa
Bumping version to: 3.0.11 (#3835) 2025-09-24 23:51:08 +02:00
Matteo 88753b1f40
Update Italian translation (#3832)
Translated new strings
2025-09-24 07:14:37 +10:00
utuhiro78 ca85f39b71
Update Japanese translation (#3831) 2025-09-24 07:14:20 +10:00
renovate[bot] 5f96390719
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.18.2 (#3823)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 20:38:59 +10:00
renovate[bot] fceee34dfa
chore(deps): update dependency ruff to v0.13.1 (#3810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 17:43:37 +10:00
renovate[bot] 49ead6fa38
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.13.1 (#3811)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 16:44:30 +10:00
Bruno Rosa 792db9e8b8
quick fix to run archinstall and enable systemd services (#3815) 2025-09-19 15:52:30 +10:00
renovate[bot] df3b5d7593
chore(deps): update dependency mypy to v1.18.2 (#3816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 15:45:43 +10:00
Daniel Girtler d256e70480
Fix 3785 - F2FS parameters require 'extra_attr' (#3796)
Co-authored-by: Daniel Girtler <dgirtler@atlassian.com>
2025-09-17 10:32:04 +02:00
Monochrome de05dcb7e0
changed version of cryptography package to 45.0.7 (#3795)
Co-authored-by: QuotientParadoxx <117741113+QuotientParadox@users.noreply.github.com>
2025-09-16 10:25:43 +00:00
Rémy Marquis 9ddd41da56
Remove outdated link to video demo installer (#3790) 2025-09-16 10:25:24 +00:00
renovate[bot] cec68b6e9b
chore(deps): update dependency pydantic to v2.11.9 (#3791)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 08:38:27 +10:00
renovate[bot] c60d9bd232
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.18.1 (#3792)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 08:37:45 +10:00
renovate[bot] ad02f5bffc
chore(deps): update dependency mypy to v1.18.1 (#3788)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 07:29:10 +10:00
renovate[bot] 3c416943e2
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.13.0 (#3787)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 07:28:46 +10:00
renovate[bot] e846a0ec58
chore(deps): update dependency ruff to v0.13.0 (#3786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 07:28:30 +10:00
BlocksumoGuys d40d77ce25
Finalized French Translation (with improvments) (#3784)
The french translation is hopefully finished, I also improved in some sectors of the translations
2025-09-09 17:03:48 +10:00
veprogames de50a31609
Fundamentally improve German translations (#3780)
* Get rid of all warnings that poedit displayed

Bring translations closer to the English original, also removing some extraneous information. Get rid of all warnings poedit displayed. Correct translation that didn't match at all.

* Fix locales_generator.sh detection

xgettext was not recognizing tr() invocations. Following https://stackoverflow.com/a/11901925 fixed the issue

* Add more German translations

Improve consistency with some translations. Add translations for messages that were just detected in the previous commit. Add translations for Graphics Drivers

* Add more translations

Look for untranslated strings in the source files and add make them recognized by gettext

* Improve conistency of German translations and correct typos

* formatting

* Remove translations from enum members

* More translation tweaks
2025-09-09 17:02:24 +10:00
Dee 1ef52f56cb
gracefully return "undefined" if DMI is not in sysfs (#3771)
* gracefully return "undefined" if DMI is not in sysfs

fixes #3770, in theory

* None works too and is consistent with other behaviour

* None doesn't *just* work
2025-09-08 10:51:28 +00:00
BlocksumoGuys bca3f4b660
Update French Language inside of base.po (#3782)
Some error found inside the base.po file
2025-09-08 19:06:20 +10:00
renovate[bot] 9bb7ced10e
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.12.12 (#3779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 21:34:33 +10:00
renovate[bot] b8baf096dd
chore(deps): update dependency ruff to v0.12.12 (#3778)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 21:34:11 +10:00
renovate[bot] 0ad513a8e5
chore(deps): update dependency pytest to v8.4.2 (#3777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 21:33:54 +10:00
renovate[bot] ed8b6ac045
chore(deps): update actions/setup-python action to v6 (#3776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 18:39:12 +10:00
veprogames fd2c20a900
Add missing German translations (#3774) 2025-09-04 16:51:11 +10:00
265 changed files with 38654 additions and 11099 deletions

4
.github/FUNDING.yml vendored Normal file
View File

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

View File

@ -41,8 +41,8 @@ body:
attributes: attributes:
value: > value: >
**Note**: Assuming you have network connectivity, **Note**: Assuming you have network connectivity,
you can easily post the installation log using the following command: you can easily upload the installation log and get a shareable URL by running:
`curl -F'file=@/var/log/archinstall/install.log' https://0x0.st` `archinstall share-log`
- type: textarea - type: textarea
id: freeform id: freeform

View File

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

View File

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

View File

@ -21,8 +21,8 @@ jobs:
image: archlinux/archlinux:latest image: archlinux/archlinux:latest
options: --privileged options: --privileged
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
- name: Install pre-dependencies - name: Install pre-dependencies
run: | run: |
pacman -Sy --noconfirm tree git python-pyparted python-setuptools python-sphinx python-sphinx_rtd_theme python-build python-installer python-wheel 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 image: archlinux/archlinux:latest
options: --privileged options: --privileged
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- run: pwd - run: pwd
- run: find . - run: find .
- run: cat /etc/os-release - run: cat /etc/os-release
- run: pacman-key --init - run: pacman-key --init
- run: pacman --noconfirm -Sy archlinux-keyring - run: pacman --noconfirm -Sy archlinux-keyring
- run: ./build_iso.sh - run: ./test_tooling/mkarchiso/build_iso.sh
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: with:
name: Arch Live ISO name: Arch Live ISO
path: /tmp/archlive/out/*.iso path: /tmp/archlive/out/*.iso

View File

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

View File

@ -6,7 +6,7 @@ jobs:
container: container:
image: archlinux/archlinux:latest image: archlinux/archlinux:latest
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Prepare arch - name: Prepare arch
run: | run: |
pacman-key --init pacman-key --init
@ -14,7 +14,7 @@ jobs:
pacman --noconfirm -Syyu pacman --noconfirm -Syyu
pacman --noconfirm -Sy python-pip python-pyparted pkgconfig gcc pacman --noconfirm -Sy python-pip python-pyparted pkgconfig gcc
- run: pip install --break-system-packages --upgrade pip - run: pip install --break-system-packages --upgrade pip
- name: Install Pylint and Pylint plug-ins - name: Install Pylint
run: pip install --break-system-packages .[dev] run: pip install --break-system-packages .[dev]
- run: python --version - run: python --version
- run: pylint --version - run: pylint --version

View File

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

View File

@ -11,14 +11,14 @@ jobs:
image: archlinux/archlinux:latest image: archlinux/archlinux:latest
options: --privileged options: --privileged
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Prepare arch - name: Prepare arch
run: | run: |
pacman-key --init pacman-key --init
pacman --noconfirm -Sy archlinux-keyring pacman --noconfirm -Sy archlinux-keyring
pacman --noconfirm -Syyu pacman --noconfirm -Syyu
pacman --noconfirm -Sy python-uv python-setuptools python-pip pacman --noconfirm -Sy python-uv python-setuptools python-pip
pacman --noconfirm -Sy python-pyparted python-pydantic pacman --noconfirm -Sy python-pyparted python-pydantic python-textual
- name: Remove existing archinstall (if any) - name: Remove existing archinstall (if any)
run: run:
uv pip uninstall archinstall --break-system-packages --system uv pip uninstall archinstall --break-system-packages --system
@ -33,7 +33,7 @@ jobs:
archinstall --script guided -v archinstall --script guided -v
archinstall --script only_hd -v archinstall --script only_hd -v
archinstall --script minimal -v archinstall --script minimal -v
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: with:
name: archinstall name: archinstall
path: dist/* path: dist/*

View File

@ -18,13 +18,13 @@ jobs:
image: archlinux/archlinux:latest image: archlinux/archlinux:latest
options: --privileged options: --privileged
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Prepare arch - name: Prepare arch
run: | run: |
pacman-key --init pacman-key --init
pacman --noconfirm -Sy archlinux-keyring pacman --noconfirm -Sy archlinux-keyring
pacman --noconfirm -Syyu pacman --noconfirm -Syyu
pacman --noconfirm -Sy python python-uv python-setuptools python-pip python-pyparted python-pydantic pacman --noconfirm -Sy python python-uv python-setuptools python-pip python-pyparted python-pydantic python-textual
- name: Build archinstall - name: Build archinstall
run: | run: |
uv build --no-build-isolation --wheel uv build --no-build-isolation --wheel

View File

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

View File

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

View File

@ -1,28 +1,22 @@
#on: name: Translation validation
# push: on:
# paths: push:
# - 'archinstall/locales/**' paths:
# pull_request: - 'archinstall/**/*.py'
# paths: - 'archinstall/locales/**'
# - 'archinstall/locales/**' - '.github/workflows/translation-check.yaml'
#name: Verify local_generate script was run on translation changes pull_request:
#jobs: paths:
# translation-check: - 'archinstall/**/*.py'
# runs-on: ubuntu-latest - 'archinstall/locales/**'
# container: - '.github/workflows/translation-check.yaml'
# image: archlinux/archlinux:latest jobs:
# steps: translations:
# - uses: actions/checkout@v4 name: Validate translations
# - run: pacman --noconfirm -Syu python git diffutils runs-on: ubuntu-latest
# - name: Verify all translation scripts are up to date steps:
# run: | - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# cd .. - name: Install gettext
# cp -r archinstall archinstall_orig run: sudo apt-get update && sudo apt-get install -y gettext
# cd archinstall/archinstall/locales - name: Run translation checks
# bash locales_generator.sh 1> /dev/null run: bash archinstall/locales/locales_generator.sh check
# 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)

40
.github/workflows/uki-build.yaml vendored Normal file
View File

@ -0,0 +1,40 @@
# 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,4 +39,9 @@ requirements.txt
/.gitconfig /.gitconfig
/actions-runner /actions-runner
/cmd_output.txt /cmd_output.txt
node_modules/
uv.lock 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 - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
# We currently do not have unit tests implemented but this stage is written in anticipation of their future usage. # We currently do not have unit tests implemented but this stage is written in anticipation of their future usage.
# When a stage name is preceeded 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 preceded with a '.' it's treated as "disabled" by GitLab and is not executed, so it's fine for it to be declared.
.pytest: .pytest:
stage: test stage: test
tags: tags:

View File

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

View File

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

103
README.md
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) [![Lint Python and Find Syntax Errors](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml/badge.svg)](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml)
Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist. Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist.
The installer also doubles as a python library to install Arch Linux and manage services, packages, and other things inside the installed system *(Usually from a live medium)*. 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)*.
* archinstall [discord](https://discord.gg/aDeMffrxNg) server * archinstall [discord](https://discord.gg/aDeMffrxNg) server
* archinstall [#archinstall:matrix.org](https://matrix.to/#/#archinstall:matrix.org) Matrix channel * archinstall [#archinstall:matrix.org](https://matrix.to/#/#archinstall:matrix.org) Matrix channel
@ -14,28 +14,53 @@ The installer also doubles as a python library to install Arch Linux and manage
* archinstall [documentation](https://archinstall.archlinux.page/) * archinstall [documentation](https://archinstall.archlinux.page/)
# Installation & Usage # Installation & Usage
> [!TIP]
> In the ISO you are root by default. Use sudo if running from an existing system.
```shell ```shell
sudo pacman -S archinstall pacman-key --init
``` pacman -Sy archinstall
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`:
```shell
archinstall archinstall
``` ```
## Running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer using `git` 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 ```shell
# cd archinstall-git pacman -Syu
# python -m archinstall ```
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
## Running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer
Assuming you are on an Arch Linux live-ISO or installed via `pip`, `archinstall` will use the `guided` script by default
```shell
archinstall
```
similar goes for running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer using `git
```shell
git clone https://github.com/archlinux/archinstall
cd archinstall
python -m archinstall $@
```
To run alternative scripts using the `--script` parameter
```
archinstall --script <name>
``` ```
#### Advanced #### Advanced
Some additional options that most users do not need are hidden behind the `--advanced` flag. Some additional options that most users do not need are hidden behind the `--advanced` flag and all options/args can be consulted through `-h` or `--help`.
## Running from a declarative configuration file or URL ## Running from a declarative configuration file or URL
@ -57,7 +82,7 @@ archinstall --config <path to user config file or URL> --creds <path to user cre
``` ```
### Credentials configuration file encryption ### 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. 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. However, when selecting to save configuration files, `archinstall` will prompt for the option to encrypt the `user_credentials.json` file content.
@ -70,15 +95,15 @@ there are multiple ways to provide the decryption key:
# Help or Issues # 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. [discord](https://discord.gg/aDeMffrxNg) help channel.
When submitting an issue, please: When submitting an issue, please:
* Provide the stacktrace of the output if applicable * Provide the stacktrace of the output if applicable
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you! * Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
* To extract the log from the ISO image, one way is to use<br> * To upload the log from the ISO image and get a shareable URL, run<br>
```shell ```shell
curl -F'file=@/var/log/archinstall/install.log' https://0x0.st archinstall share-log
``` ```
@ -133,12 +158,6 @@ 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, 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. 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 1. You need a working network connection
2. Install the build requirements with `pacman -Sy; pacman -S git python-pip gcc pkgconf` 2. Install the build requirements with `pacman -Sy; pacman -S git python-pip gcc pkgconf`
*(note that this may or may not work depending on your RAM and current state of the squashfs maximum filesystem free space)* *(note that this may or may not work depending on your RAM and current state of the squashfs maximum filesystem free space)*
@ -157,10 +176,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: This can be done by installing `pacman -S arch-install-scripts util-linux` locally and doing the following:
# truncate -s 20G testimage.img # truncate -s 20G testimage.img
# losetup --partscan --show --find ./testimage.img # losetup --partscan --show ./testimage.img
# pip install --upgrade archinstall # pip install --upgrade archinstall
# python -m archinstall --script guided # python -m archinstall --script guided
# qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.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/edk2/x64/OVMF_CODE.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.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> This will create a *20 GB* `testimage.img` and create a loop device which we can use to format and install to.<br>
`archinstall` is installed and executed in [guided mode](#docs-todo). Once the installation is complete, ~~you can use qemu/kvm to boot the test media.~~<br> `archinstall` is installed and executed in [guided mode](#docs-todo). Once the installation is complete, ~~you can use qemu/kvm to boot the test media.~~<br>
@ -169,9 +188,41 @@ This will create a *20 GB* `testimage.img` and create a loop device which we can
There's also a [Building and Testing](https://github.com/archlinux/archinstall/wiki/Building-and-Testing) guide.<br> There's also a [Building and Testing](https://github.com/archlinux/archinstall/wiki/Building-and-Testing) guide.<br>
It will go through everything from packaging, building and running *(with qemu)* the installer against a dev branch. It will go through everything from packaging, building and running *(with qemu)* the installer against a dev branch.
## Boot an Arch ISO image in a VM
You may want to boot an ISO image in a VM to test `archinstall` in there.
* Download the latest [Arch ISO](https://archlinux.org/download/)
* Use the the below command to boot the ISO in a VM
```
qemu-system-x86_64 -enable-kvm \
-machine q35,accel=kvm -device intel-iommu \
-cpu host -m 4096 -boot order=d \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd \
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw
```
HINT: For espeakup support
```
qemu-system-x86_64 -enable-kvm \
-machine q35,accel=kvm -device intel-iommu \
-cpu host -m 4096 -boot order=d \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd \
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw \
-device intel-hda -device hda-duplex,audiodev=snd0 \
-audiodev pa,id=snd0,server=/run/user/1000/pulse/native
```
# FAQ # 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 ## Keyring out-of-date
For a description of the problem see https://archinstall.archlinux.page/help/known_issues.html#keyring-is-out-of-date-2213 and discussion in issue https://github.com/archlinux/archinstall/issues/2213. For a 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,143 +1,3 @@
"""Arch Linux installer - guided, templates etc.""" from archinstall.lib.plugins import plugin
import importlib __all__ = ['plugin']
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.packages.packages import check_package_upgrade
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 _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_new_version() -> 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)
time.sleep(3)
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()
if not arch_config_handler.args.offline:
_fetch_arch_db()
if not arch_config_handler.args.skip_version_check:
_check_new_version()
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,4 +1,6 @@
import archinstall import sys
from archinstall.main import main
if __name__ == '__main__': if __name__ == '__main__':
archinstall.run_as_a_module() sys.exit(main())

View File

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

View File

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

View File

@ -0,0 +1,52 @@
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

@ -0,0 +1,14 @@
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

@ -0,0 +1,49 @@
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

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,22 @@
from typing import override from typing import override
from archinstall.default_profiles.profile import GreeterType, ProfileType from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.xorg import XorgProfile from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class BspwmProfile(XorgProfile): class BspwmProfile(Profile):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__('Bspwm', ProfileType.WindowMgr) super().__init__(
'Bspwm',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property @property
@override @override
def packages(self) -> list[str]: def packages(self) -> list[str]:
# return super().packages + [
return [ return [
'bspwm', 'bspwm',
'sxhkd', 'sxhkd',
@ -24,3 +29,11 @@ class BspwmProfile(XorgProfile):
@override @override
def default_greeter_type(self) -> GreeterType: def default_greeter_type(self) -> GreeterType:
return GreeterType.Lightdm 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,12 +1,16 @@
from typing import override from typing import override
from archinstall.default_profiles.profile import GreeterType, ProfileType from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class BudgieProfile(XorgProfile): class BudgieProfile(Profile):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__('Budgie', ProfileType.DesktopEnv) super().__init__(
'Budgie',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
@property @property
@override @override
@ -16,6 +20,7 @@ class BudgieProfile(XorgProfile):
'budgie', 'budgie',
'mate-terminal', 'mate-terminal',
'nemo', 'nemo',
'nemo-fileroller',
'papirus-icon-theme', 'papirus-icon-theme',
] ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,19 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops import SeatAccess from archinstall.default_profiles.desktops.utils import select_seat_access
from archinstall.default_profiles.profile import GreeterType, ProfileType from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, 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(XorgProfile): class HyprlandProfile(Profile):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__('Hyprland', ProfileType.DesktopEnv) super().__init__(
'Hyprland',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
self.custom_settings = {'seat_access': None} self.custom_settings = {CustomSetting.SeatAccess: None}
@property @property
@override @override
@ -42,33 +41,12 @@ class HyprlandProfile(XorgProfile):
@property @property
@override @override
def services(self) -> list[str]: def services(self) -> list[str]:
if pref := self.custom_settings.get('seat_access', None): if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
return [pref] return [pref]
return [] 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 @override
def do_on_select(self) -> None: async def do_on_select(self) -> None:
self._ask_seat_access() default = self.custom_settings.get(CustomSetting.SeatAccess, None)
return None seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

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

View File

@ -1,29 +1,25 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops import SeatAccess from archinstall.default_profiles.desktops.utils import select_seat_access
from archinstall.default_profiles.profile import GreeterType, ProfileType from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, 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(XorgProfile): class LabwcProfile(Profile):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
'Labwc', 'Labwc',
ProfileType.WindowMgr, ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
) )
self.custom_settings = {'seat_access': None} self.custom_settings = {CustomSetting.SeatAccess: None}
@property @property
@override @override
def packages(self) -> list[str]: def packages(self) -> list[str]:
additional = [] additional = []
if seat := self.custom_settings.get('seat_access', None): if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
additional = [seat] additional = [seat]
return [ return [
@ -39,33 +35,12 @@ class LabwcProfile(XorgProfile):
@property @property
@override @override
def services(self) -> list[str]: def services(self) -> list[str]:
if pref := self.custom_settings.get('seat_access', None): if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
return [pref] return [pref]
return [] 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 @override
def do_on_select(self) -> None: async def do_on_select(self) -> None:
self._ask_seat_access() default = self.custom_settings.get(CustomSetting.SeatAccess, None)
return None seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

@ -1,12 +1,16 @@
from typing import override from typing import override
from archinstall.default_profiles.profile import GreeterType, ProfileType from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.xorg import XorgProfile
class LxqtProfile(XorgProfile): class LxqtProfile(Profile):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__('Lxqt', ProfileType.DesktopEnv) super().__init__(
'Lxqt',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
# NOTE: SDDM is the only officially supported greeter for LXQt, so unlike other DEs, lightdm is not used here. # 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. # LXQt works with lightdm, but since this is not supported, we will not default to this.
@ -20,7 +24,7 @@ class LxqtProfile(XorgProfile):
'oxygen-icons', 'oxygen-icons',
'xdg-utils', 'xdg-utils',
'ttf-freefont', 'ttf-freefont',
'leafpad', 'l3afpad',
'slock', 'slock',
] ]

View File

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

View File

@ -1,29 +1,25 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops import SeatAccess from archinstall.default_profiles.desktops.utils import select_seat_access
from archinstall.default_profiles.profile import GreeterType, ProfileType from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, 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(XorgProfile): class NiriProfile(Profile):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
'Niri', 'niri',
ProfileType.WindowMgr, ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
) )
self.custom_settings = {'seat_access': None} self.custom_settings = {CustomSetting.SeatAccess: None}
@property @property
@override @override
def packages(self) -> list[str]: def packages(self) -> list[str]:
additional = [] additional = []
if seat := self.custom_settings.get('seat_access', None): if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
additional = [seat] additional = [seat]
return [ return [
@ -47,33 +43,12 @@ class NiriProfile(XorgProfile):
@property @property
@override @override
def services(self) -> list[str]: def services(self) -> list[str]:
if pref := self.custom_settings.get('seat_access', None): if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
return [pref] return [pref]
return [] 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 @override
def do_on_select(self) -> None: async def do_on_select(self) -> None:
self._ask_seat_access() default = self.custom_settings.get(CustomSetting.SeatAccess, None)
return None seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

@ -0,0 +1,66 @@
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

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

View File

@ -0,0 +1,221 @@
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

@ -0,0 +1,39 @@
// ! 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

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

View File

@ -0,0 +1,22 @@
// ! 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

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

View File

@ -0,0 +1,279 @@
// 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,26 +1,120 @@
from enum import StrEnum
from typing import override from typing import override
from archinstall.default_profiles.profile import GreeterType, ProfileType from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
from archinstall.default_profiles.xorg import XorgProfile from archinstall.lib.menu.helpers import Selection
from archinstall.lib.packages.packages import available_package, package_group_info
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class PlasmaProfile(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):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__('KDE Plasma', ProfileType.DesktopEnv) super().__init__(
'KDE Plasma',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
@property @property
@override @override
def packages(self) -> list[str]: def packages(self) -> list[str]:
return [ flavor_str = self.custom_settings.get(CustomSetting.PlasmaFlavor)
'plasma-meta',
'konsole', if flavor_str is not None:
'kate', flavor = PlasmaFlavor(flavor_str)
'dolphin', return flavor.packages()
'ark', else:
'plasma-workspace', return PlasmaFlavor.Meta.packages() # use plasma-meta as the recommended default
]
@property @property
@override @override
def default_greeter_type(self) -> GreeterType: def default_greeter_type(self) -> GreeterType:
return GreeterType.Sddm return GreeterType.PlasmaLoginManager
async def _select_flavor(self) -> None:
header = tr('Select a flavor of KDE Plasma to install') + '\n'
items = [
MenuItem(
s.show(),
value=s,
preview_action=lambda x: x.value.package_details() if x.value else None,
)
for s in PlasmaFlavor
]
group = MenuItemGroup(items, sort_items=False)
default = self.custom_settings.get(CustomSetting.PlasmaFlavor, None)
group.set_default_by_value(default)
result = await Selection[PlasmaFlavor](
group,
header=header,
allow_skip=False,
preview_location='right',
).show()
if result.type_ == ResultType.Selection:
self.custom_settings[CustomSetting.PlasmaFlavor] = result.get_value().value
@override
async def do_on_select(self) -> None:
await self._select_flavor()

View File

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

View File

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

View File

@ -1,29 +1,25 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops import SeatAccess from archinstall.default_profiles.desktops.utils import select_seat_access
from archinstall.default_profiles.profile import GreeterType, ProfileType from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, 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(XorgProfile): class SwayProfile(Profile):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
'Sway', 'Sway',
ProfileType.WindowMgr, ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
) )
self.custom_settings = {'seat_access': None} self.custom_settings = {CustomSetting.SeatAccess: None}
@property @property
@override @override
def packages(self) -> list[str]: def packages(self) -> list[str]:
additional = [] additional = []
if seat := self.custom_settings.get('seat_access', None): if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
additional = [seat] additional = [seat]
return [ return [
@ -49,33 +45,12 @@ class SwayProfile(XorgProfile):
@property @property
@override @override
def services(self) -> list[str]: def services(self) -> list[str]:
if pref := self.custom_settings.get('seat_access', None): if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
return [pref] return [pref]
return [] 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 @override
def do_on_select(self) -> None: async def do_on_select(self) -> None:
self._ask_seat_access() default = self.custom_settings.get(CustomSetting.SeatAccess, None)
return None seat_access = await select_seat_access(self.name, default)
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value

View File

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

View File

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

View File

@ -1,13 +1,16 @@
from __future__ import annotations from enum import Enum, StrEnum, auto
from typing import TYPE_CHECKING, Self
import sys
from enum import Enum, auto
from typing import TYPE_CHECKING
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
if TYPE_CHECKING: if TYPE_CHECKING:
from ..lib.installer import Installer from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class DisplayServerType(Enum):
Xorg = 'Xorg'
Wayland = 'Wayland'
class ProfileType(Enum): class ProfileType(Enum):
@ -23,7 +26,6 @@ class ProfileType(Enum):
DesktopEnv = 'Desktop Environment' DesktopEnv = 'Desktop Environment'
CustomType = 'CustomType' CustomType = 'CustomType'
# special things # special things
Tailored = 'Tailored'
Application = 'Application' Application = 'Application'
@ -33,10 +35,9 @@ class GreeterType(Enum):
Sddm = 'sddm' Sddm = 'sddm'
Gdm = 'gdm' Gdm = 'gdm'
Ly = 'ly' Ly = 'ly'
# .. todo:: Remove when we un-hide cosmic behind --advanced
if '--advanced' in sys.argv:
CosmicSession = 'cosmic-greeter' CosmicSession = 'cosmic-greeter'
PlasmaLoginManager = 'plasma-login-manager'
GreetdDms = 'dms-greeter'
class SelectResult(Enum): class SelectResult(Enum):
@ -45,25 +46,30 @@ class SelectResult(Enum):
ResetCurrent = auto() ResetCurrent = auto()
class CustomSetting(StrEnum):
SeatAccess = 'seat_access'
PlasmaFlavor = 'plasma_flavor'
class Profile: class Profile:
def __init__( def __init__(
self, self,
name: str, name: str,
profile_type: ProfileType, profile_type: ProfileType,
current_selection: list[Profile] = [], current_selection: list[Self] = [],
packages: list[str] = [], packages: list[str] = [],
services: list[str] = [], services: list[str] = [],
support_gfx_driver: bool = False, support_gfx_driver: bool = False,
support_greeter: bool = False, support_greeter: bool = False,
advanced: bool = False, display_server: DisplayServerType | None = None,
) -> None: ) -> None:
self.name = name self.name = name
self.profile_type = profile_type self.profile_type = profile_type
self.custom_settings: dict[str, str | None] = {} self.custom_settings: dict[CustomSetting, str | None] = {}
self.advanced = advanced
self._support_gfx_driver = support_gfx_driver self._support_gfx_driver = support_gfx_driver
self._support_greeter = support_greeter self._support_greeter = support_greeter
self._display_server = display_server
# self.gfx_driver: str | None = None # self.gfx_driver: str | None = None
@ -97,40 +103,38 @@ class Profile:
""" """
return None return None
def _advanced_check(self) -> bool: def install(self, install_session: Installer) -> None:
"""
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 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 Hook that will be called when the installation process is
finished and custom installation steps for specific default_profiles finished and custom installation steps for specific default_profiles
are needed 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]: def json(self) -> dict[str, str]:
""" """
Returns a json representation of the profile Returns a json representation of the profile
""" """
return {} return {}
def do_on_select(self) -> SelectResult | None: async def do_on_select(self) -> SelectResult | None:
""" """
Hook that will be called when a profile is selected Hook that will be called when a profile is selected
""" """
return SelectResult.NewSelection return SelectResult.NewSelection
def set_custom_settings(self, settings: dict[str, str | None]) -> None: def set_custom_settings(self, settings: dict[CustomSetting, str | None]) -> None:
""" """
Set the custom settings for the profile. Set the custom settings for the profile.
This is also called when the settings are parsed from the config This is also called when the settings are parsed from the config
@ -151,19 +155,16 @@ class Profile:
return self.profile_type in top_levels return self.profile_type in top_levels
def is_desktop_profile(self) -> bool: def is_desktop_profile(self) -> bool:
return self.profile_type == ProfileType.Desktop if self._advanced_check() else False return self.profile_type == ProfileType.Desktop
def is_server_type_profile(self) -> bool: def is_server_type_profile(self) -> bool:
return self.profile_type == ProfileType.ServerType return self.profile_type == ProfileType.ServerType
def is_desktop_type_profile(self) -> bool: def is_desktop_type_profile(self) -> bool:
return (self.profile_type == ProfileType.DesktopEnv or self.profile_type == ProfileType.WindowMgr) if self._advanced_check() else False return self.profile_type == ProfileType.DesktopEnv or self.profile_type == ProfileType.WindowMgr
def is_xorg_type_profile(self) -> bool: def is_xorg_type_profile(self) -> bool:
return self.profile_type == ProfileType.Xorg if self._advanced_check() else False return self.profile_type == ProfileType.Xorg
def is_tailored(self) -> bool:
return self.profile_type == ProfileType.Tailored
def is_custom_type_profile(self) -> bool: def is_custom_type_profile(self) -> bool:
return self.profile_type == ProfileType.CustomType return self.profile_type == ProfileType.CustomType
@ -179,10 +180,23 @@ class Profile:
def is_greeter_supported(self) -> bool: def is_greeter_supported(self) -> bool:
return self._support_greeter return self._support_greeter
@property
def display_server(self) -> DisplayServerType | None:
return self._display_server
def preview_text(self) -> str: def preview_text(self) -> str:
""" """
Override this method to provide a preview text for the profile 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() return self.packages_text()
def packages_text(self, include_sub_packages: bool = False) -> str: def packages_text(self, include_sub_packages: bool = False) -> str:
@ -199,6 +213,6 @@ class Profile:
text = tr('Installed packages') + ':\n' text = tr('Installed packages') + ':\n'
for pkg in sorted(packages): for pkg in sorted(packages):
text += f'\t- {pkg}\n' text += f' - {pkg}\n'
return text return text

View File

@ -1,19 +1,19 @@
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, Self, override
from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult
from archinstall.lib.output import info from archinstall.lib.log import info
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.profile.profiles_handler import profile_handler 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.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.result import ResultType
from archinstall.tui.types import FrameProperties, PreviewStyle
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class ServerProfile(Profile): class ServerProfile(Profile):
def __init__(self, current_value: list[Profile] = []): def __init__(self, current_value: list[Self] = []):
super().__init__( super().__init__(
'Server', 'Server',
ProfileType.Server, ProfileType.Server,
@ -21,12 +21,12 @@ class ServerProfile(Profile):
) )
@override @override
def do_on_select(self) -> SelectResult: async def do_on_select(self) -> SelectResult:
items = [ items = [
MenuItem( MenuItem(
p.name, p.name,
value=p, value=p,
preview_action=lambda x: x.value.preview_text(), preview_action=lambda x: x.value.preview_text() if x.value else None,
) )
for p in profile_handler.get_server_profiles() for p in profile_handler.get_server_profiles()
] ]
@ -34,15 +34,13 @@ class ServerProfile(Profile):
group = MenuItemGroup(items, sort_items=True) group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(self.current_selection) group.set_selected_by_value(self.current_selection)
result = SelectMenu[Profile]( result = await Selection[Self](
group, group,
allow_reset=True, allow_reset=True,
allow_skip=True, allow_skip=True,
preview_style=PreviewStyle.RIGHT,
preview_size='auto',
preview_frame=FrameProperties.max('Info'),
multi=True, multi=True,
).run() preview_location='right',
).show()
match result.type_: match result.type_:
case ResultType.Selection: case ResultType.Selection:
@ -55,12 +53,17 @@ class ServerProfile(Profile):
return SelectResult.ResetCurrent return SelectResult.ResetCurrent
@override @override
def post_install(self, install_session: 'Installer') -> None: def provision(self, install_session: Installer, users: list[User]) -> None:
for profile in self.current_selection:
profile.provision(install_session, users)
@override
def post_install(self, install_session: Installer) -> None:
for profile in self.current_selection: for profile in self.current_selection:
profile.post_install(install_session) profile.post_install(install_session)
@override @override
def install(self, install_session: 'Installer') -> None: def install(self, install_session: Installer) -> None:
server_info = self.current_selection_names() server_info = self.current_selection_names()
details = ', '.join(server_info) details = ', '.join(server_info)
info(f'Now installing the selected servers: {details}') info(f'Now installing the selected servers: {details}')

View File

@ -4,6 +4,7 @@ from archinstall.default_profiles.profile import Profile, ProfileType
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class DockerProfile(Profile): class DockerProfile(Profile):
@ -24,9 +25,6 @@ class DockerProfile(Profile):
return ['docker'] return ['docker']
@override @override
def post_install(self, install_session: 'Installer') -> None: def provision(self, install_session: Installer, users: list[User]) -> None:
from archinstall.lib.args import arch_config_handler for user in users:
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}') install_session.arch_chroot(f'usermod -a -G docker {user.username}')

View File

@ -24,5 +24,5 @@ class MariadbProfile(Profile):
return ['mariadb'] return ['mariadb']
@override @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') 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'] return ['postgresql']
@override @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') install_session.arch_chroot('initdb -D /var/lib/postgres/data', run_as='postgres')

View File

@ -1,22 +0,0 @@
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,7 +1,9 @@
from typing import override from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import Profile, ProfileType from archinstall.default_profiles.profile import DisplayServerType, Profile, ProfileType
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class XorgProfile(Profile): class XorgProfile(Profile):
@ -9,26 +11,22 @@ class XorgProfile(Profile):
self, self,
name: str = 'Xorg', name: str = 'Xorg',
profile_type: ProfileType = ProfileType.Xorg, profile_type: ProfileType = ProfileType.Xorg,
advanced: bool = False,
): ):
super().__init__( super().__init__(
name, name,
profile_type, profile_type,
support_gfx_driver=True, support_gfx_driver=True,
advanced=advanced, display_server=DisplayServerType.Xorg,
) )
@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 @property
@override @override
def packages(self) -> list[str]: def packages(self) -> list[str]:
return [ return [
'xorg-server', 'xorg-server',
'xorg-xinit',
] ]
@override
def install(self, install_session: Installer) -> None:
install_session.add_additional_packages(self.packages)

View File

@ -2,6 +2,11 @@ from typing import TYPE_CHECKING
from archinstall.applications.audio import AudioApp from archinstall.applications.audio import AudioApp
from archinstall.applications.bluetooth import BluetoothApp 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.application import ApplicationConfiguration
from archinstall.lib.models.users import User from archinstall.lib.models.users import User
@ -13,16 +18,34 @@ class ApplicationHandler:
def __init__(self) -> None: def __init__(self) -> None:
pass 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: if app_config.bluetooth_config and app_config.bluetooth_config.enabled:
BluetoothApp().install(install_session) BluetoothApp().install(install_session)
if app_config.audio_config: if app_config.audio_config and app_config.audio_config.audio != Audio.NO_AUDIO:
AudioApp().install( AudioApp().install(
install_session, install_session,
app_config.audio_config, app_config.audio_config,
users, users,
) )
if app_config.power_management_config:
PowerManagementApp().install(
install_session,
app_config.power_management_config,
)
application_handler = ApplicationHandler() if app_config.print_service_config and app_config.print_service_config.enabled:
PrintServiceApp().install(install_session)
if app_config.firewall_config:
FirewallApp().install(
install_session,
app_config.firewall_config,
)
if app_config.fonts_config:
FontsApp().install(
install_session,
app_config.fonts_config,
)

View File

@ -1,12 +1,24 @@
from typing import override from typing import override
from archinstall.lib.hardware import SysInfo
from archinstall.lib.menu.abstract_menu import AbstractSubMenu from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.models.application import ApplicationConfiguration, Audio, AudioConfiguration, BluetoothConfiguration from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.application import (
ApplicationConfiguration,
Audio,
AudioConfiguration,
BluetoothConfiguration,
Firewall,
FirewallConfiguration,
FontPackage,
FontsConfiguration,
PowerManagement,
PowerManagementConfiguration,
PrintServiceConfiguration,
)
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, Orientation
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]): class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
@ -19,8 +31,8 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
else: else:
self._app_config = ApplicationConfiguration() self._app_config = ApplicationConfiguration()
menu_optioons = self._define_menu_options() menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True) self._item_group = MenuItemGroup(menu_options, checkmarks=True)
super().__init__( super().__init__(
self._item_group, self._item_group,
@ -29,8 +41,8 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
) )
@override @override
def run(self, additional_title: str | None = None) -> ApplicationConfiguration: async def show(self) -> ApplicationConfiguration | None:
super().run(additional_title=additional_title) _ = await super().show()
return self._app_config return self._app_config
def _define_menu_options(self) -> list[MenuItem]: def _define_menu_options(self) -> list[MenuItem]:
@ -48,13 +60,45 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
preview_action=self._prev_audio, preview_action=self._prev_audio,
key='audio_config', 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: def _prev_bluetooth(self, item: MenuItem) -> str | None:
if item.value is not None: if item.value is not None:
bluetooth_config: BluetoothConfiguration = item.value bluetooth_config: BluetoothConfiguration = item.value
output = 'Bluetooth: ' output = f'{tr("Bluetooth")}: '
output += tr('Enabled') if bluetooth_config.enabled else tr('Disabled') output += tr('Enabled') if bluetooth_config.enabled else tr('Disabled')
return output return output
return None return None
@ -65,48 +109,101 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
return f'{tr("Audio")}: {config.audio.value}' return f'{tr("Audio")}: {config.audio.value}'
return None return None
def _prev_print_service(self, item: MenuItem) -> str | None:
if item.value is not None:
print_service_config: PrintServiceConfiguration = item.value
def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None: output = f'{tr("Print service")}: '
group = MenuItemGroup.yes_no() output += tr('Enabled') if print_service_config.enabled else tr('Disabled')
group.focus_item = MenuItem.no() return output
return None
if preset is not None: def _prev_firewall(self, item: MenuItem) -> str | None:
group.set_selected_by_value(preset.enabled) if item.value is not None:
config: FirewallConfiguration = item.value
return f'{tr("Firewall")}: {config.firewall.value}'
return None
header = tr('Would you like to configure Bluetooth?') + '\n' 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
result = SelectMenu[bool](
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, group,
header=header,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
allow_skip=True, allow_skip=True,
).run() allow_reset=True,
).show()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return PowerManagementConfiguration(power_management=result.get_value())
case ResultType.Reset:
return None
async def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None:
header = tr('Would you like to configure Bluetooth?') + '\n'
preset_val = preset.enabled if preset else False
result = await Confirmation(
header=header,
allow_skip=True,
preset=preset_val,
).show()
match result.type_: match result.type_:
case ResultType.Selection: case ResultType.Selection:
enabled = result.item() == MenuItem.yes() return BluetoothConfiguration(result.get_value())
return BluetoothConfiguration(enabled)
case ResultType.Skip: case ResultType.Skip:
return preset return preset
case _: case _:
raise ValueError('Unhandled result type') raise ValueError('Unhandled result type')
def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration | None: async def select_print_service(preset: PrintServiceConfiguration | None) -> PrintServiceConfiguration | None:
header = tr('Would you like to configure the print service?') + '\n'
preset_val = preset.enabled if preset else False
result = await Confirmation(
header=header,
allow_skip=True,
preset=preset_val,
).show()
match result.type_:
case ResultType.Selection:
result.get_value()
return PrintServiceConfiguration(result.get_value())
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled result type')
async def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration | None:
items = [MenuItem(a.value, value=a) for a in Audio] items = [MenuItem(a.value, value=a) for a in Audio]
group = MenuItemGroup(items) group = MenuItemGroup(items)
if preset: if preset:
group.set_focus_by_value(preset.audio) group.set_focus_by_value(preset.audio)
result = SelectMenu[Audio]( result = await Selection[Audio](
group, group,
header=tr('Select audio configuration'),
allow_skip=True, allow_skip=True,
alignment=Alignment.CENTER, ).show()
frame=FrameProperties.min(tr('Audio')),
).run()
match result.type_: match result.type_:
case ResultType.Skip: case ResultType.Skip:
@ -115,3 +212,52 @@ def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration
return AudioConfiguration(audio=result.get_value()) return AudioConfiguration(audio=result.get_value())
case ResultType.Reset: case ResultType.Reset:
raise ValueError('Unhandled result type') 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,33 +1,42 @@
import argparse import argparse
import json import json
import os import os
import sys
import urllib.error import urllib.error
import urllib.parse import urllib.parse
from argparse import ArgumentParser, Namespace from argparse import ArgumentParser, Namespace
from dataclasses import dataclass, field from dataclasses import dataclass, field
from importlib.metadata import version from enum import Enum, StrEnum
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Self
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from pydantic.dataclasses import dataclass as p_dataclass from pydantic.dataclasses import dataclass as p_dataclass
from archinstall.lib.crypt import decrypt from archinstall.lib.crypt import decrypt
from archinstall.lib.models.application import ApplicationConfiguration from archinstall.lib.log import debug, error, logger, warn
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration
from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration
from archinstall.lib.models.network import NetworkConfiguration 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.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.models.users import Password, User, UserSerialization 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.plugins import load_plugin
from archinstall.lib.translationhandler import Language, tr, translation_handler from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.util import get_password from archinstall.lib.version import get_version
from archinstall.tui.curses_menu import Tui from archinstall.tui.components import tui
class SubCommand(Enum):
SHARE_LOG = 'share-log'
@p_dataclass @p_dataclass
@ -49,9 +58,87 @@ class Arguments:
no_pkg_lookups: bool = False no_pkg_lookups: bool = False
plugin: str | None = None plugin: str | None = None
skip_version_check: bool = False skip_version_check: bool = False
skip_wifi_check: bool = False
advanced: bool = False advanced: bool = False
verbose: 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 @dataclass
class ArchConfig: class ArchConfig:
@ -63,76 +150,101 @@ class ArchConfig:
profile_config: ProfileConfiguration | None = None profile_config: ProfileConfiguration | None = None
mirror_config: MirrorConfiguration | None = None mirror_config: MirrorConfiguration | None = None
network_config: NetworkConfiguration | None = None network_config: NetworkConfiguration | None = None
bootloader: Bootloader | None = None bootloader_config: BootloaderConfiguration | None = None
uki: bool = False
app_config: ApplicationConfiguration | None = None app_config: ApplicationConfiguration | None = None
auth_config: AuthenticationConfiguration | None = None auth_config: AuthenticationConfiguration | None = None
swap: ZramConfiguration | None = None
hostname: str = 'archlinux' hostname: str = 'archlinux'
kernels: list[str] = field(default_factory=lambda: ['linux']) kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
ntp: bool = True ntp: bool = True
packages: list[str] = field(default_factory=list) packages: list[str] = field(default_factory=list)
parallel_downloads: int = 0 pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
swap: bool = True
timezone: str = 'UTC' timezone: str = 'UTC'
services: list[str] = field(default_factory=list) services: list[str] = field(default_factory=list)
custom_commands: list[str] = field(default_factory=list) custom_commands: list[str] = field(default_factory=list)
def unsafe_json(self) -> dict[str, Any]: def unsafe_config(self) -> dict[ArchConfigType, Any]:
config: dict[str, list[UserSerialization] | str | None] = {} config: dict[ArchConfigType, list[UserSerialization] | str | None] = {}
if self.auth_config: if self.auth_config:
if self.auth_config.users: if self.auth_config.users:
config['users'] = [user.json() for user in self.auth_config.users] config[ArchConfigType.USERS] = [user.json() for user in self.auth_config.users]
if self.auth_config.root_enc_password: if self.auth_config.root_enc_password:
config['root_enc_password'] = self.auth_config.root_enc_password.enc_password config[ArchConfigType.ROOT_ENC_PASSWORD] = self.auth_config.root_enc_password.enc_password
if self.disk_config: if self.disk_config:
disk_encryption = self.disk_config.disk_encryption disk_encryption = self.disk_config.disk_encryption
if disk_encryption and disk_encryption.encryption_password: if disk_encryption and disk_encryption.encryption_password:
config['encryption_password'] = disk_encryption.encryption_password.plaintext config[ArchConfigType.ENCRYPTION_PASSWORD] = disk_encryption.encryption_password.plaintext
return config return config
def safe_json(self) -> dict[str, Any]: def safe_config(self) -> dict[ArchConfigType, Any]:
config: Any = { base_config: dict[ArchConfigType, Any] = {
'version': self.version, ArchConfigType.VERSION: self.version,
'script': self.script, ArchConfigType.SCRIPT: self.script,
'archinstall-language': self.archinstall_language.json(), ArchConfigType.ARCHINSTALL_LANGUAGE: self.archinstall_language.json(),
'hostname': self.hostname,
'kernels': self.kernels,
'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,
} }
if self.locale_config: base_config.update(self.plain_cfg())
config['locale_config'] = self.locale_config.json() sub_config = self.sub_cfg()
if self.disk_config: for config_type, value in sub_config.items():
config['disk_config'] = self.disk_config.json() if not hasattr(value, 'json'):
raise ValueError(f'Config value for {config_type} must implement json() method')
base_config[config_type] = value.json()
if self.profile_config: return base_config
config['profile_config'] = self.profile_config.json()
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: if self.mirror_config:
config['mirror_config'] = self.mirror_config.json() cfg[ArchConfigType.MIRROR_CONFIG] = self.mirror_config
if self.bootloader_config:
cfg[ArchConfigType.BOOTLOADER_CONFIG] = self.bootloader_config
if self.disk_config:
cfg[ArchConfigType.DISK_CONFIG] = self.disk_config
if self.swap:
cfg[ArchConfigType.SWAP] = self.swap
if self.auth_config:
cfg[ArchConfigType.AUTH_CONFIG] = self.auth_config
if self.locale_config:
cfg[ArchConfigType.LOCALE_CONFIG] = self.locale_config
if self.profile_config:
cfg[ArchConfigType.PROFILE_CONFIG] = self.profile_config
if self.network_config: if self.network_config:
config['network_config'] = self.network_config.json() cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config
return config if self.app_config:
cfg[ArchConfigType.APP_CONFIG] = self.app_config
return cfg
@classmethod @classmethod
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> 'ArchConfig': def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
arch_config = ArchConfig() arch_config = cls()
arch_config.locale_config = LocaleConfiguration.parse_arg(args_config) arch_config.locale_config = LocaleConfiguration.parse_arg(args_config)
@ -141,6 +253,7 @@ class ArchConfig:
if archinstall_lang := args_config.get('archinstall-language', None): if archinstall_lang := args_config.get('archinstall-language', None):
arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang) 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', {}): if disk_config := args_config.get('disk_config', {}):
enc_password = args_config.get('encryption_password', '') enc_password = args_config.get('encryption_password', '')
@ -177,11 +290,15 @@ class ArchConfig:
if net_config := args_config.get('network_config', None): if net_config := args_config.get('network_config', None):
arch_config.network_config = NetworkConfiguration.parse_arg(net_config) arch_config.network_config = NetworkConfiguration.parse_arg(net_config)
if bootloader_config := args_config.get('bootloader', None): if bootloader_config_dict := args_config.get('bootloader_config', None):
arch_config.bootloader = Bootloader.from_arg(bootloader_config, args.skip_boot) arch_config.bootloader_config = BootloaderConfiguration.parse_arg(bootloader_config_dict, args.skip_boot)
# DEPRECATED: separate bootloader and uki fields (backward compatibility)
if args_config.get('uki') and (arch_config.bootloader is None or not arch_config.bootloader.has_uki_support()): elif bootloader_str := args_config.get('bootloader', None):
arch_config.uki = False bootloader = Bootloader.from_arg(bootloader_str, args.skip_boot)
uki = args_config.get('uki', False)
if uki and not bootloader.has_uki_support():
uki = False
arch_config.bootloader_config = BootloaderConfiguration(bootloader=bootloader, uki=uki, removable=True)
# deprecated: backwards compatibility # deprecated: backwards compatibility
audio_config_args = args_config.get('audio_config', None) audio_config_args = args_config.get('audio_config', None)
@ -204,10 +321,14 @@ class ArchConfig:
if packages := args_config.get('packages', []): if packages := args_config.get('packages', []):
arch_config.packages = packages arch_config.packages = packages
if parallel_downloads := args_config.get('parallel_downloads', 0): if pacman_config := args_config.get('pacman_config', None):
arch_config.parallel_downloads = parallel_downloads 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))
arch_config.swap = args_config.get('swap', True) swap_arg = args_config.get('swap')
if swap_arg is not None:
arch_config.swap = ZramConfiguration.parse_arg(swap_arg)
if timezone := args_config.get('timezone', 'UTC'): if timezone := args_config.get('timezone', 'UTC'):
arch_config.timezone = timezone arch_config.timezone = timezone
@ -228,7 +349,7 @@ class ArchConfig:
arch_config.auth_config = AuthenticationConfiguration() arch_config.auth_config = AuthenticationConfiguration()
arch_config.auth_config.root_enc_password = root_password arch_config.auth_config.root_enc_password = root_password
# DEPRECATED: backwards copatibility # DEPRECATED: backwards compatibility
users: list[User] = [] users: list[User] = []
if args_users := args_config.get('!users', None): if args_users := args_config.get('!users', None):
users = User.parse_arguments(args_users) users = User.parse_arguments(args_users)
@ -250,17 +371,17 @@ class ArchConfig:
class ArchConfigHandler: class ArchConfigHandler:
def __init__(self) -> None: def __init__(self) -> None:
self._parser: ArgumentParser = self._define_arguments() self._parser: ArgumentParser = self._define_arguments()
args: Arguments = self._parse_args() self._add_sub_parsers()
self._args = args
self._args: Arguments = self._parse_args()
config = self._parse_config() config = self._parse_config()
try: try:
self._config = ArchConfig.from_config(config, args) self._config = ArchConfig.from_config(config, self._args)
self._config.version = self._get_version() self._config.version = get_version()
except ValueError as err: except ValueError as err:
warn(str(err)) warn(str(err))
exit(1) sys.exit(1)
@property @property
def config(self) -> ArchConfig: def config(self) -> ArchConfig:
@ -282,20 +403,19 @@ class ArchConfigHandler:
def print_help(self) -> None: def print_help(self) -> None:
self._parser.print_help() self._parser.print_help()
def _get_version(self) -> str: def _add_sub_parsers(self) -> None:
try: subparsers = self._parser.add_subparsers(dest='command', help='Available subcommands')
return version('archinstall') _ = subparsers.add_parser(SubCommand.SHARE_LOG.value, help='Upload log file to public server')
except Exception:
return 'Archinstall version not found'
def _define_arguments(self) -> ArgumentParser: def _define_arguments(self) -> ArgumentParser:
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument( parser.add_argument(
'-v', '-v',
'--version', '--version',
action='version', action='version',
default=False, default=False,
version='%(prog)s ' + self._get_version(), version='%(prog)s ' + get_version(),
) )
parser.add_argument( parser.add_argument(
'--config', '--config',
@ -407,6 +527,12 @@ class ArchConfigHandler:
default=False, default=False,
help='Skip the version check when running archinstall', help='Skip the version check when running archinstall',
) )
parser.add_argument(
'--skip-wifi-check',
action='store_true',
default=False,
help='Skip wifi check when running archinstall',
)
parser.add_argument( parser.add_argument(
'--advanced', '--advanced',
action='store_true', action='store_true',
@ -419,7 +545,6 @@ class ArchConfigHandler:
default=False, default=False,
help='Enabled verbose options', help='Enabled verbose options',
) )
return parser return parser
def _parse_args(self) -> Arguments: def _parse_args(self) -> Arguments:
@ -480,22 +605,22 @@ class ArchConfigHandler:
except ValueError as err: except ValueError as err:
if 'Invalid password' in str(err): if 'Invalid password' in str(err):
error(tr('Incorrect credentials file decryption password')) error(tr('Incorrect credentials file decryption password'))
exit(1) sys.exit(1)
else: else:
debug(f'Error decrypting credentials file: {err}') debug(f'Error decrypting credentials file: {err}')
raise err from err raise err from err
else: else:
incorrect_password = False header = tr('Enter credentials file decryption password')
wrong_pwd_text = tr('Incorrect password')
prompt = header
with Tui():
while True: while True:
header = tr('Incorrect password') if incorrect_password else None decryption_pwd: Password | None = tui.run(
lambda p=prompt: get_password( # type: ignore[misc]
decryption_pwd = get_password( header=p,
text=tr('Credentials file decryption password'),
header=header,
allow_skip=False, allow_skip=False,
skip_confirmation=True, no_confirmation=True,
)
) )
if not decryption_pwd: if not decryption_pwd:
@ -507,7 +632,7 @@ class ArchConfigHandler:
except ValueError as err: except ValueError as err:
if 'Invalid password' in str(err): if 'Invalid password' in str(err):
debug('Incorrect credentials file decryption password') debug('Incorrect credentials file decryption password')
incorrect_password = True prompt = f'{header}' + f'\n\n{wrong_pwd_text}'
else: else:
debug(f'Error decrypting credentials file: {err}') debug(f'Error decrypting credentials file: {err}')
raise err from err raise err from err
@ -525,12 +650,12 @@ class ArchConfigHandler:
else: else:
error('Not a valid url') error('Not a valid url')
exit(1) sys.exit(1)
def _read_file(self, path: Path) -> str: def _read_file(self, path: Path) -> str:
if not path.exists(): if not path.exists():
error(f'Could not find file {path}') error(f'Could not find file {path}')
exit(1) sys.exit(1)
return path.read_text() return path.read_text()
@ -544,6 +669,3 @@ class ArchConfigHandler:
clean_args[key] = val clean_args[key] = val
return clean_args return clean_args
arch_config_handler: ArchConfigHandler = ArchConfigHandler()

View File

@ -2,12 +2,11 @@ import getpass
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from archinstall.lib.general import SysCommandWorker from archinstall.lib.command import SysCommandWorker
from archinstall.lib.log import debug, info
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import User from archinstall.lib.models.users import User
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import Tui
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
@ -16,20 +15,20 @@ if TYPE_CHECKING:
class AuthenticationHandler: class AuthenticationHandler:
def setup_auth( def setup_auth(
self, self,
install_session: 'Installer', install_session: Installer,
auth_config: AuthenticationConfiguration, auth_config: AuthenticationConfiguration,
hostname: str, hostname: str,
) -> None: ) -> None:
if auth_config.u2f_config and auth_config.users is not 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) 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._configure_u2f_mapping(install_session, u2f_config, users, hostname)
self._update_pam_config(install_session, u2f_config) self._update_pam_config(install_session, u2f_config)
def _update_pam_config( def _update_pam_config(
self, self,
install_session: 'Installer', install_session: Installer,
u2f_config: U2FLoginConfiguration, u2f_config: U2FLoginConfiguration,
) -> None: ) -> None:
match u2f_config.u2f_login_method: match u2f_config.u2f_login_method:
@ -54,7 +53,7 @@ class AuthenticationHandler:
def _add_u2f_entry(self, file: Path, entry: str) -> None: def _add_u2f_entry(self, file: Path, entry: str) -> None:
if not file.exists(): if not file.exists():
debug(f'File does not exist: {file}') debug(f'File does not exist: {file}')
return None return
content = file.read_text().splitlines() content = file.read_text().splitlines()
@ -73,7 +72,7 @@ class AuthenticationHandler:
def _configure_u2f_mapping( def _configure_u2f_mapping(
self, self,
install_session: 'Installer', install_session: Installer,
u2f_config: U2FLoginConfiguration, u2f_config: U2FLoginConfiguration,
users: list[User], users: list[User],
hostname: str, hostname: str,
@ -82,7 +81,7 @@ class AuthenticationHandler:
install_session.pacman.strap('pam-u2f') install_session.pacman.strap('pam-u2f')
Tui.print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}')) print(tr('Setting up U2F login: {}').format(u2f_config.u2f_login_method.value))
# https://developers.yubico.com/pam-u2f/ # https://developers.yubico.com/pam-u2f/
u2f_auth_file = install_session.target / 'etc/u2f_mappings' u2f_auth_file = install_session.target / 'etc/u2f_mappings'
@ -92,11 +91,13 @@ class AuthenticationHandler:
registered_keys: list[str] = [] registered_keys: list[str] = []
for user in users: for user in users:
Tui.print('') print('')
Tui.print(tr('Setting up U2F device for user: {}').format(user.username)) info(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')) info(tr('You may need to enter the PIN and then touch your U2F device to register it'))
cmd = ' '.join(['arch-chroot', str(install_session.target), 'pamu2fcfg', '-u', user.username, '-o', f'pam://{hostname}', '-i', f'pam://{hostname}']) cmd = ' '.join(
['arch-chroot', '-S', str(install_session.target), 'pamu2fcfg', '-u', user.username, '-o', f'pam://{hostname}', '-i', f'pam://{hostname}']
)
debug(f'Enrolling U2F device: {cmd}') debug(f'Enrolling U2F device: {cmd}')
@ -123,6 +124,3 @@ class AuthenticationHandler:
existing_keys = all_keys existing_keys = all_keys
u2f_auth_file.write_text(existing_keys) u2f_auth_file.write_text(existing_keys)
auth_handler = AuthenticationHandler()

View File

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

View File

@ -1,28 +1,33 @@
import time import time
from collections.abc import Iterator from collections.abc import Iterator
from pathlib import Path
from types import TracebackType from types import TracebackType
from typing import ClassVar, Self
from .exceptions import SysCallError from archinstall.lib.command import SysCommand, SysCommandWorker
from .general import SysCommand, SysCommandWorker, locate_binary from archinstall.lib.exceptions import SysCallError
from .installer import Installer from archinstall.lib.log import error
from .output import error
from .storage import storage
class Boot: class Boot:
def __init__(self, installation: Installer): _active_boot: ClassVar[Self | None] = None
self.instance = installation
def __init__(self, path: Path | str):
if isinstance(path, Path):
path = str(path)
self.path = path
self.container_name = 'archinstall' self.container_name = 'archinstall'
self.session: SysCommandWorker | None = None self.session: SysCommandWorker | None = None
self.ready = False self.ready = False
def __enter__(self) -> 'Boot': def __enter__(self) -> Self:
if (existing_session := storage.get('active_boot', None)) and existing_session.instance != self.instance: if Boot._active_boot and Boot._active_boot.path != self.path:
raise KeyError('Archinstall only supports booting up one instance and another session is already active.') raise KeyError('Archinstall only supports booting up one instance and another session is already active.')
if existing_session: if Boot._active_boot:
self.session = existing_session.session self.session = Boot._active_boot.session
self.ready = existing_session.ready self.ready = Boot._active_boot.ready
else: else:
# '-P' or --console=pipe could help us not having to do a bunch # '-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. # of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual.
@ -30,7 +35,7 @@ class Boot:
[ [
'systemd-nspawn', 'systemd-nspawn',
'-D', '-D',
str(self.instance.target), self.path,
'--timezone=off', '--timezone=off',
'-b', '-b',
'--no-pager', '--no-pager',
@ -45,7 +50,7 @@ class Boot:
self.ready = True self.ready = True
break break
storage['active_boot'] = self Boot._active_boot = self
return self return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None: def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
@ -55,7 +60,7 @@ class Boot:
if exc_type is not None: if exc_type is not None:
error( error(
str(exc_value), str(exc_value),
f'The error above occurred in a temporary boot-up of the installation {self.instance}', f'The error above occurred in a temporary boot-up of the installation {self.path!r}',
) )
shutdown = None shutdown = None
@ -74,12 +79,12 @@ class Boot:
shutdown_exit_code = shutdown.exit_code shutdown_exit_code = shutdown.exit_code
if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0): if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0):
storage['active_boot'] = None Boot._active_boot = None
else: else:
session_exit_code = self.session.exit_code if self.session else -1 session_exit_code = self.session.exit_code if self.session else -1
raise SysCallError( raise SysCallError(
f'Could not shut down temporary boot of {self.instance}: {session_exit_code}/{shutdown_exit_code}', f'Could not shut down temporary boot of {self.path!r}: {session_exit_code}/{shutdown_exit_code}',
exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])), exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])),
) )
@ -100,17 +105,7 @@ class Boot:
return self.session.is_alive() return self.session.is_alive()
def SysCommand(self, cmd: list[str], *args, **kwargs) -> SysCommand: # type: ignore[no-untyped-def] 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) 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] 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) return SysCommandWorker(['systemd-run', f'--machine={self.container_name}', '--pty', *cmd], *args, **kwargs)

View File

View File

@ -0,0 +1,217 @@
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

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

View File

@ -1,101 +1,18 @@
from __future__ import annotations
import json
import os import os
import re
import secrets
import shlex import shlex
import stat import stat
import string
import subprocess import subprocess
import sys import sys
import time import time
from collections.abc import Iterator 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 select import EPOLLHUP, EPOLLIN, epoll
from shutil import which from shutil import which
from types import TracebackType from types import TracebackType
from typing import Any, override from typing import Any, Self, override
from .exceptions import RequirementError, SysCallError from archinstall.lib.exceptions import RequirementError, SysCallError
from .output import debug, error, logger from archinstall.lib.log import debug, error, logger
from archinstall.lib.utils.encoding import clear_vt100_escape_codes
# 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: class SysCommandWorker:
@ -127,8 +44,8 @@ class SysCommandWorker:
self._trace_log_pos = 0 self._trace_log_pos = 0
self.poll_object = epoll() self.poll_object = epoll()
self.child_fd: int | None = None self.child_fd: int | None = None
self.started: float | None = None self.started = False
self.ended: float | None = None self.ended = False
self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines
def __contains__(self, key: bytes) -> bool: def __contains__(self, key: bytes) -> bool:
@ -168,7 +85,7 @@ class SysCommandWorker:
except UnicodeDecodeError: except UnicodeDecodeError:
return str(self._trace_log) return str(self._trace_log)
def __enter__(self) -> 'SysCommandWorker': def __enter__(self) -> Self:
return self return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None: def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
@ -200,7 +117,7 @@ class SysCommandWorker:
def is_alive(self) -> bool: def is_alive(self) -> bool:
self.poll() self.poll()
if self.started and self.ended is None: if self.started and not self.ended:
return True return True
return False return False
@ -256,11 +173,11 @@ class SysCommandWorker:
self.peak(output) self.peak(output)
self._trace_log += output self._trace_log += output
except OSError: except OSError:
self.ended = time.time() self.ended = True
break break
if self.ended or (not got_output and not _pid_exists(self.pid)): if self.ended or (not got_output and not _pid_exists(self.pid)):
self.ended = time.time() self.ended = True
try: try:
wait_status = os.waitpid(self.pid, 0)[1] wait_status = os.waitpid(self.pid, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status) self.exit_code = os.waitstatus_to_exitcode(wait_status)
@ -298,7 +215,7 @@ class SysCommandWorker:
# Only parent process moves back to the original working directory # Only parent process moves back to the original working directory
os.chdir(old_dir) os.chdir(old_dir)
self.started = time.time() self.started = True
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP) self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
return True return True
@ -414,31 +331,6 @@ class SysCommand:
return None 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( def run(
cmd: list[str], cmd: list[str],
input_data: bytes | None = None, input_data: bytes | None = None,
@ -454,8 +346,39 @@ 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: def _pid_exists(pid: int) -> bool:
try: try:
return any(subprocess.check_output(['ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip()) return any(subprocess.check_output(['ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip())
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return False 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

@ -2,18 +2,20 @@ import json
import readline import readline
import stat import stat
from pathlib import Path 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.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu, Tui from archinstall.lib.utils.format import as_key_value_pair
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType 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: class ConfigurationOutput:
@ -41,43 +43,86 @@ class ConfigurationOutput:
return self._user_creds_file return self._user_creds_file
def user_config_to_json(self) -> str: def user_config_to_json(self) -> str:
out = self._config.safe_json() config = self._config.safe_config()
return json.dumps(out, indent=4, sort_keys=True, cls=JSON)
adapter = TypeAdapter(dict[ArchConfigType, Any])
python_dict = adapter.dump_python(config)
return json.dumps(python_dict, indent=4, sort_keys=True)
def user_credentials_to_json(self) -> str: def user_credentials_to_json(self) -> str:
out = self._config.unsafe_json() cfg = self._config.unsafe_config()
return json.dumps(out, indent=4, sort_keys=True, cls=UNSAFE_JSON)
adapter = TypeAdapter(dict[ArchConfigType, Any])
python_dict = adapter.dump_python(cfg)
return json.dumps(python_dict, indent=4, sort_keys=True)
def write_debug(self) -> None: def write_debug(self) -> None:
debug(' -- Chosen configuration --') debug(' -- Chosen configuration --')
debug(self.user_config_to_json()) debug(self.user_config_to_json())
def confirm_config(self) -> bool: 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:
header = f'{tr("The specified configuration will be applied")}. ' header = f'{tr("The specified configuration will be applied")}. '
header += tr('Would you like to continue?') + '\n' header += tr('Would you like to continue?') + '\n'
with Tui(): if show_install_warnings:
header += self._render_install_warnings()
group = MenuItemGroup.yes_no() group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.yes()
group.set_preview_for_all(lambda x: self.user_config_to_json()) group.set_preview_for_all(lambda x: self.user_config_to_json())
result = SelectMenu[bool]( result = await Confirmation(
group, group=group,
header=header, header=header,
alignment=Alignment.CENTER,
columns=2,
orientation=Orientation.HORIZONTAL,
allow_skip=False, allow_skip=False,
preview_size='auto', preset=True,
preview_style=PreviewStyle.BOTTOM, preview_location='bottom',
preview_frame=FrameProperties.max(tr('Configuration')), preview_header=tr('Configuration preview'),
).run() ).show()
if result.item() != MenuItem.yes(): if not result.get_value():
return False return False
return True 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: def _is_valid_path(self, dest_path: Path) -> bool:
dest_path_ok = dest_path.exists() and dest_path.is_dir() dest_path_ok = dest_path.exists() and dest_path.is_dir()
if not dest_path_ok: if not dest_path_ok:
@ -122,7 +167,7 @@ class ConfigurationOutput:
self.save_user_creds(save_path, password=password) self.save_user_creds(save_path, password=password)
def save_config(config: ArchConfig) -> None: async def save_config(config: ArchConfig) -> None:
def preview(item: MenuItem) -> str | None: def preview(item: MenuItem) -> str | None:
match item.value: match item.value:
case 'user_config': case 'user_config':
@ -160,13 +205,11 @@ def save_config(config: ArchConfig) -> None:
] ]
group = MenuItemGroup(items) group = MenuItemGroup(items)
result = SelectMenu[str]( result = await Selection[str](
group, group,
allow_skip=True, allow_skip=True,
preview_frame=FrameProperties.max(tr('Configuration')), preview_location='right',
preview_size='auto', ).show()
preview_style=PreviewStyle.RIGHT,
).run()
match result.type_: match result.type_:
case ResultType.Skip: case ResultType.Skip:
@ -179,9 +222,8 @@ def save_config(config: ArchConfig) -> None:
readline.set_completer_delims('\t\n=') readline.set_completer_delims('\t\n=')
readline.parse_and_bind('tab: complete') readline.parse_and_bind('tab: complete')
dest_path = prompt_dir( dest_path = await prompt_dir(
tr('Directory'), tr('Enter a directory for the configuration(s) to be saved') + '\n',
tr('Enter a directory for the configuration(s) to be saved (tab completion enabled)') + '\n',
allow_skip=True, allow_skip=True,
) )
@ -190,45 +232,34 @@ def save_config(config: ArchConfig) -> None:
header = tr('Do you want to save the configuration file(s) to {}?').format(dest_path) header = tr('Do you want to save the configuration file(s) to {}?').format(dest_path)
group = MenuItemGroup.yes_no() save_result = await Confirmation(
group.focus_item = MenuItem.yes()
result = SelectMenu(
group,
header=header, header=header,
allow_skip=False, allow_skip=False,
alignment=Alignment.CENTER, preset=True,
columns=2, ).show()
orientation=Orientation.HORIZONTAL,
).run()
match result.type_: match save_result.type_:
case ResultType.Selection: case ResultType.Selection:
if result.item() == MenuItem.no(): if not save_result.get_value():
return
case _:
return return
debug(f'Saving configuration files to {dest_path.absolute()}') debug(f'Saving configuration files to {dest_path.absolute()}')
header = tr('Do you want to encrypt the user_credentials.json file?') header = tr('Do you want to encrypt the user_credentials.json file?')
group = MenuItemGroup.yes_no() enc_result = await Confirmation(
group.focus_item = MenuItem.no()
result = SelectMenu(
group,
header=header, header=header,
allow_skip=False, allow_skip=False,
alignment=Alignment.CENTER, preset=False,
columns=2, ).show()
orientation=Orientation.HORIZONTAL,
).run()
enc_password: str | None = None enc_password: str | None = None
match result.type_: if enc_result.type_ == ResultType.Selection:
case ResultType.Selection: if enc_result.get_value():
if result.item() == MenuItem.yes(): password = await get_password(
password = get_password( header=tr('Credentials file encryption password'),
text=tr('Credentials file encryption password'),
allow_skip=True, allow_skip=True,
) )

View File

@ -6,7 +6,7 @@ from pathlib import Path
from cryptography.fernet import Fernet, InvalidToken from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
from .output import debug from archinstall.lib.log import debug
libcrypt = ctypes.CDLL('libcrypt.so') 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: def crypt_yescrypt(plaintext: str) -> str:
""" """
By default chpasswd in Arch uses PAM to to hash the password with crypt_yescrypt By default chpasswd in Arch uses PAM to hash the password with crypt_yescrypt
the PAM code https://github.com/linux-pam/linux-pam/blob/master/modules/pam_unix/support.c 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 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 choosen If no value was specified (or commented out) a default of 5 is chosen
""" """
value = _search_login_defs('YESCRYPT_COST_FACTOR') value = _search_login_defs('YESCRYPT_COST_FACTOR')
if value is not None: if value is not None:

View File

@ -1,19 +1,22 @@
from __future__ import annotations
import json
import logging import logging
import os import os
import time
from collections.abc import Iterable
from pathlib import Path 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 parted import Device, Disk, DiskException, FileSystem, Geometry, IOException, Partition, PartitionException, freshDisk, getAllDevices, getDevice, newDisk
from ..exceptions import DiskError, SysCallError, UnknownFilesystemFormat from archinstall.lib.command import SysCommand
from ..general import SysCommand, SysCommandWorker from archinstall.lib.disk.luks import Luks2, unlock_luks2_dev
from ..luks import Luks2 from archinstall.lib.disk.utils import (
from ..models.device import ( find_lsblk_info,
get_all_lsblk_info,
get_lsblk_info,
mount,
udev_sync,
umount,
)
from archinstall.lib.exceptions import DiskError, SysCallError, UnknownFilesystemFormat
from archinstall.lib.log import debug, error, info, log
from archinstall.lib.models.device import (
DEFAULT_ITER_TIME, DEFAULT_ITER_TIME,
BDevice, BDevice,
BtrfsMountOption, BtrfsMountOption,
@ -21,33 +24,19 @@ from ..models.device import (
DiskEncryption, DiskEncryption,
FilesystemType, FilesystemType,
LsblkInfo, LsblkInfo,
LvmGroupInfo,
LvmPVInfo,
LvmVolume,
LvmVolumeGroup,
LvmVolumeInfo,
ModificationStatus, ModificationStatus,
PartitionFlag, PartitionFlag,
PartitionGUID, PartitionGUID,
PartitionModification, PartitionModification,
PartitionTable, PartitionTable,
SectorSize,
Size,
SubvolumeModification, SubvolumeModification,
Unit, Unit,
_BtrfsSubvolumeInfo, _BtrfsSubvolumeInfo,
_DeviceInfo, _DeviceInfo,
_PartitionInfo, _PartitionInfo,
) )
from ..models.users import Password from archinstall.lib.models.users import Password
from ..output import debug, error, info, log from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
from ..utils.util import is_subpath
from .utils import (
find_lsblk_info,
get_all_lsblk_info,
get_lsblk_info,
umount,
)
class DeviceHandler: class DeviceHandler:
@ -69,13 +58,11 @@ class DeviceHandler:
def load_devices(self) -> None: def load_devices(self) -> None:
block_devices = {} block_devices = {}
self.udev_sync() udev_sync()
all_lsblk_info = get_all_lsblk_info() all_lsblk_info = get_all_lsblk_info()
devices = getAllDevices() devices = getAllDevices()
devices.extend(self.get_loop_devices()) devices.extend(self.get_loop_devices())
archiso_mountpoint = Path('/run/archiso/airootfs')
for device in devices: for device in devices:
dev_lsblk_info = find_lsblk_info(device.path, all_lsblk_info) dev_lsblk_info = find_lsblk_info(device.path, all_lsblk_info)
@ -87,7 +74,7 @@ class DeviceHandler:
continue continue
# exclude archiso loop device # exclude archiso loop device
if dev_lsblk_info.mountpoint == archiso_mountpoint: if dev_lsblk_info.mountpoint == ARCHISO_MOUNTPOINT:
continue continue
try: try:
@ -112,7 +99,7 @@ class DeviceHandler:
fs_type = self._determine_fs_type(partition, lsblk_info) fs_type = self._determine_fs_type(partition, lsblk_info)
subvol_infos = [] subvol_infos = []
if fs_type == FilesystemType.Btrfs: if fs_type == FilesystemType.BTRFS:
subvol_infos = self.get_btrfs_info(partition.path, lsblk_info) subvol_infos = self.get_btrfs_info(partition.path, lsblk_info)
partition_infos.append( partition_infos.append(
@ -160,8 +147,8 @@ class DeviceHandler:
) -> FilesystemType | None: ) -> FilesystemType | None:
try: try:
if partition.fileSystem: if partition.fileSystem:
if partition.fileSystem.type == FilesystemType.LinuxSwap.parted_value: if partition.fileSystem.type == FilesystemType.LINUX_SWAP.parted_value:
return FilesystemType.LinuxSwap return FilesystemType.LINUX_SWAP
return FilesystemType(partition.fileSystem.type) return FilesystemType(partition.fileSystem.type)
elif lsblk_info is not None: elif lsblk_info is not None:
return FilesystemType(lsblk_info.fstype) if lsblk_info.fstype else None return FilesystemType(lsblk_info.fstype) if lsblk_info.fstype else None
@ -188,23 +175,6 @@ class DeviceHandler:
return part return part
return None 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: def get_uuid_for_path(self, path: Path) -> str | None:
partition = self.find_partition(path) partition = self.find_partition(path)
return partition.partuuid if partition else None return partition.partuuid if partition else None
@ -220,7 +190,7 @@ class DeviceHandler:
subvol_infos: list[_BtrfsSubvolumeInfo] = [] subvol_infos: list[_BtrfsSubvolumeInfo] = []
if not lsblk_info.mountpoint: if not lsblk_info.mountpoint:
self.mount(dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) mount(dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
mountpoint = self._TMP_BTRFS_MOUNT mountpoint = self._TMP_BTRFS_MOUNT
else: else:
# when multiple subvolumes are mounted then the lsblk output may look like # when multiple subvolumes are mounted then the lsblk output may look like
@ -271,20 +241,20 @@ class DeviceHandler:
options = [] options = []
match fs_type: match fs_type:
case FilesystemType.Btrfs | FilesystemType.F2fs | FilesystemType.Xfs: case FilesystemType.BTRFS | FilesystemType.XFS:
# Force overwrite # Force overwrite
options.append('-f') options.append('-f')
case FilesystemType.Ext2 | FilesystemType.Ext3 | FilesystemType.Ext4: case FilesystemType.F2FS:
options.append('-f')
options.extend(('-O', 'extra_attr'))
case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4:
# Force create # Force create
options.append('-F') options.append('-F')
case FilesystemType.Fat12 | FilesystemType.Fat16 | FilesystemType.Fat32: case _ if fs_type.is_fat():
mkfs_type = 'fat' mkfs_type = 'fat'
# Set FAT size # Set FAT size
options.extend(('-F', fs_type.value.removeprefix(mkfs_type))) options.extend(('-F', fs_type.value.removeprefix(mkfs_type)))
case FilesystemType.Ntfs: case FilesystemType.LINUX_SWAP:
# Skip zeroing and bad sector check
options.append('--fast')
case FilesystemType.LinuxSwap:
command = 'mkswap' command = 'mkswap'
case _: case _:
raise UnknownFilesystemFormat(f'Filetype "{fs_type.value}" is not supported') raise UnknownFilesystemFormat(f'Filetype "{fs_type.value}" is not supported')
@ -319,7 +289,7 @@ class DeviceHandler:
key_file = luks_handler.encrypt(iter_time=iter_time) key_file = luks_handler.encrypt(iter_time=iter_time)
self.udev_sync() udev_sync()
luks_handler.unlock(key_file=key_file) luks_handler.unlock(key_file=key_file)
@ -350,7 +320,7 @@ class DeviceHandler:
key_file = luks_handler.encrypt(iter_time=enc_conf.iter_time) key_file = luks_handler.encrypt(iter_time=enc_conf.iter_time)
self.udev_sync() udev_sync()
luks_handler.unlock(key_file=key_file) luks_handler.unlock(key_file=key_file)
@ -363,145 +333,6 @@ class DeviceHandler:
info(f'luks2 locking device: {dev_path}') info(f'luks2 locking device: {dev_path}')
luks_handler.lock() 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( def _setup_partition(
self, self,
part_mod: PartitionModification, part_mod: PartitionModification,
@ -511,7 +342,7 @@ class DeviceHandler:
) -> None: ) -> None:
# when we require a delete and the partition to be (re)created # when we require a delete and the partition to be (re)created
# already exists then we have to delete it first # 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}') info(f'Delete existing partition: {part_mod.safe_dev_path}')
part_info = self.find_partition(part_mod.safe_dev_path) part_info = self.find_partition(part_mod.safe_dev_path)
@ -520,7 +351,7 @@ class DeviceHandler:
disk.deletePartition(part_info.partition) disk.deletePartition(part_info.partition)
if part_mod.status == ModificationStatus.Delete: if part_mod.status == ModificationStatus.DELETE:
return return
start_sector = part_mod.start.convert( start_sector = part_mod.start.convert(
@ -597,7 +428,7 @@ class DeviceHandler:
) -> None: ) -> None:
info(f'Creating subvolumes: {path}') info(f'Creating subvolumes: {path}')
self.mount(path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) mount(path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
for sub_vol in sorted(btrfs_subvols, key=lambda x: x.name): for sub_vol in sorted(btrfs_subvols, key=lambda x: x.name):
debug(f'Creating subvolume: {sub_vol.name}') debug(f'Creating subvolume: {sub_vol.name}')
@ -632,7 +463,7 @@ class DeviceHandler:
if not part_mod.mapper_name: if not part_mod.mapper_name:
raise ValueError('No device path specified for modification') raise ValueError('No device path specified for modification')
luks_handler = self.unlock_luks2_dev( luks_handler = unlock_luks2_dev(
part_mod.safe_dev_path, part_mod.safe_dev_path,
part_mod.mapper_name, part_mod.mapper_name,
enc_conf.encryption_password, enc_conf.encryption_password,
@ -646,7 +477,7 @@ class DeviceHandler:
luks_handler = None luks_handler = None
dev_path = part_mod.safe_dev_path dev_path = part_mod.safe_dev_path
self.mount( mount(
dev_path, dev_path,
self._TMP_BTRFS_MOUNT, self._TMP_BTRFS_MOUNT,
create_target_mountpoint=True, create_target_mountpoint=True,
@ -665,19 +496,6 @@ class DeviceHandler:
if luks_handler is not None and luks_handler.mapper_dev is not None: if luks_handler is not None and luks_handler.mapper_dev is not None:
luks_handler.lock() 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: def umount_all_existing(self, device_path: Path) -> None:
debug(f'Unmounting all existing partitions: {device_path}') debug(f'Unmounting all existing partitions: {device_path}')
@ -687,7 +505,7 @@ class DeviceHandler:
debug(f'Unmounting: {partition.path}') debug(f'Unmounting: {partition.path}')
# un-mount for existing encrypted partitions # un-mount for existing encrypted partitions
if partition.fs_type == FilesystemType.Crypto_luks: if partition.fs_type == FilesystemType.CRYPTO_LUKS:
Luks2(partition.path).lock() Luks2(partition.path).lock()
else: else:
umount(partition.path, recursive=True) umount(partition.path, recursive=True)
@ -726,49 +544,16 @@ class DeviceHandler:
disk.commit() disk.commit()
@staticmethod # Wipe filesystem/LVM signatures from newly created partitions
def swapon(path: Path) -> None: # to prevent "signature detected" errors
try: for part_mod in filtered_part:
SysCommand(['swapon', str(path)]) if part_mod.dev_path:
except SysCallError as err: debug(f'Wiping signatures from: {part_mod.dev_path}')
raise DiskError(f'Could not enable swap {path}:\n{err.message}') SysCommand(f'wipefs --all {part_mod.dev_path}')
def mount( # Sync with udev after wiping signatures
self, if filtered_part:
dev_path: Path, udev_sync()
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]: def detect_pre_mounted_mods(self, base_mountpoint: Path) -> list[DeviceModification]:
part_mods: dict[Path, list[PartitionModification]] = {} part_mods: dict[Path, list[PartitionModification]] = {}
@ -776,7 +561,7 @@ class DeviceHandler:
for device in self.devices: for device in self.devices:
for part_info in device.partition_infos: for part_info in device.partition_infos:
for mountpoint in part_info.mountpoints: for mountpoint in part_info.mountpoints:
if is_subpath(mountpoint, base_mountpoint): if mountpoint.is_relative_to(base_mountpoint):
path = Path(part_info.disk.device.path) path = Path(part_info.disk.device.path)
part_mods.setdefault(path, []) part_mods.setdefault(path, [])
part_mod = PartitionModification.from_existing_partition(part_info) part_mod = PartitionModification.from_existing_partition(part_info)
@ -837,12 +622,5 @@ class DeviceHandler:
self._wipe(block_device.device_info.path) 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() device_handler = DeviceHandler()

View File

@ -1,27 +1,45 @@
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from typing import override 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.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 ( from archinstall.lib.models.device import (
DEFAULT_ITER_TIME, DEFAULT_ITER_TIME,
BDevice,
BtrfsMountOption,
BtrfsOptions, BtrfsOptions,
DeviceModification,
DiskEncryption, DiskEncryption,
DiskLayoutConfiguration, DiskLayoutConfiguration,
DiskLayoutType, DiskLayoutType,
EncryptionType, EncryptionType,
FilesystemType,
LvmConfiguration, LvmConfiguration,
LvmLayoutType,
LvmVolume,
LvmVolumeGroup,
ModificationStatus,
PartitionFlag,
PartitionModification,
PartitionType,
SectorSize,
Size,
SnapshotConfig, SnapshotConfig,
SnapshotType, SnapshotType,
SubvolumeModification,
Unit,
_DeviceInfo,
) )
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType 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 @dataclass
@ -32,7 +50,7 @@ class DiskMenuConfig:
disk_encryption: DiskEncryption | None disk_encryption: DiskEncryption | None
class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]): class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
def __init__(self, disk_layout_config: DiskLayoutConfiguration | None): def __init__(self, disk_layout_config: DiskLayoutConfiguration | None):
if not disk_layout_config: if not disk_layout_config:
self._disk_menu_config = DiskMenuConfig( self._disk_menu_config = DiskMenuConfig(
@ -51,8 +69,8 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
btrfs_snapshot_config=snapshot_config, btrfs_snapshot_config=snapshot_config,
) )
menu_optioons = self._define_menu_options() menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True) self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True)
super().__init__( super().__init__(
self._item_group, self._item_group,
@ -95,14 +113,16 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
] ]
@override @override
def run(self, additional_title: str | None = None) -> DiskLayoutConfiguration | None: async def show(self) -> DiskLayoutConfiguration | None: # type: ignore[override]
super().run(additional_title=additional_title) config: DiskMenuConfig | None = await super().show()
if config is None:
return None
if self._disk_menu_config.disk_config: if config.disk_config:
self._disk_menu_config.disk_config.lvm_config = self._disk_menu_config.lvm_config 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) 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 config.disk_config.disk_encryption = self._disk_menu_config.disk_encryption
return self._disk_menu_config.disk_config return config.disk_config
return None return None
@ -122,7 +142,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
return False return False
def _select_disk_encryption(self, preset: DiskEncryption | None) -> DiskEncryption | None: async def _select_disk_encryption(self, preset: DiskEncryption | None) -> DiskEncryption | None:
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value 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 lvm_config: LvmConfiguration | None = self._item_group.find_by_key('lvm_config').value
@ -134,12 +154,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
if not DiskEncryption.validate_enc(modifications, lvm_config): if not DiskEncryption.validate_enc(modifications, lvm_config):
return None return None
disk_encryption = DiskEncryptionMenu(modifications, lvm_config=lvm_config, preset=preset).run() disk_encryption = await DiskEncryptionMenu(modifications, lvm_config=lvm_config, preset=preset).show()
return disk_encryption return disk_encryption
def _select_disk_layout_config(self, preset: DiskLayoutConfiguration | None) -> DiskLayoutConfiguration | None: async def _select_disk_layout_config(self, preset: DiskLayoutConfiguration | None) -> DiskLayoutConfiguration | None:
disk_config = select_disk_config(preset) disk_config = await select_disk_config(preset)
if disk_config != preset: if disk_config != preset:
self._menu_item_group.find_by_key('lvm_config').value = None self._menu_item_group.find_by_key('lvm_config').value = None
@ -147,20 +167,20 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
return disk_config return disk_config
def _select_lvm_config(self, preset: LvmConfiguration | None) -> LvmConfiguration | None: async def _select_lvm_config(self, preset: LvmConfiguration | None) -> LvmConfiguration | None:
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
if not disk_config: if not disk_config:
return preset return preset
lvm_config = select_lvm_config(disk_config, preset=preset) lvm_config = await select_lvm_config(disk_config, preset=preset)
if lvm_config != preset: if lvm_config != preset:
self._menu_item_group.find_by_key('disk_encryption').value = None self._menu_item_group.find_by_key('disk_encryption').value = None
return lvm_config return lvm_config
def _select_btrfs_snapshots(self, preset: SnapshotConfig | None) -> SnapshotConfig | None: async def _select_btrfs_snapshots(self, preset: SnapshotConfig | None) -> SnapshotConfig | None:
preset_type = preset.snapshot_type if preset else None preset_type = preset.snapshot_type if preset else None
group = MenuItemGroup.from_enum( group = MenuItemGroup.from_enum(
@ -169,13 +189,11 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
preset=preset_type, preset=preset_type,
) )
result = SelectMenu[SnapshotType]( result = await Selection[SnapshotType](
group, group,
allow_reset=True, allow_reset=True,
allow_skip=True, allow_skip=True,
frame=FrameProperties.min(tr('Snapshot type')), ).show()
alignment=Alignment.CENTER,
).run()
match result.type_: match result.type_:
case ResultType.Skip: case ResultType.Skip:
@ -204,7 +222,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
for mod in device_mods: for mod in device_mods:
# create partition table # create partition table
partition_table = FormattedOutput.as_table(mod.partitions) partition_table = as_table(mod.partitions)
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n' output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe) output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe)
@ -213,7 +231,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
# create btrfs table # create btrfs table
btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols] btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols]
for partition in btrfs_partitions: for partition in btrfs_partitions:
output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n' output_btrfs += as_table(partition.btrfs_subvols) + '\n'
output = output_partition + output_btrfs output = output_partition + output_btrfs
return output.rstrip() return output.rstrip()
@ -229,12 +247,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg()) output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg())
for vol_gp in lvm_config.vol_groups: for vol_gp in lvm_config.vol_groups:
pv_table = FormattedOutput.as_table(vol_gp.pvs) pv_table = as_table(vol_gp.pvs)
output += '{}:\n{}'.format(tr('Physical volumes'), pv_table) output += '{}:\n{}'.format(tr('Physical volumes'), pv_table)
output += f'\nVolume Group: {vol_gp.name}' output += f'\nVolume Group: {vol_gp.name}'
lvm_volumes = FormattedOutput.as_table(vol_gp.volumes) lvm_volumes = as_table(vol_gp.volumes)
output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes) output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes)
return output return output
@ -250,19 +268,20 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
def _prev_disk_encryption(self, item: MenuItem) -> str | None: def _prev_disk_encryption(self, item: MenuItem) -> str | None:
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value 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 enc_config: DiskEncryption | None = item.value
if disk_config and not DiskEncryption.validate_enc(disk_config.device_modifications, disk_config.lvm_config): if disk_config and not DiskEncryption.validate_enc(disk_config.device_modifications, lvm_config):
return tr('LVM disk encryption with more than 2 partitions is currently not supported') return tr('LVM disk encryption with more than 2 partitions is currently not supported')
if enc_config: if enc_config:
enc_type = enc_config.encryption_type enc_type = enc_config.encryption_type
output = tr('Encryption type') + f': {EncryptionType.type_to_text(enc_type)}\n' output = tr('Encryption type') + f': {enc_type.type_to_text()}\n'
if enc_config.encryption_password: if enc_config.encryption_password:
output += tr('Password') + f': {enc_config.encryption_password.hidden()}\n' output += tr('Password') + f': {enc_config.encryption_password.hidden()}\n'
if enc_type != EncryptionType.NoEncryption: if enc_type != EncryptionType.NO_ENCRYPTION:
output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n' output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n'
if enc_config.partitions: if enc_config.partitions:
@ -276,3 +295,582 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
return output return output
return None 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,27 +1,26 @@
from pathlib import Path from pathlib import Path
from typing import override 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.menu_helper import MenuHelper
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.device import ( from archinstall.lib.models.device import (
DEFAULT_ITER_TIME,
DeviceModification, DeviceModification,
DiskEncryption, DiskEncryption,
EncryptionType, EncryptionType,
Fido2Device,
LvmConfiguration, LvmConfiguration,
LvmVolume, LvmVolume,
PartitionModification, PartitionModification,
) )
from archinstall.lib.models.users import Password
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import EditMenu, SelectMenu from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType 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]): class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
@ -39,8 +38,8 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
self._device_modifications = device_modifications self._device_modifications = device_modifications
self._lvm_config = lvm_config self._lvm_config = lvm_config
menu_optioons = self._define_menu_options() menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True) self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True)
super().__init__( super().__init__(
self._item_group, self._item_group,
@ -52,9 +51,9 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
return [ return [
MenuItem( MenuItem(
text=tr('Encryption type'), text=tr('Encryption type'),
action=lambda x: select_encryption_type(self._device_modifications, self._lvm_config, x), action=lambda x: select_encryption_type(self._lvm_config, x),
value=self._enc_config.encryption_type, value=self._enc_config.encryption_type,
preview_action=self._preview, preview_action=self._prev_type,
key='encryption_type', key='encryption_type',
), ),
MenuItem( MenuItem(
@ -62,7 +61,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
action=lambda x: select_encrypted_password(), action=lambda x: select_encrypted_password(),
value=self._enc_config.encryption_password, value=self._enc_config.encryption_password,
dependencies=[self._check_dep_enc_type], dependencies=[self._check_dep_enc_type],
preview_action=self._preview, preview_action=self._prev_password,
key='encryption_password', key='encryption_password',
), ),
MenuItem( MenuItem(
@ -70,7 +69,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
action=select_iteration_time, action=select_iteration_time,
value=self._enc_config.iter_time, value=self._enc_config.iter_time,
dependencies=[self._check_dep_enc_type], dependencies=[self._check_dep_enc_type],
preview_action=self._preview, preview_action=self._prev_iter_time,
key='iter_time', key='iter_time',
), ),
MenuItem( MenuItem(
@ -78,7 +77,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
action=lambda x: select_partitions_to_encrypt(self._device_modifications, x), action=lambda x: select_partitions_to_encrypt(self._device_modifications, x),
value=self._enc_config.partitions, value=self._enc_config.partitions,
dependencies=[self._check_dep_partitions], dependencies=[self._check_dep_partitions],
preview_action=self._preview, preview_action=self._prev_partitions,
key='partitions', key='partitions',
), ),
MenuItem( MenuItem(
@ -86,7 +85,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
action=self._select_lvm_vols, action=self._select_lvm_vols,
value=self._enc_config.lvm_volumes, value=self._enc_config.lvm_volumes,
dependencies=[self._check_dep_lvm_vols], dependencies=[self._check_dep_lvm_vols],
preview_action=self._preview, preview_action=self._prev_lvm_vols,
key='lvm_volumes', key='lvm_volumes',
), ),
MenuItem( MenuItem(
@ -94,37 +93,39 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
action=select_hsm, action=select_hsm,
value=self._enc_config.hsm_device, value=self._enc_config.hsm_device,
dependencies=[self._check_dep_enc_type], dependencies=[self._check_dep_enc_type],
preview_action=self._preview, preview_action=self._prev_hsm,
key='hsm_device', key='hsm_device',
), ),
] ]
def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]: async def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]:
if self._lvm_config: if self._lvm_config:
return select_lvm_vols_to_encrypt(self._lvm_config, preset=preset) return await select_lvm_vols_to_encrypt(self._lvm_config, preset=preset)
return [] return []
def _check_dep_enc_type(self) -> bool: def _check_dep_enc_type(self) -> bool:
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type != EncryptionType.NoEncryption: if enc_type and enc_type != EncryptionType.NO_ENCRYPTION:
return True return True
return False return False
def _check_dep_partitions(self) -> bool: def _check_dep_partitions(self) -> bool:
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks]: if enc_type and enc_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS]:
return True return True
return False return False
def _check_dep_lvm_vols(self) -> bool: def _check_dep_lvm_vols(self) -> bool:
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
if enc_type and enc_type == EncryptionType.LuksOnLvm: if enc_type and enc_type == EncryptionType.LUKS_ON_LVM:
return True return True
return False return False
@override @override
def run(self, additional_title: str | None = None) -> DiskEncryption | None: async def show(self) -> DiskEncryption | None:
super().run(additional_title=additional_title) enc_config = await super().show()
if enc_config is None:
return None
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value enc_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 enc_password: Password | None = self._item_group.find_by_key('encryption_password').value
@ -136,19 +137,19 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
assert enc_partitions is not None assert enc_partitions is not None
assert enc_lvm_vols is not None assert enc_lvm_vols is not None
if enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and enc_partitions: if enc_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and enc_partitions:
enc_lvm_vols = [] enc_lvm_vols = []
if enc_type == EncryptionType.LuksOnLvm: if enc_type == EncryptionType.LUKS_ON_LVM:
enc_partitions = [] enc_partitions = []
if enc_type != EncryptionType.NoEncryption and enc_password and (enc_partitions or enc_lvm_vols): if enc_type != EncryptionType.NO_ENCRYPTION and enc_password and (enc_partitions or enc_lvm_vols):
return DiskEncryption( return DiskEncryption(
encryption_password=enc_password, encryption_password=enc_password,
encryption_type=enc_type, encryption_type=enc_type,
partitions=enc_partitions, partitions=enc_partitions,
lvm_volumes=enc_lvm_vols, lvm_volumes=enc_lvm_vols,
hsm_device=self._enc_config.hsm_device, hsm_device=enc_config.hsm_device,
iter_time=iter_time or DEFAULT_ITER_TIME, iter_time=iter_time or DEFAULT_ITER_TIME,
) )
@ -157,22 +158,22 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _preview(self, item: MenuItem) -> str | None: def _preview(self, item: MenuItem) -> str | None:
output = '' output = ''
if (enc_type := self._prev_type()) is not None: if (enc_type := self._prev_type(item)) is not None:
output += enc_type output += enc_type
if (enc_pwd := self._prev_password()) is not None: if (enc_pwd := self._prev_password(item)) is not None:
output += f'\n{enc_pwd}' output += f'\n{enc_pwd}'
if (iter_time := self._prev_iter_time()) is not None: if (iter_time := self._prev_iter_time(item)) is not None:
output += f'\n{iter_time}' output += f'\n{iter_time}'
if (fido_device := self._prev_hsm()) is not None: if (fido_device := self._prev_hsm(item)) is not None:
output += f'\n{fido_device}' output += f'\n{fido_device}'
if (partitions := self._prev_partitions()) is not None: if (partitions := self._prev_partitions(item)) is not None:
output += f'\n\n{partitions}' output += f'\n\n{partitions}'
if (lvm := self._prev_lvm_vols()) is not None: if (lvm := self._prev_lvm_vols(item)) is not None:
output += f'\n\n{lvm}' output += f'\n\n{lvm}'
if not output: if not output:
@ -180,91 +181,84 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
return output return output
def _prev_type(self) -> str | None: def _prev_type(self, item: MenuItem) -> str | None:
enc_type = self._item_group.find_by_key('encryption_type').value enc_type = self._item_group.find_by_key('encryption_type').value
if enc_type: if enc_type:
enc_text = EncryptionType.type_to_text(enc_type) enc_text = enc_type.type_to_text()
return f'{tr("Encryption type")}: {enc_text}' return f'{tr("Encryption type")}: {enc_text}'
return None return None
def _prev_password(self) -> str | None: def _prev_password(self, item: MenuItem) -> str | None:
enc_pwd = self._item_group.find_by_key('encryption_password').value if item.value:
return f'{tr("Encryption password")}: {item.value.hidden()}'
if enc_pwd:
return f'{tr("Encryption password")}: {enc_pwd.hidden()}'
return None return None
def _prev_partitions(self) -> str | None: def _prev_partitions(self, item: MenuItem) -> str | None:
partitions: list[PartitionModification] | None = self._item_group.find_by_key('partitions').value if item.value:
if partitions:
output = tr('Partitions to be encrypted') + '\n' output = tr('Partitions to be encrypted') + '\n'
output += FormattedOutput.as_table(partitions) output += as_table(item.value)
return output.rstrip() return output.rstrip()
return None return None
def _prev_lvm_vols(self) -> str | None: def _prev_lvm_vols(self, item: MenuItem) -> str | None:
volumes: list[PartitionModification] | None = self._item_group.find_by_key('lvm_volumes').value if item.value:
if volumes:
output = tr('LVM volumes to be encrypted') + '\n' output = tr('LVM volumes to be encrypted') + '\n'
output += FormattedOutput.as_table(volumes) output += as_table(item.value)
return output.rstrip() return output.rstrip()
return None return None
def _prev_hsm(self) -> str | None: def _prev_hsm(self, item: MenuItem) -> str | None:
fido_device: Fido2Device | None = self._item_group.find_by_key('hsm_device').value if not item.value:
if not fido_device:
return None return None
fido_device: Fido2Device = item.value
output = str(fido_device.path) output = str(fido_device.path)
output += f' ({fido_device.manufacturer}, {fido_device.product})' output += f' ({fido_device.manufacturer}, {fido_device.product})'
return f'{tr("HSM device")}: {output}' return f'{tr("HSM device")}: {output}'
def _prev_iter_time(self) -> str | None: def _prev_iter_time(self, item: MenuItem) -> str | None:
iter_time = self._item_group.find_by_key('iter_time').value if item.value:
iter_time = item.value
enc_type = self._item_group.find_by_key('encryption_type').value enc_type = self._item_group.find_by_key('encryption_type').value
if iter_time and enc_type != EncryptionType.NoEncryption: if iter_time and enc_type != EncryptionType.NO_ENCRYPTION:
return f'{tr("Iteration time")}: {iter_time}ms' return f'{tr("Iteration time")}: {iter_time}ms'
return None return None
def select_encryption_type( async def select_encryption_type(
device_modifications: list[DeviceModification],
lvm_config: LvmConfiguration | None = None, lvm_config: LvmConfiguration | None = None,
preset: EncryptionType | None = None, preset: EncryptionType | None = None,
) -> EncryptionType | None: ) -> EncryptionType | None:
options: list[EncryptionType] = [] options: list[EncryptionType] = []
if lvm_config: if lvm_config:
options = [EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm] options = [EncryptionType.LVM_ON_LUKS, EncryptionType.LUKS_ON_LVM]
else: else:
options = [EncryptionType.Luks] options = [EncryptionType.LUKS]
if not preset: if not preset:
preset = options[0] preset = options[0]
preset_value = EncryptionType.type_to_text(preset) preset_value = preset.type_to_text()
items = [MenuItem(EncryptionType.type_to_text(o), value=o) for o in options] items = [MenuItem(o.type_to_text(), value=o) for o in options]
group = MenuItemGroup(items) group = MenuItemGroup(items)
group.set_focus_by_value(preset_value) group.set_focus_by_value(preset_value)
result = SelectMenu[EncryptionType]( result = await Selection[EncryptionType](
group, group,
header=tr('Select encryption type'),
allow_skip=True, allow_skip=True,
allow_reset=True, allow_reset=True,
alignment=Alignment.CENTER, ).show()
frame=FrameProperties.min(tr('Encryption type')),
).run()
match result.type_: match result.type_:
case ResultType.Reset: case ResultType.Reset:
@ -275,10 +269,9 @@ def select_encryption_type(
return result.get_value() return result.get_value()
def select_encrypted_password() -> Password | None: async def select_encrypted_password() -> Password | None:
header = tr('Enter disk encryption password (leave blank for no encryption)') + '\n' header = tr('Enter disk encryption password (leave blank for no encryption)') + '\n'
password = get_password( password = await get_password(
text=tr('Disk encryption password'),
header=header, header=header,
allow_skip=True, allow_skip=True,
) )
@ -286,7 +279,7 @@ def select_encrypted_password() -> Password | None:
return password return password
def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None: async def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
header = tr('Select a FIDO2 device to use for HSM') + '\n' header = tr('Select a FIDO2 device to use for HSM') + '\n'
try: try:
@ -297,12 +290,11 @@ def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
if fido_devices: if fido_devices:
group = MenuHelper(data=fido_devices).create_menu_group() group = MenuHelper(data=fido_devices).create_menu_group()
result = SelectMenu[Fido2Device]( result = await Selection[Fido2Device](
group, group,
header=header, header=header,
alignment=Alignment.CENTER,
allow_skip=True, allow_skip=True,
).run() ).show()
match result.type_: match result.type_:
case ResultType.Reset: case ResultType.Reset:
@ -315,7 +307,7 @@ def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
return None return None
def select_partitions_to_encrypt( async def select_partitions_to_encrypt(
modification: list[DeviceModification], modification: list[DeviceModification],
preset: list[PartitionModification], preset: list[PartitionModification],
) -> list[PartitionModification]: ) -> list[PartitionModification]:
@ -329,15 +321,15 @@ def select_partitions_to_encrypt(
avail_partitions = [p for p in partitions if not p.exists()] avail_partitions = [p for p in partitions if not p.exists()]
if avail_partitions: if avail_partitions:
group = MenuHelper(data=avail_partitions).create_menu_group() group = MenuItemGroup.from_objects(avail_partitions)
group.set_selected_by_value(preset) group.set_selected_by_value(preset)
result = SelectMenu[PartitionModification]( result = await Table[PartitionModification](
group, header=tr('Select disks for the installation'),
alignment=Alignment.CENTER, group=group,
multi=True,
allow_skip=True, allow_skip=True,
).run() multi=True,
).show()
match result.type_: match result.type_:
case ResultType.Reset: case ResultType.Reset:
@ -351,20 +343,22 @@ def select_partitions_to_encrypt(
return [] return []
def select_lvm_vols_to_encrypt( async def select_lvm_vols_to_encrypt(
lvm_config: LvmConfiguration, lvm_config: LvmConfiguration,
preset: list[LvmVolume], preset: list[LvmVolume],
) -> list[LvmVolume]: ) -> list[LvmVolume]:
volumes: list[LvmVolume] = lvm_config.get_all_volumes() volumes: list[LvmVolume] = lvm_config.get_all_volumes()
if volumes: if volumes:
group = MenuHelper(data=volumes).create_menu_group() group = MenuItemGroup.from_objects(volumes)
group.set_selected_by_value(preset)
result = SelectMenu[LvmVolume]( result = await Table[LvmVolume](
group, header=tr('Select disks for the installation'),
alignment=Alignment.CENTER, group=group,
allow_skip=True,
multi=True, multi=True,
).run() ).show()
match result.type_: match result.type_:
case ResultType.Reset: case ResultType.Reset:
@ -378,15 +372,12 @@ def select_lvm_vols_to_encrypt(
return [] return []
def select_iteration_time(preset: int | None = None) -> int | None: async def select_iteration_time(preset: int | None = None) -> int | None:
header = tr('Enter iteration time for LUKS encryption (in milliseconds)') + '\n' 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('Higher values increase security but slow down boot time') + '\n'
header += tr(f'Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000') + '\n' header += tr('Default: {}ms, Recommended range: 1000-60000').format(DEFAULT_ITER_TIME) + '\n'
def validate_iter_time(value: str | None) -> str | None:
if not value:
return None
def validate_iter_time(value: str) -> str | None:
try: try:
iter_time = int(value) iter_time = int(value)
if iter_time < 100: if iter_time < 100:
@ -397,21 +388,19 @@ def select_iteration_time(preset: int | None = None) -> int | None:
except ValueError: except ValueError:
return tr('Please enter a valid number') return tr('Please enter a valid number')
result = EditMenu( result = await Input(
tr('Iteration time'),
header=header, header=header,
alignment=Alignment.CENTER,
allow_skip=True, allow_skip=True,
default_text=str(preset) if preset else str(DEFAULT_ITER_TIME), default_value=str(preset) if preset else str(DEFAULT_ITER_TIME),
validator=validate_iter_time, validator_callback=validate_iter_time,
).input() ).show()
match result.type_: match result.type_:
case ResultType.Skip: case ResultType.Skip:
return preset return preset
case ResultType.Selection: case ResultType.Selection:
if not result.text(): if not result.get_value():
return preset return preset
return int(result.text()) return int(result.get_value())
case ResultType.Reset: case ResultType.Reset:
return None return None

View File

@ -1,15 +1,13 @@
from __future__ import annotations
import getpass import getpass
from pathlib import Path from pathlib import Path
from typing import ClassVar 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.device import Fido2Device
from archinstall.lib.models.users import Password
from ..exceptions import SysCallError from archinstall.lib.utils.encoding import clear_vt100_escape_codes_from_str
from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes_from_str
from ..models.users import Password
from ..output import error, info
class Fido2: class Fido2:
@ -101,17 +99,14 @@ class Fido2:
return cls._cryptenroll_devices return cls._cryptenroll_devices
@classmethod @staticmethod
def fido2_enroll( def fido2_enroll(hsm_device: Fido2Device, dev_path: Path, password: Password) -> None:
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) worker = SysCommandWorker(f'systemd-cryptenroll --fido2-device={hsm_device.path} {dev_path}', peek_output=True)
pw_inputted = False pw_inputted = False
pin_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(): while worker.is_alive():
if pw_inputted is False: if pw_inputted is False:
if bytes(f'please enter current passphrase for disk {dev_path}', 'UTF-8') in worker._trace_log.lower(): if bytes(f'please enter current passphrase for disk {dev_path}', 'UTF-8') in worker._trace_log.lower():
@ -121,5 +116,3 @@ class Fido2:
if bytes('please enter security token pin', 'UTF-8') in worker._trace_log.lower(): if bytes('please enter security token pin', 'UTF-8') in worker._trace_log.lower():
worker.write(bytes(getpass.getpass(' '), 'UTF-8')) worker.write(bytes(getpass.getpass(' '), 'UTF-8'))
pin_inputted = True 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,15 +1,20 @@
from __future__ import annotations
import math import math
import time import time
from pathlib import Path from pathlib import Path
from archinstall.lib.translationhandler import tr from archinstall.lib.disk.device_handler import device_handler
from archinstall.tui.curses_menu import Tui from archinstall.lib.disk.luks import Luks2
from archinstall.lib.disk.lvm import (
from ..interactions.general_conf import ask_abort lvm_group_info,
from ..luks import Luks2 lvm_pv_create,
from ..models.device import ( lvm_vg_create,
lvm_vol_create,
lvm_vol_info,
lvm_vol_reduce,
)
from archinstall.lib.disk.utils import udev_sync
from archinstall.lib.log import debug, info
from archinstall.lib.models.device import (
DiskEncryption, DiskEncryption,
DiskLayoutConfiguration, DiskLayoutConfiguration,
DiskLayoutType, DiskLayoutType,
@ -23,8 +28,6 @@ from ..models.device import (
Size, Size,
Unit, Unit,
) )
from ..output import debug, info
from .device_handler import device_handler
class FilesystemHandler: class FilesystemHandler:
@ -32,7 +35,7 @@ class FilesystemHandler:
self._disk_config = disk_config self._disk_config = disk_config
self._enc_config = disk_config.disk_encryption self._enc_config = disk_config.disk_encryption
def perform_filesystem_operations(self, show_countdown: bool = True) -> None: def perform_filesystem_operations(self) -> None:
if self._disk_config.config_type == DiskLayoutType.Pre_mount: if self._disk_config.config_type == DiskLayoutType.Pre_mount:
debug('Disk layout configuration is set to pre-mount, not performing any operations') debug('Disk layout configuration is set to pre-mount, not performing any operations')
return return
@ -43,11 +46,6 @@ class FilesystemHandler:
debug('No modifications required') debug('No modifications required')
return return
device_paths = ', '.join([str(mod.device.device_info.path) for mod in device_mods])
if show_countdown:
self._final_warning(device_paths)
# Setup the blockdevice, filesystem (and optionally encryption). # Setup the blockdevice, filesystem (and optionally encryption).
# Once that's done, we'll hand over to perform_installation() # Once that's done, we'll hand over to perform_installation()
@ -58,7 +56,7 @@ class FilesystemHandler:
for mod in device_mods: for mod in device_mods:
device_handler.partition(mod) device_handler.partition(mod)
device_handler.udev_sync() udev_sync()
if self._disk_config.lvm_config: if self._disk_config.lvm_config:
for mod in device_mods: for mod in device_mods:
@ -72,7 +70,7 @@ class FilesystemHandler:
self._format_partitions(mod.partitions) self._format_partitions(mod.partitions)
for part_mod in 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) device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config)
def _format_partitions( def _format_partitions(
@ -102,7 +100,7 @@ class FilesystemHandler:
device_handler.format(part_mod.safe_fs_type, part_mod.safe_dev_path) device_handler.format(part_mod.safe_fs_type, part_mod.safe_dev_path)
# synchronize with udev before using lsblk # synchronize with udev before using lsblk
device_handler.udev_sync() udev_sync()
lsblk_info = device_handler.fetch_part_info(part_mod.safe_dev_path) lsblk_info = device_handler.fetch_part_info(part_mod.safe_dev_path)
@ -115,7 +113,7 @@ class FilesystemHandler:
# verify that all partitions have a path set (which implies that they have been created) # 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'), 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 # 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 # file system type must be set
lambda x: x.fs_type is None: ValueError('File system type must be set for modification'), lambda x: x.fs_type is None: ValueError('File system type must be set for modification'),
} }
@ -141,34 +139,25 @@ class FilesystemHandler:
self._format_lvm_vols(self._disk_config.lvm_config) self._format_lvm_vols(self._disk_config.lvm_config)
def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption) -> None: def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption) -> None:
if enc_config.encryption_type == EncryptionType.LvmOnLuks: if enc_config.encryption_type == EncryptionType.LVM_ON_LUKS:
enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False) enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False)
self._setup_lvm(lvm_config, enc_mods) self._setup_lvm(lvm_config, enc_mods)
self._format_lvm_vols(lvm_config) self._format_lvm_vols(lvm_config)
# export the lvm group safely otherwise the Luks cannot be closed # Don't close LVM or LUKS during setup - keep everything active
self._safely_close_lvm(lvm_config) # The installation phase will handle unlocking and mounting
# Closing causes "parent leaked" and lvchange errors
for luks in enc_mods.values(): elif enc_config.encryption_type == EncryptionType.LUKS_ON_LVM:
luks.lock()
elif enc_config.encryption_type == EncryptionType.LuksOnLvm:
self._setup_lvm(lvm_config) self._setup_lvm(lvm_config)
enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False) enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False)
self._format_lvm_vols(lvm_config, enc_vols) 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(): for luks in enc_vols.values():
luks.lock() 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( def _setup_lvm(
self, self,
lvm_config: LvmConfiguration, lvm_config: LvmConfiguration,
@ -179,10 +168,10 @@ class FilesystemHandler:
for vg in lvm_config.vol_groups: for vg in lvm_config.vol_groups:
pv_dev_paths = self._get_all_pv_dev_paths(vg.pvs, enc_mods) pv_dev_paths = self._get_all_pv_dev_paths(vg.pvs, enc_mods)
device_handler.lvm_vg_create(pv_dev_paths, vg.name) lvm_vg_create(pv_dev_paths, vg.name)
# figure out what the actual available size in the group is # figure out what the actual available size in the group is
vg_info = device_handler.lvm_group_info(vg.name) vg_info = lvm_group_info(vg.name)
if not vg_info: if not vg_info:
raise ValueError('Unable to fetch VG info') raise ValueError('Unable to fetch VG info')
@ -212,11 +201,11 @@ class FilesystemHandler:
offset = max_vol_offset if lv == max_vol else None offset = max_vol_offset if lv == max_vol else None
debug(f'vg: {vg.name}, vol: {lv.name}, offset: {offset}') debug(f'vg: {vg.name}, vol: {lv.name}, offset: {offset}')
device_handler.lvm_vol_create(vg.name, lv, offset) lvm_vol_create(vg.name, lv, offset)
while True: while True:
debug('Fetching LVM volume info') debug('Fetching LVM volume info')
lv_info = device_handler.lvm_vol_info(lv.name) lv_info = lvm_vol_info(lv.name)
if lv_info is not None: if lv_info is not None:
break break
@ -241,7 +230,7 @@ class FilesystemHandler:
# find the mapper device yet # find the mapper device yet
device_handler.format(vol.fs_type, path) 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) device_handler.create_lvm_btrfs_subvolumes(path, vol.btrfs_subvols, vol.mount_options)
def _lvm_create_pvs( def _lvm_create_pvs(
@ -254,7 +243,7 @@ class FilesystemHandler:
for vg in lvm_config.vol_groups: for vg in lvm_config.vol_groups:
pv_paths |= self._get_all_pv_dev_paths(vg.pvs, enc_mods) pv_paths |= self._get_all_pv_dev_paths(vg.pvs, enc_mods)
device_handler.lvm_pv_create(pv_paths) lvm_pv_create(pv_paths)
def _get_all_pv_dev_paths( def _get_all_pv_dev_paths(
self, self,
@ -329,27 +318,10 @@ class FilesystemHandler:
# from arch wiki: # from arch wiki:
# If a logical volume will be formatted with ext4, leave at least 256 MiB # If a logical volume will be formatted with ext4, leave at least 256 MiB
# free space in the volume group to allow using e2scrub # 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) largest_vol = max(vol_gp.volumes, key=lambda x: x.length)
device_handler.lvm_vol_reduce( lvm_vol_reduce(
largest_vol.safe_dev_path, largest_vol.safe_dev_path,
Size(256, Unit.MiB, SectorSize.default()), Size(256, Unit.MiB, SectorSize.default()),
) )
def _final_warning(self, device_paths: str) -> 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(' ! Formatting {} in ').format(device_paths)
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,18 +1,16 @@
from __future__ import annotations
import shlex import shlex
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from subprocess import CalledProcessError from subprocess import CalledProcessError
from types import TracebackType from types import TracebackType
from archinstall.lib.command import SysCommand, SysCommandWorker, run
from archinstall.lib.disk.utils import get_lsblk_info, umount from archinstall.lib.disk.utils import get_lsblk_info, umount
from archinstall.lib.exceptions import DiskError, SysCallError
from archinstall.lib.log import debug, info
from archinstall.lib.models.device import DEFAULT_ITER_TIME from archinstall.lib.models.device import DEFAULT_ITER_TIME
from archinstall.lib.models.users import Password
from .exceptions import DiskError, SysCallError from archinstall.lib.utils.util import generate_password
from .general import SysCommand, SysCommandWorker, generate_password, run
from .models.users import Password
from .output import debug, info
@dataclass @dataclass
@ -42,10 +40,6 @@ class Luks2:
worker.poll() worker.poll()
worker.write(b'YES\n', line_ending=False) worker.write(b'YES\n', line_ending=False)
def __post_init__(self) -> None:
if self.luks_dev_path is None:
raise ValueError('Partition must have a path set')
def __enter__(self) -> None: def __enter__(self) -> None:
self.unlock(self.key_file) self.unlock(self.key_file)
@ -210,6 +204,18 @@ class Luks2:
self._add_key(key_file) self._add_key(key_file)
self._crypttab(crypttab_path, kf_path, options=['luks', 'key-slot=1']) self._crypttab(crypttab_path, kf_path, options=['luks', 'key-slot=1'])
def create_crypttab_entry(self, target_path: Path) -> None:
"""
Add a crypttab entry without a keyfile so systemd prompts
for the passphrase at boot.
"""
if self.mapper_name is None:
raise ValueError('Mapper name must be provided')
crypttab_path = target_path / 'etc/crypttab'
crypttab_path.parent.mkdir(parents=True, exist_ok=True)
self._crypttab(crypttab_path, Path('none'), options=['luks'])
def _add_key(self, key_file: Path) -> None: def _add_key(self, key_file: Path) -> None:
debug(f'Adding additional key-file {key_file}') debug(f'Adding additional key-file {key_file}')
@ -238,3 +244,16 @@ class Luks2:
uuid = self._get_luks_uuid() uuid = self._get_luks_uuid()
row = f'{self.mapper_name} UUID={uuid} {key_file} {opt}\n' row = f'{self.mapper_name} UUID={uuid} {key_file} {opt}\n'
crypttab.write(row) crypttab.write(row)
def unlock_luks2_dev(
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

196
archinstall/lib/disk/lvm.py Normal file
View File

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

View File

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

View File

@ -2,10 +2,10 @@ from pathlib import Path
from pydantic import BaseModel from pydantic import BaseModel
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import DiskError, SysCallError from archinstall.lib.exceptions import DiskError, SysCallError
from archinstall.lib.general import SysCommand from archinstall.lib.log import debug, info, warn
from archinstall.lib.models.device import LsblkInfo from archinstall.lib.models.device import LsblkInfo
from archinstall.lib.output import debug, warn
class LsblkOutput(BaseModel): class LsblkOutput(BaseModel):
@ -67,12 +67,12 @@ def get_lsblk_output() -> LsblkOutput:
def find_lsblk_info( def find_lsblk_info(
dev_path: Path | str, dev_path: Path | str,
info: list[LsblkInfo], info_list: list[LsblkInfo],
) -> LsblkInfo | None: ) -> LsblkInfo | None:
if isinstance(dev_path, str): if isinstance(dev_path, str):
dev_path = Path(dev_path) dev_path = Path(dev_path)
for lsblk_info in info: for lsblk_info in info_list:
if lsblk_info.path == dev_path: if lsblk_info.path == dev_path:
return lsblk_info return lsblk_info
@ -110,6 +110,69 @@ def disk_layouts() -> str:
return lsblk_output.model_dump_json(indent=4) 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: def umount(mountpoint: Path, recursive: bool = False) -> None:
lsblk_info = get_lsblk_info(mountpoint) lsblk_info = get_lsblk_info(mountpoint)
@ -126,3 +189,10 @@ def umount(mountpoint: Path, recursive: bool = False) -> None:
for path in lsblk_info.mountpoints: for path in lsblk_info.mountpoints:
debug(f'Unmounting mountpoint: {path}') debug(f'Unmounting mountpoint: {path}')
SysCommand(cmd + [str(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

@ -0,0 +1,17 @@
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

@ -0,0 +1,149 @@
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

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

View File

@ -2,12 +2,13 @@ import os
from enum import Enum from enum import Enum
from functools import cached_property from functools import cached_property
from pathlib import Path from pathlib import Path
from typing import Self
from .exceptions import SysCallError from archinstall.lib.command import SysCommand
from .general import SysCommand from archinstall.lib.exceptions import SysCallError
from .networking import enrich_iface_types, list_interfaces from archinstall.lib.log import debug
from .output import debug from archinstall.lib.networking import enrich_iface_types, list_interfaces
from .translationhandler import tr from archinstall.lib.translationhandler import tr
class CpuVendor(Enum): class CpuVendor(Enum):
@ -16,7 +17,7 @@ class CpuVendor(Enum):
_Unknown = 'unknown' _Unknown = 'unknown'
@classmethod @classmethod
def get_vendor(cls, name: str) -> 'CpuVendor': def get_vendor(cls, name: str) -> Self:
if vendor := getattr(cls, name, None): if vendor := getattr(cls, name, None):
return vendor return vendor
else: else:
@ -40,10 +41,9 @@ class GfxPackage(Enum):
Dkms = 'dkms' Dkms = 'dkms'
IntelMediaDriver = 'intel-media-driver' IntelMediaDriver = 'intel-media-driver'
LibvaIntelDriver = 'libva-intel-driver' LibvaIntelDriver = 'libva-intel-driver'
LibvaMesaDriver = 'libva-mesa-driver'
LibvaNvidiaDriver = 'libva-nvidia-driver' LibvaNvidiaDriver = 'libva-nvidia-driver'
Mesa = 'mesa' Mesa = 'mesa'
NvidiaDkms = 'nvidia-dkms' NvidiaOpen = 'nvidia-open'
NvidiaOpenDkms = 'nvidia-open-dkms' NvidiaOpenDkms = 'nvidia-open-dkms'
VulkanIntel = 'vulkan-intel' VulkanIntel = 'vulkan-intel'
VulkanRadeon = 'vulkan-radeon' VulkanRadeon = 'vulkan-radeon'
@ -51,8 +51,6 @@ class GfxPackage(Enum):
Xf86VideoAmdgpu = 'xf86-video-amdgpu' Xf86VideoAmdgpu = 'xf86-video-amdgpu'
Xf86VideoAti = 'xf86-video-ati' Xf86VideoAti = 'xf86-video-ati'
Xf86VideoNouveau = 'xf86-video-nouveau' Xf86VideoNouveau = 'xf86-video-nouveau'
XorgServer = 'xorg-server'
XorgXinit = 'xorg-xinit'
class GfxDriver(Enum): class GfxDriver(Enum):
@ -61,12 +59,24 @@ class GfxDriver(Enum):
IntelOpenSource = 'Intel (open-source)' IntelOpenSource = 'Intel (open-source)'
NvidiaOpenKernel = 'Nvidia (open kernel module for newer GPUs, Turing+)' NvidiaOpenKernel = 'Nvidia (open kernel module for newer GPUs, Turing+)'
NvidiaOpenSource = 'Nvidia (open-source nouveau driver)' NvidiaOpenSource = 'Nvidia (open-source nouveau driver)'
NvidiaProprietary = 'Nvidia (proprietary)'
VMOpenSource = 'VirtualBox (open-source)' VMOpenSource = 'VirtualBox (open-source)'
def is_nvidia(self) -> bool: def is_nvidia(self) -> bool:
match self: match self:
case GfxDriver.NvidiaProprietary | GfxDriver.NvidiaOpenSource | GfxDriver.NvidiaOpenKernel: 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:
return True return True
case _: case _:
return False return False
@ -76,12 +86,12 @@ class GfxDriver(Enum):
text = tr('Installed packages') + ':\n' text = tr('Installed packages') + ':\n'
for p in sorted(pkg_names): for p in sorted(pkg_names):
text += f'\t- {p}\n' text += f' - {p}\n'
return text return text
def gfx_packages(self) -> list[GfxPackage]: def gfx_packages(self) -> list[GfxPackage]:
packages = [GfxPackage.XorgServer, GfxPackage.XorgXinit] packages: list[GfxPackage] = []
match self: match self:
case GfxDriver.AllOpenSource: case GfxDriver.AllOpenSource:
@ -90,7 +100,6 @@ class GfxDriver(Enum):
GfxPackage.Xf86VideoAmdgpu, GfxPackage.Xf86VideoAmdgpu,
GfxPackage.Xf86VideoAti, GfxPackage.Xf86VideoAti,
GfxPackage.Xf86VideoNouveau, GfxPackage.Xf86VideoNouveau,
GfxPackage.LibvaMesaDriver,
GfxPackage.LibvaIntelDriver, GfxPackage.LibvaIntelDriver,
GfxPackage.IntelMediaDriver, GfxPackage.IntelMediaDriver,
GfxPackage.VulkanRadeon, GfxPackage.VulkanRadeon,
@ -102,7 +111,6 @@ class GfxDriver(Enum):
GfxPackage.Mesa, GfxPackage.Mesa,
GfxPackage.Xf86VideoAmdgpu, GfxPackage.Xf86VideoAmdgpu,
GfxPackage.Xf86VideoAti, GfxPackage.Xf86VideoAti,
GfxPackage.LibvaMesaDriver,
GfxPackage.VulkanRadeon, GfxPackage.VulkanRadeon,
] ]
case GfxDriver.IntelOpenSource: case GfxDriver.IntelOpenSource:
@ -122,15 +130,8 @@ class GfxDriver(Enum):
packages += [ packages += [
GfxPackage.Mesa, GfxPackage.Mesa,
GfxPackage.Xf86VideoNouveau, GfxPackage.Xf86VideoNouveau,
GfxPackage.LibvaMesaDriver,
GfxPackage.VulkanNouveau, GfxPackage.VulkanNouveau,
] ]
case GfxDriver.NvidiaProprietary:
packages += [
GfxPackage.NvidiaDkms,
GfxPackage.Dkms,
GfxPackage.LibvaNvidiaDriver,
]
case GfxDriver.VMOpenSource: case GfxDriver.VMOpenSource:
packages += [ packages += [
GfxPackage.Mesa, GfxPackage.Mesa,
@ -143,6 +144,18 @@ class _SysInfo:
def __init__(self) -> None: def __init__(self) -> None:
pass 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 @cached_property
def cpu_info(self) -> dict[str, str]: def cpu_info(self) -> dict[str, str]:
""" """
@ -193,11 +206,27 @@ class _SysInfo:
return modules 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() _sys_info = _SysInfo()
class SysInfo: class SysInfo:
@staticmethod
def has_battery() -> bool:
return _sys_info.has_battery
@staticmethod @staticmethod
def has_wifi() -> bool: def has_wifi() -> bool:
ifaces = list(list_interfaces().values()) ifaces = list(list_interfaces().values())
@ -209,24 +238,19 @@ class SysInfo:
@staticmethod @staticmethod
def _graphics_devices() -> dict[str, str]: def _graphics_devices() -> dict[str, str]:
cards: dict[str, str] = {} return _sys_info.graphics_devices
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 @staticmethod
def has_nvidia_graphics() -> bool: def has_nvidia_graphics() -> bool:
return any('nvidia' in x.lower() for x in SysInfo._graphics_devices()) return any('nvidia' in x.lower() for x in _sys_info.graphics_devices)
@staticmethod @staticmethod
def has_amd_graphics() -> bool: def has_amd_graphics() -> bool:
return any('amd' in x.lower() for x in SysInfo._graphics_devices()) return any('amd' in x.lower() for x in _sys_info.graphics_devices)
@staticmethod @staticmethod
def has_intel_graphics() -> bool: def has_intel_graphics() -> bool:
return any('intel' in x.lower() for x in SysInfo._graphics_devices()) return any('intel' in x.lower() for x in _sys_info.graphics_devices)
@staticmethod @staticmethod
def cpu_vendor() -> CpuVendor | None: def cpu_vendor() -> CpuVendor | None:
@ -239,14 +263,20 @@ class SysInfo:
return _sys_info.cpu_info.get('model name', None) return _sys_info.cpu_info.get('model name', None)
@staticmethod @staticmethod
def sys_vendor() -> str: def sys_vendor() -> str | None:
try:
with open('/sys/devices/virtual/dmi/id/sys_vendor') as vendor: with open('/sys/devices/virtual/dmi/id/sys_vendor') as vendor:
return vendor.read().strip() return vendor.read().strip()
except FileNotFoundError:
return None
@staticmethod @staticmethod
def product_name() -> str: def product_name() -> str | None:
try:
with open('/sys/devices/virtual/dmi/id/product_name') as product: with open('/sys/devices/virtual/dmi/id/product_name') as product:
return product.read().strip() return product.read().strip()
except FileNotFoundError:
return None
@staticmethod @staticmethod
def mem_available() -> int: def mem_available() -> int:

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +0,0 @@
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

@ -1,633 +0,0 @@
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

@ -1,305 +0,0 @@
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,188 +0,0 @@
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