Compare commits

...

80 Commits
4.4 ... master

Author SHA1 Message Date
Victor Zacarias e24af61e22
Fix examples/config-sample.json and cover the example configs with a test (#4709)
* Fix examples/config-sample.json to match the current config format

The sample config has not been valid since the 2023 disk layout rework
and currently fails to parse, so the file the README points users at
cannot be used:

- sector_size was null, but SectorSize became a required object
- partitions were missing the dev_path key, which the parser reads
- the /home size used the Percent unit, which no longer exists
- /home started at 20 GiB while / ended at 20993 MiB, overlapping it
- gfx_driver kept the old "All open-source (default)" value

Sizes and keys now mirror what the installer itself writes when saving
a configuration.

* Cover the example configs with a parsing test

Nothing in the test suite or CI reads examples/, which is how the sample
config could stay broken for years while every other config surface kept
working. Parse both example files through ArchConfigHandler, and check
the partition entries directly since the parser only reaches them when
the configured device exists on the machine, which is never true in CI.

* Fail the partition test when the example config has none

The loops in test_example_config_partitions would not execute if
device_modifications or a device's partitions were empty, so the test
could pass while checking nothing.
2026-08-13 10:25:05 +10:00
h8d13 8157685d04
feat(kernels): add RT variants from upstream (#4711)
* feat(kernels): add `RT` variants from upstream

* https://archlinux.org/packages/?sort=&q=linux-rt

Signed-off-by: h8d13 <hadean-eon-dev@proton.me>

* chore(fmt): use tab

Signed-off-by: h8d13 <hadean-eon-dev@proton.me>

* fixup! feat(kernels): add `RT` variants from upstream

---------

Signed-off-by: h8d13 <hadean-eon-dev@proton.me>
2026-08-13 10:24:39 +10:00
h8d13 3ffa1a98a0
chore(#4705): continue parent (#4713)
as per: https://wiki.archlinux.org/title/Hardware_video_acceleration#Intel_Video_Processing_Library_(Intel_VPL)

Signed-off-by: h8d13 <hadean-eon-dev@proton.me>
2026-08-13 09:50:27 +10:00
h8d13 5ebdd27ceb
chore(desktops): remove print/power #4618 follow-up #4554 (#4707)
Signed-off-by: h8d13 <hadean-eon-dev@proton.me>
2026-08-12 09:55:49 +10:00
h8d13 f0268ec8ff
chore(servers): update `cockpit` #4688 (#4706)
Signed-off-by: h8d13 <hadean-eon-dev@proton.me>
2026-08-12 09:55:22 +10:00
codefiles 2a83b502da
Revert "feat(disk): add custom cipher selection for LUKS2 encryption (#4581)" (#4708)
This reverts commit 7b7fd6573e.
2026-08-11 22:03:19 +10:00
h8d13 426e273265
fix(hw): intel open-source post Gen12 needs this for ffpmeg, OBS, etc (#4705)
* fix(hw): intel open-source post Gen12 needs this for ffpmeg, OBS, etc

Signed-off-by: h8d13 <hadean-eon-dev@proton.me>

* chore(docs): stray whitespaces and missing `

Signed-off-by: h8d13 <hadean-eon-dev@proton.me>

---------

Signed-off-by: h8d13 <hadean-eon-dev@proton.me>
2026-08-11 20:56:42 +10:00
Alperen42v 7b7fd6573e
feat(disk): add custom cipher selection for LUKS2 encryption (#4581)
* feat(disk): fix menu rendering and complete backend data pipeline for custom LUKS2 cipher

* fix : removed chacha20

* fix the problems for filesystems

* ruff test fix

* luksfix xfs file system fix cipher

* ruff fix

* new ciphers

* yes yes ruff fix

* Device.py : removed bloat commets

* refactor(disk): address review feedback on cipher selection

- always serialize iter_time and cipher in DiskEncryption.json
- simplify select_encryption_cipher using MenuItemGroup.from_enum
- add cipher to _prev_disk_encryption preview in disk_menu.py
- drop unnecessary inline comments from EncryptionCipher enum
2026-08-11 20:18:42 +10:00
codefiles f4e0d4cf66
Refactor meminfo (#4698) 2026-08-11 13:23:29 +10:00
Martin d571283d69
fix: Stop hardcoding dependency versions (#4695)
Fixes #4694
2026-08-11 11:56:47 +10:00
dependabot[bot] dd6ea5f47b
Bump cryptography from 49.0.0 to 50.0.0 (#4703)
Bumps [cryptography](https://github.com/pyca/cryptography) from 49.0.0 to 50.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/49.0.0...50.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 20:57:36 +10:00
renovate[bot] 578934e4ef
Update dependency ruff to v0.16.2 (#4699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 09:21:31 +00:00
renovate[bot] 233e411c14
Update pre-commit hook astral-sh/ruff-pre-commit to v0.16.2 (#4700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 19:21:02 +10:00
renovate[bot] 121c07fe41
Update dependency pylint to v4.0.7 (#4702)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 19:20:20 +10:00
Nathan Marley caf9cc8289
Fix WiFi SSIDs with spaces missing from scan list (#4634)
* Add tests for SSIDs with space

* Fix WiFi scan drop for spaced SSIDs

The wpa_cli scan_results puts the SSID last, splitting on all whitespace dropped
any network whose name contained spaces.
2026-08-10 19:01:45 +10:00
codefiles 9a19e39a25
Use staticmethod for get_type_from_code (#4693)
Addresses the following:

pyright

```
  archinstall/lib/models/device.py:759:11 - error: Type "Literal[PartitionType.PRIMARY]" is not assignable to return type "Self@PartitionType"
    Type "Literal[PartitionType.PRIMARY]" is not assignable to type "Self@PartitionType" (reportReturnType)
  archinstall/lib/models/device.py:762:11 - error: Type "Literal[PartitionType._UNKNOWN]" is not assignable to return type "Self@PartitionType"
    Type "Literal[PartitionType._UNKNOWN]" is not assignable to type "Self@PartitionType" (reportReturnType)
```

pyrefly

```
ERROR Returned type `Literal[PartitionType.PRIMARY]` is not assignable to declared return type `Self@PartitionType` [bad-return]
   --> archinstall/lib/models/device.py:759:11
    |
757 |     def get_type_from_code(cls, code: int) -> Self:
    |                                               ---- declared return type
758 |         if code == parted.PARTITION_NORMAL:
759 |             return cls.PRIMARY
    |                    ^^^^^^^^^^^
    |
ERROR Returned type `Literal[PartitionType._UNKNOWN]` is not assignable to declared return type `Self@PartitionType` [bad-return]
   --> archinstall/lib/models/device.py:762:11
    |
757 |     def get_type_from_code(cls, code: int) -> Self:
    |                                               ---- declared return type
758 |         if code == parted.PARTITION_NORMAL:
759 |             return cls.PRIMARY
760 |         else:
761 |             debug(f'Partition code not supported: {code}')
762 |             return cls._UNKNOWN
    |                    ^^^^^^^^^^^^
    |
```

ty

```
error[invalid-return-type]: Return type does not match returned value
   --> archinstall/lib/models/device.py:757:44
    |
757 |     def get_type_from_code(cls, code: int) -> Self:
    |                                               ---- Expected `Self@get_type_from_code` because of return type
758 |         if code == parted.PARTITION_NORMAL:
759 |             return cls.PRIMARY
    |                    ^^^^^^^^^^^ expected `Self@get_type_from_code`, found `Literal[PartitionType.PRIMARY]`
    |

error[invalid-return-type]: Return type does not match returned value
   --> archinstall/lib/models/device.py:757:44
    |
757 |     def get_type_from_code(cls, code: int) -> Self:
    |                                               ---- Expected `Self@get_type_from_code` because of return type
758 |         if code == parted.PARTITION_NORMAL:
759 |             return cls.PRIMARY
760 |         else:
761 |             debug(f'Partition code not supported: {code}')
762 |             return cls._UNKNOWN
    |                    ^^^^^^^^^^^^ expected `Self@get_type_from_code`, found `Literal[PartitionType._UNKNOWN]`
    |
```
2026-08-04 06:31:06 +10:00
codefiles 34b3cecced
Refactor TranslationHandler locales directory (#4692) 2026-08-03 21:39:22 +10:00
codefiles 0d6ffc3795
Refactor default PartitionTable (#4689)
Addresses the following:

pyright

```
  archinstall/lib/models/device.py:262:10 - error: Type "Literal[PartitionTable.GPT, PartitionTable.MBR]" is not assignable to return type "Self@PartitionTable"
    Type "Literal[PartitionTable.GPT, PartitionTable.MBR]" is not assignable to type "Self@PartitionTable" (reportReturnType)
```

ty

```
error[invalid-return-type]: Return type does not match returned value
   --> archinstall/lib/models/device.py:261:22
    |
261 |     def default(cls) -> Self:
    |                         ---- Expected `Self@default` because of return type
262 |         return cls.GPT if SysInfo.has_uefi() else cls.MBR
    |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Self@default`, found `PartitionTable`
    |
```
2026-08-03 21:38:20 +10:00
codefiles c7e512a294
Refactor CPUVendor (#4691)
Addresses the following:

pyright

```
  archinstall/lib/hardware.py:25:11 - error: Type "Literal[CpuVendor._Unknown]" is not assignable to return type "Self@CpuVendor"
    Type "Literal[CpuVendor._Unknown]" is not assignable to type "Self@CpuVendor" (reportReturnType)
```

pyrefly

```
ERROR Returned type `Literal[CpuVendor._Unknown]` is not assignable to declared return type `Self@CpuVendor` [bad-return]
  --> archinstall/lib/hardware.py:25:11
   |
20 |     def get_vendor(cls, name: str) -> Self:
   |                                       ---- declared return type
21 |         if vendor := getattr(cls, name, None):
22 |             return vendor
23 |         else:
24 |             debug(f"Unknown CPU vendor '{name}' detected.")
25 |             return cls._Unknown
   |                    ^^^^^^^^^^^^
   |
```

ty

```
error[invalid-return-type]: Return type does not match returned value
  --> archinstall/lib/hardware.py:20:36
   |
20 |     def get_vendor(cls, name: str) -> Self:
   |                                       ---- Expected `Self@get_vendor` because of return type
21 |         if vendor := getattr(cls, name, None):
22 |             return vendor
23 |         else:
24 |             debug(f"Unknown CPU vendor '{name}' detected.")
25 |             return cls._Unknown
   |                    ^^^^^^^^^^^^ expected `Self@get_vendor`, found `Literal[CpuVendor._Unknown]`
   |
```
2026-08-03 21:37:09 +10:00
codefiles c7da3cd8b9
Use staticmethod for Bootloader.get_default (#4690)
Addresses the following:

pyright

```
  archinstall/lib/models/bootloader.py:42:11 - error: Type "Literal[Bootloader.NO_BOOTLOADER]" is not assignable to return type "Self@Bootloader"
    Type "Literal[Bootloader.NO_BOOTLOADER]" is not assignable to type "Self@Bootloader" (reportReturnType)
  archinstall/lib/models/bootloader.py:44:11 - error: Type "Literal[Bootloader.Systemd]" is not assignable to return type "Self@Bootloader"
    Type "Literal[Bootloader.Systemd]" is not assignable to type "Self@Bootloader" (reportReturnType)
  archinstall/lib/models/bootloader.py:46:11 - error: Type "Literal[Bootloader.Grub]" is not assignable to return type "Self@Bootloader"
    Type "Literal[Bootloader.Grub]" is not assignable to type "Self@Bootloader" (reportReturnType)
```

pyrefly

```
ERROR Returned type `Literal[Bootloader.NO_BOOTLOADER]` is not assignable to declared return type `Self@Bootloader` [bad-return]
  --> archinstall/lib/models/bootloader.py:42:11
   |
40 |     def get_default(cls, uefi: bool, skip_boot: bool = False) -> Self:
   |                                                                  ---- declared return type
41 |         if skip_boot:
42 |             return cls.NO_BOOTLOADER
   |                    ^^^^^^^^^^^^^^^^^
   |
ERROR Returned type `Literal[Bootloader.Systemd]` is not assignable to declared return type `Self@Bootloader` [bad-return]
  --> archinstall/lib/models/bootloader.py:44:11
   |
40 |     def get_default(cls, uefi: bool, skip_boot: bool = False) -> Self:
   |                                                                  ---- declared return type
41 |         if skip_boot:
42 |             return cls.NO_BOOTLOADER
43 |         elif uefi:
44 |             return cls.Systemd
   |                    ^^^^^^^^^^^
   |
ERROR Returned type `Literal[Bootloader.Grub]` is not assignable to declared return type `Self@Bootloader`
[bad-return]
  --> archinstall/lib/models/bootloader.py:46:11
   |
40 |     def get_default(cls, uefi: bool, skip_boot: bool = False) -> Self:
   |                                                                  ---- declared return type
41 |         if skip_boot:
42 |             return cls.NO_BOOTLOADER
43 |         elif uefi:
44 |             return cls.Systemd
45 |         else:
46 |             return cls.Grub
   |                    ^^^^^^^^
   |
```

ty

```
error[invalid-return-type]: Return type does not match returned value
  --> archinstall/lib/models/bootloader.py:40:63
   |
40 |     def get_default(cls, uefi: bool, skip_boot: bool = False) -> Self:
   |                                                                  ---- Expected `Self@get_default` because of return type
41 |         if skip_boot:
42 |             return cls.NO_BOOTLOADER
   |                    ^^^^^^^^^^^^^^^^^ expected `Self@get_default`, found `Literal[Bootloader.NO_BOOTLOADER]`
   |

error[invalid-return-type]: Return type does not match returned value
  --> archinstall/lib/models/bootloader.py:40:63
   |
40 |     def get_default(cls, uefi: bool, skip_boot: bool = False) -> Self:
   |                                                                  ---- Expected `Self@get_default` because of return type
41 |         if skip_boot:
42 |             return cls.NO_BOOTLOADER
43 |         elif uefi:
44 |             return cls.Systemd
   |                    ^^^^^^^^^^^ expected `Self@get_default`, found `Literal[Bootloader.Systemd]`
   |

error[invalid-return-type]: Return type does not match returned value
  --> archinstall/lib/models/bootloader.py:46:11
   |
46 |             return cls.Grub
   |                    ^^^^^^^^ expected `Self@get_default`, found `Literal[Bootloader.Grub]`
   |
  ::: archinstall/lib/models/bootloader.py:40:63
   |
40 |     def get_default(cls, uefi: bool, skip_boot: bool = False) -> Self:
   |                                                                  ---- Expected `Self@get_default` because of return type
   |
```
2026-08-03 21:35:39 +10:00
renovate[bot] d43da6a1c2
Update pre-commit hook astral-sh/ruff-pre-commit to v0.16.1 (#4684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-31 22:15:46 +10:00
renovate[bot] 3c56d9735e
Update dependency ruff to v0.16.1 (#4683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-31 22:15:25 +10:00
Sanjay Santhanam 0f7c5c58fb
fix(applications): add ghostscript to print service packages (#4670)
* fix(applications): add ghostscript to print service packages

Selecting the print service installs cups, system-config-printer and
cups-pk-helper, but not ghostscript. cups relies on ghostscript for the
PDF/PostScript rendering filter, so driverless (IPP Everywhere) print
jobs fail out of the box with:

  cfFilterGhostscript: Unable to launch Ghostscript: gs: No such file or directory

Add ghostscript to the package list and cover it with a regression test.

Closes #4595

* test: drop low-value print service package assertion
2026-07-31 22:15:00 +10:00
codefiles 3576eec234
Remove unused parameter from _prepare_fs_type (#4685) 2026-07-31 14:46:45 +10:00
codefiles 274f6c2190
Remove superfluous PacmanConfiguration.default (#4682) 2026-07-31 14:44:05 +10:00
codefiles c0affc895c
Remove superfluous underscore variable assignment (#4679) 2026-07-31 14:43:37 +10:00
codefiles 1d66204940
Remove reintroduced __future__ import (#4678) 2026-07-31 14:43:03 +10:00
Matteo a327efcd6d
Update Italian translation (#4675)
* Added new strings
* Updated strings
* Deleted some redundant unused strings
2026-07-28 19:15:01 +10:00
utuhiro78 88e524231b
Update Japanese translation (#4672)
* Update Japanese translation

* Update Japanese translation
2026-07-27 22:10:00 +10:00
codefiles c25589e772
Support AArch64 root partition type GUID (#4671) 2026-07-27 22:07:28 +10:00
codefiles bb0df33556
Deduplicate enabling systemd-resolved stub mode (#4669) 2026-07-27 22:05:20 +10:00
codefiles fe16238983
Refactor run_custom_user_commands (#4668) 2026-07-25 15:33:22 +10:00
codefiles c1dcce8c2c
Add constant for path /sys/class/net (#4667) 2026-07-25 15:32:22 +10:00
correctmost 615c3341dc
Enable flake8-gettext checks in ruff (#4664)
This will help catch incorrect translation placeholders, like those
fixed in commit dccd0c8cc.

This commit also removes custom checking code from the
locales_generator.sh script.
2026-07-25 08:01:39 +10:00
Steev Klimaszewski 3653fbd695
Support aarch64 for GRUB and Limine EFI installation (#4641)
On aarch64 both the GRUB and Limine installers assume x86 and fail.

GRUB names its EFI target 'arm64', but platform.machine() returns
'aarch64', so passing --target=aarch64-efi is rejected. Map aarch64
to the arm64 target name expected by grub-install.

Limine ships architecture-specific default EFI binaries: BOOTAA64.EFI
on aarch64 versus BOOTIA32.EFI and BOOTX64.EFI on x86. Select the
correct binaries in the three places that reference them: when copying
them into the ESP, when building the pacman hook that refreshes them on
update, and when choosing the 64-bit EFI boot menu loader path.

Co-authored-by: Steev Klimaszewski <steev@parrotsec.org>
2026-07-24 21:30:54 +10:00
renovate[bot] 11e45aebeb
Update pre-commit hook astral-sh/ruff-pre-commit to v0.16.0 (#4660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-24 12:31:30 +10:00
codefiles 4004593852
Refactor MirrorStatusEntryV3 field validator (#4661) 2026-07-24 10:01:03 +10:00
codefiles a68dcd6a28
Return bytes from fetch_data_from_url() (#4653) 2026-07-24 09:59:55 +10:00
renovate[bot] 639de7e953
Update dependency ruff to v0.16.0 (#4659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-24 09:58:32 +10:00
codefiles 39e0fb39d2
Refactor MirrorStatusListV3 model validator (#4657) 2026-07-23 22:08:31 +10:00
codefiles ed388cab3f
Enable non-empty-init-module ruff rule (#4656) 2026-07-23 22:07:38 +10:00
codefiles 865ff3f65a
Make type field in lsblk required (#4655) 2026-07-23 22:07:05 +10:00
Softer 7fd4b4382b
Extract TUI binding descriptions into base.pot and translate raw widget bindings (#4587)
* Extract binding descriptions into base.pot and translate raw widget bindings (#4584)

xgettext only knew the tr keyword, so binding descriptions - translated
at runtime via tr(b.description) in _translate_bindings() - never made
it into base.pot. The strings added manually in #4363 were wiped by the
regeneration in #4557.

Teach the generator two more keywords: Binding:3 picks up our own
binding definitions, and tr_noop marks strings that only exist inside
the textual package, listed in the new tui/binding_descriptions.py.
The list walks the full MRO of the textual classes used in components.py,
since _translate_bindings() operates on the merged bindings map (e.g.
Scroll Up comes from ScrollView and carries live translations in 18
locales today).

Raw Input and Button widgets never went through _translate_bindings(),
so their help-panel descriptions stayed English regardless of .po
content - subclass them (_Input/_Button) following the _OptionList
pattern to make those translations real.

test_tooling/check_binding_descriptions.py verifies the list against
the installed textual after upgrades (manual, not wired into CI).

* Document key binding descriptions in the locales README
2026-07-23 13:02:22 +10:00
codefiles a633fc5d27
Use run for lsblk command (#4654) 2026-07-22 18:25:55 +10:00
codefiles 6dff8525fd
Remove unused format_cols (#4652) 2026-07-22 10:01:55 +10:00
renovate[bot] b7ad6f7514
Update dependency pre-commit to v4.6.1 (#4650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 10:00:37 +10:00
renovate[bot] 05ceb5789f
Update actions/checkout digest to 3d3c42e (#4649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 10:00:02 +10:00
Franco Castillo fc7dd17ebd
Update Spanish translation (#4651)
Signed-off-by: Franco Castillo <castillofrancodamian@gmail.com>
2026-07-22 09:41:26 +10:00
codefiles 2bf116030b
Fix too-many-statements-in-try-clause violations (#4648)
* Fix too-many-statements-in-try-clause violations

* Remove obsolete _wpa_cli comments

* Refactor _wpa_cli try block
2026-07-22 09:39:42 +10:00
renovate[bot] 389f4cfc64
Update actions/setup-python action to v7 (#4647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-21 21:03:30 +10:00
Ksan 91d6d4dca1
Add Serbian translation (#4642) 2026-07-21 21:02:55 +10:00
codefiles 2f344a84ff
Remove redundant is_valid_path (#4646) 2026-07-21 20:50:56 +10:00
codefiles 2751716d2e
Set LANG to fully qualified locale name (#4645) 2026-07-21 20:44:10 +10:00
renovate[bot] b6d8fe25a1
Update dependency arch/python-markdown-it-py to v4.2.0 (#4644)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-21 20:39:28 +10:00
renovate[bot] b0bfb43661
Update dependency ruff to v0.15.22 (#4636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-18 21:17:15 +10:00
renovate[bot] 39dbebf26c
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.22 (#4637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-18 21:16:43 +10:00
Jonathan Pichel caff461f0b
Fix Galician locale (#4630)
* Sync Galician translation with template

* Delete all outdated Galician translation entries

* Improve Galician translation
2026-07-14 21:13:26 +10:00
Softer 01c9432afd
Use sentence case for summary labels (#4617)
str.title() title-cased every label word, which is wrong outside
English: it turned the Ukrainian "Ім'я хоста" into "Ім'Я Хоста" and
broke acronyms ("NTP" -> "Ntp"). The source strings and their
translations are already written with the correct casing.

Capitalize only the first letter of each label instead.
2026-07-14 21:12:53 +10:00
renovate[bot] 53158a8aad
Update pre-commit hook pre-commit/mirrors-mypy to v2.3.0 (#4629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14 10:14:03 +10:00
renovate[bot] 46320deb33
Update dependency mypy to v2.3.0 (#4628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14 10:13:43 +10:00
renovate[bot] 12baa2e64a
Update pre-commit hook pre-commit/mirrors-mypy to v2.2.0 (#4627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 13:09:55 +10:00
renovate[bot] 2531825b59
Update dependency mypy to v2.2.0 (#4626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 13:09:27 +10:00
renovate[bot] e3e9d43324
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.21 (#4623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 13:09:05 +10:00
renovate[bot] 781728b91b
Update dependency ruff to v0.15.21 (#4622)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 13:08:47 +10:00
renovate[bot] 34c299e42e
Update astral-sh/ruff-action action to v4.1.0 (#4625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 07:05:31 +10:00
renovate[bot] 3f4f815a34
Update dependency arch/python-textual to v8.2.8 (#4624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 07:04:41 +10:00
Christos Longros c60385a6f7
Update Greek translations: TUI navigation strings (#4621) 2026-07-12 21:21:58 +10:00
correctmost c110081a42
Enable more mypy and Pylint checks for test_tooling/ (#4620) 2026-07-12 21:21:18 +10:00
correctmost 8902d2e310
Use a match statement for PasswordStrength enum values (#4619)
This unblocks the upgrade to mypy 2.2.0, which now flags the
unreachable "return None" statement.
2026-07-12 21:20:44 +10:00
BlackCatOfficial 💀 (Python, C++, Lua) aca482b691
Add Vietnamese to Archinstall (#4582)
* add vn lang, trans lang, translating ~28%

* done 80/735 (10% not ~28% sorry) and 735/735 done (counted fuzzy or google translate helper). See https://gist.github.com/BlackCatOfficialytb/e85bad1758d9e29c349f2d75b91363f1 for program to help me translate

* doing plenty of things

* up to 40% by removing fuzzy tag in correct translate words/paragraph

* fix the extension

* more words/paragraphs are unfuzzied

* trans up to 57%

* translate up to 61%
2026-07-12 21:18:50 +10:00
renovate[bot] 338e3cf4b8
Update dependency ruff to v0.15.20 (#4609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-30 07:58:34 +10:00
renovate[bot] ec9295634b
Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.20 (#4610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-30 07:58:17 +10:00
renovate[bot] f94c27430c
Update dependency pytest to v9.1.1 (#4612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-30 07:58:02 +10:00
renovate[bot] caa8887204
Update dependency arch/python-systemd to v236 (#4615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-30 07:57:22 +10:00
renovate[bot] 8aec36aa0a
Update dependency arch/python-cryptography to v49 (#4614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-30 07:57:04 +10:00
renovate[bot] ebdf625e5c
Update actions/checkout action to v7 (#4613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-30 07:56:42 +10:00
Arvind fc3a22683e
Add Norwegian Bokmål (nb) translation (#4599)
* Add Norwegian Bokmål (nb) translation

* Register Norwegian Bokmål in languages.json
2026-06-29 21:52:02 +10:00
Softer faf6fe5ce2
Improve Ukrainian translation (#4611)
Translate the new Plymouth bootloader strings (enable warning prompt,
theme selection) and add the graphics driver label.
2026-06-29 21:46:30 +10:00
renovate[bot] e2b8697864
Update dependency pylint to v4.0.6 (#4606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-29 21:45:20 +10:00
renovate[bot] 214e55248c
Update actions/setup-python digest to ece7cb0 (#4605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-29 21:44:58 +10:00
74 changed files with 9596 additions and 4882 deletions

View File

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

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -21,8 +21,8 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
- name: Install pre-dependencies
run: |
pacman -Sy --noconfirm tree git python-pyparted python-setuptools python-sphinx python-sphinx_rtd_theme python-build python-installer python-wheel

View File

@ -26,7 +26,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- run: pwd
- run: find .
- run: cat /etc/os-release

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -7,7 +7,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -11,7 +11,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -18,7 +18,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -4,6 +4,6 @@ jobs:
ruff_format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
- run: ruff format --diff

View File

@ -4,5 +4,5 @@ jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0

View File

@ -15,7 +15,7 @@ jobs:
name: Validate translations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Install gettext
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Run translation checks

View File

@ -26,7 +26,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- run: pwd
- run: find .
- run: cat /etc/os-release

View File

@ -1,7 +1,7 @@
default_stages: ['pre-commit']
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.17
rev: v0.16.2
hooks:
# fix unused imports and sort them
- id: ruff
@ -31,7 +31,7 @@ repos:
args: [--config=.flake8]
fail_fast: true
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
rev: v2.3.0
hooks:
- id: mypy
args: [

View File

@ -27,7 +27,7 @@ Alternative ways to install are `git clone` the repository (and is better since
## Upgrade `archinstall` on live Arch ISO image
Upgrading archinstall on the ISO needs to be done via a full system upgrade using
Upgrading archinstall on the ISO needs to be done via a full system upgrade using
```shell
pacman -Syu
@ -45,7 +45,7 @@ Assuming you are on an Arch Linux live-ISO or installed via `pip`, `archinstall`
```shell
archinstall
```
similar goes for running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer using `git
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
@ -60,7 +60,7 @@ archinstall --script <name>
```
#### Advanced
Some additional options that most users do not need are hidden behind the `--advanced` flag and all options/args can be consulted through `-h` or `--help`.
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
@ -179,7 +179,7 @@ This can be done by installing `pacman -S arch-install-scripts util-linux` local
# losetup --partscan --show ./testimage.img
# pip install --upgrade archinstall
# python -m archinstall --script guided
# qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd
# qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/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>
`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>

View File

@ -9,7 +9,7 @@ if TYPE_CHECKING:
class PrintServiceApp:
@property
def packages(self) -> list[str]:
return ['cups', 'system-config-printer', 'cups-pk-helper']
return ['cups', 'system-config-printer', 'cups-pk-helper', 'ghostscript']
@property
def services(self) -> list[str]:

View File

@ -34,8 +34,6 @@ class NiriDmsProfile(Profile):
'matugen',
'cava',
'kimageformats',
'cups-pk-helper',
'tuned-ppd',
_TERMINAL,
]

View File

@ -13,7 +13,7 @@ class CockpitProfile(Profile):
@property
@override
def packages(self) -> list[str]:
return ['cockpit', 'udisks2', 'packagekit']
return ['cockpit', 'cockpit-storaged', 'cockpit-packagekit']
@property
@override

View File

@ -35,7 +35,6 @@ from archinstall.lib.models.users import Password, User, UserSerialization
from archinstall.lib.plugins import load_plugin
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_key_value_pair
from archinstall.lib.utils.util import is_valid_path
from archinstall.lib.version import get_version
from archinstall.tui.components import tui
@ -169,7 +168,7 @@ class ArchConfig:
kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
ntp: bool = True
packages: list[str] = field(default_factory=list)
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration)
timezone: str = 'UTC'
services: list[str] = field(default_factory=list)
custom_commands: list[str] = field(default_factory=list)
@ -404,7 +403,7 @@ class ArchConfig:
) -> None:
save_path = dest_path or DEFAULT_SAVE_PATH
if not is_valid_path(save_path):
if not save_path.is_dir():
warn(
f'Destination directory {save_path} does not exist or is not a directory\n.',
'Configuration files can not be saved',
@ -416,7 +415,7 @@ class ArchConfig:
self.save_user_creds(save_path, password=password)
def save_user_config(self, dest_path: Path) -> None:
if not is_valid_path(dest_path):
if not dest_path.is_dir():
error(f'Invalid path {dest_path}. User configuration could not be saved.')
return
@ -430,7 +429,7 @@ class ArchConfig:
dest_path: Path,
password: str | None = None,
) -> None:
if not is_valid_path(dest_path):
if not dest_path.is_dir():
error(f'Invalid path {dest_path}. User credentials could not be saved.')
return

View File

@ -1,5 +1,6 @@
import logging
import os
import platform
from pathlib import Path
from parted import Device, Disk, DiskException, FileSystem, Geometry, IOException, Partition, PartitionException, freshDisk, getAllDevices, getDevice, newDisk
@ -10,11 +11,13 @@ from archinstall.lib.disk.utils import (
find_lsblk_info,
get_all_lsblk_info,
get_lsblk_info,
linux_root_guid,
mount,
udev_sync,
umount,
)
from archinstall.lib.exceptions import DiskError, SysCallError, UnknownFilesystemFormat
from archinstall.lib.hardware import SysInfo
from archinstall.lib.log import debug, error, info, log
from archinstall.lib.models.device import (
DEFAULT_ITER_TIME,
@ -26,7 +29,6 @@ from archinstall.lib.models.device import (
LsblkInfo,
ModificationStatus,
PartitionFlag,
PartitionGUID,
PartitionModification,
PartitionTable,
SubvolumeModification,
@ -44,7 +46,7 @@ class DeviceHandler:
def __init__(self) -> None:
self._devices: dict[Path, BDevice] = {}
self._partition_table = PartitionTable.default()
self._partition_table = PartitionTable.GPT if SysInfo.has_uefi() else PartitionTable.MBR
self.load_devices()
@property
@ -339,6 +341,7 @@ class DeviceHandler:
block_device: BDevice,
disk: Disk,
requires_delete: bool,
arch: str | None = None,
) -> None:
# when we require a delete and the partition to be (re)created
# already exists then we have to delete it first
@ -394,7 +397,7 @@ class DeviceHandler:
if disk.type == PartitionTable.GPT.value:
if part_mod.is_root():
partition.type_uuid = PartitionGUID.LINUX_ROOT_X86_64.bytes
partition.type_uuid = linux_root_guid(arch).bytes
elif PartitionFlag.LINUX_HOME not in part_mod.flags and part_mod.is_home():
partition.setFlag(PartitionFlag.LINUX_HOME.flag_id)
@ -536,11 +539,19 @@ class DeviceHandler:
# don't touch existing partitions
filtered_part = [p for p in modification.partitions if not p.exists()]
arch = platform.machine()
for part_mod in filtered_part:
# if the entire disk got nuked then we don't have to delete
# any existing partitions anymore because they're all gone already
requires_delete = modification.wipe is False
self._setup_partition(part_mod, modification.device, disk, requires_delete=requires_delete)
self._setup_partition(
part_mod,
modification.device,
disk,
requires_delete=requires_delete,
arch=arch,
)
disk.commit()

View File

@ -1,11 +1,12 @@
from pathlib import Path
from subprocess import CalledProcessError
from pydantic import BaseModel
from archinstall.lib.command import SysCommand
from archinstall.lib.command import SysCommand, run
from archinstall.lib.exceptions import DiskError, SysCallError
from archinstall.lib.log import debug, info, warn
from archinstall.lib.models.device import LsblkInfo
from archinstall.lib.models.device import LsblkInfo, PartitionGUID
class LsblkOutput(BaseModel):
@ -29,19 +30,18 @@ def _fetch_lsblk_info(
cmd.append(str(dev_path))
try:
worker = SysCommand(cmd)
except SysCallError as err:
result = run(cmd)
except CalledProcessError as err:
# Get the output minus the message/info from lsblk if it returns a non-zero exit code.
if err.worker_log:
debug(f'Error calling lsblk: {err.worker_log.decode()}')
if stdout := err.stdout:
debug(f'Error calling lsblk: {stdout.decode().rstrip()}')
if dev_path:
raise DiskError(f'Failed to read disk "{dev_path}" with lsblk')
raise err
output = worker.output(remove_cr=False)
return LsblkOutput.model_validate_json(output)
return LsblkOutput.model_validate_json(result.stdout)
def get_lsblk_info(
@ -196,3 +196,10 @@ def swapon(path: Path) -> None:
SysCommand(['swapon', str(path)])
except SysCallError as err:
raise DiskError(f'Could not enable swap {path}:\n{err.message}')
def linux_root_guid(arch: str | None) -> PartitionGUID:
if arch == 'aarch64':
return PartitionGUID.LINUX_ROOT_AARCH64
return PartitionGUID.LINUX_ROOT_X86_64

View File

@ -146,7 +146,7 @@ class GlobalMenu(AbstractMenu[None]):
MenuItem(
text=tr('Pacman'),
action=self._pacman_configuration,
value=PacmanConfiguration.default(),
value=PacmanConfiguration(),
preview_action=self._prev_pacman_config,
key='pacman_config',
),

View File

@ -1,8 +1,8 @@
import os
from enum import Enum
from dataclasses import dataclass
from enum import Enum, StrEnum
from functools import cached_property
from pathlib import Path
from typing import Self
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError
@ -11,29 +11,21 @@ from archinstall.lib.networking import enrich_iface_types, list_interfaces
from archinstall.lib.translationhandler import tr
class CpuVendor(Enum):
AuthenticAMD = 'amd'
GenuineIntel = 'intel'
_Unknown = 'unknown'
@classmethod
def get_vendor(cls, name: str) -> Self:
if vendor := getattr(cls, name, None):
return vendor
else:
debug(f"Unknown CPU vendor '{name}' detected.")
return cls._Unknown
class CPUVendor(StrEnum):
AMD = 'AuthenticAMD'
INTEL = 'GenuineIntel'
_UNKNOWN = 'unknown'
def _has_microcode(self) -> bool:
match self:
case CpuVendor.AuthenticAMD | CpuVendor.GenuineIntel:
case CPUVendor.AMD | CPUVendor.INTEL:
return True
case _:
return False
def get_ucode(self) -> Path | None:
if self._has_microcode():
return Path(self.value + '-ucode.img')
return Path(self.name.lower() + '-ucode.img')
return None
@ -41,6 +33,8 @@ class GfxPackage(Enum):
Dkms = 'dkms'
IntelMediaDriver = 'intel-media-driver'
LibvaIntelDriver = 'libva-intel-driver'
VplGpuRt = 'vpl-gpu-rt'
LibVpl = 'libvpl'
LibvaNvidiaDriver = 'libva-nvidia-driver'
Mesa = 'mesa'
NvidiaOpen = 'nvidia-open'
@ -102,6 +96,8 @@ class GfxDriver(Enum):
GfxPackage.Xf86VideoNouveau,
GfxPackage.LibvaIntelDriver,
GfxPackage.IntelMediaDriver,
GfxPackage.VplGpuRt,
GfxPackage.LibVpl,
GfxPackage.VulkanRadeon,
GfxPackage.VulkanIntel,
GfxPackage.VulkanNouveau,
@ -118,6 +114,8 @@ class GfxDriver(Enum):
GfxPackage.Mesa,
GfxPackage.LibvaIntelDriver,
GfxPackage.IntelMediaDriver,
GfxPackage.VplGpuRt,
GfxPackage.LibVpl,
GfxPackage.VulkanIntel,
]
case GfxDriver.NvidiaOpenKernel:
@ -172,25 +170,6 @@ class _SysInfo:
return cpu
@cached_property
def mem_info(self) -> dict[str, int]:
"""
Returns system memory information
"""
mem_info_path = Path('/proc/meminfo')
mem_info: dict[str, int] = {}
with mem_info_path.open() as file:
for line in file:
key, value = line.strip().split(':')
num = value.split()[0]
mem_info[key] = int(num)
return mem_info
def mem_info_by_key(self, key: str) -> int:
return self.mem_info[key]
@cached_property
def loaded_modules(self) -> list[str]:
"""
@ -253,9 +232,13 @@ class SysInfo:
return any('intel' in x.lower() for x in _sys_info.graphics_devices)
@staticmethod
def cpu_vendor() -> CpuVendor | None:
def cpu_vendor() -> CPUVendor | None:
if vendor := _sys_info.cpu_info.get('vendor_id'):
return CpuVendor.get_vendor(vendor)
try:
return CPUVendor(vendor)
except ValueError:
debug(f"Unknown CPU vendor '{vendor}' detected.")
return CPUVendor._UNKNOWN
return None
@staticmethod
@ -278,18 +261,6 @@ class SysInfo:
except FileNotFoundError:
return None
@staticmethod
def mem_available() -> int:
return _sys_info.mem_info_by_key('MemAvailable')
@staticmethod
def mem_free() -> int:
return _sys_info.mem_info_by_key('MemFree')
@staticmethod
def mem_total() -> int:
return _sys_info.mem_info_by_key('MemTotal')
@staticmethod
def virtualization() -> str | None:
try:
@ -345,3 +316,26 @@ class SysInfo:
return True
return False
@dataclass(frozen=True)
class MemInfo:
mem_total: int
mem_free: int
mem_available: int
def read_meminfo() -> MemInfo:
data: dict[str, int] = {}
with Path('/proc/meminfo').open() as file:
for line in file:
key, _, remainder = line.partition(':')
num, _, _ = remainder.strip().partition(' ')
data[key] = int(num)
return MemInfo(
mem_total=data['MemTotal'],
mem_free=data['MemFree'],
mem_available=data['MemAvailable'],
)

View File

@ -660,16 +660,12 @@ class Installer:
# in the first column of the entry; check for both cases.
entry_re = re.compile(rf'#{lang}(\.{encoding})?{modifier} {encoding}')
lang_value = None
for index, line in enumerate(locale_gen_lines):
if entry_re.match(line):
uncommented_line = line.removeprefix('#')
locale_gen_lines[index] = uncommented_line
locale_gen_lines[index] = line.removeprefix('#')
locale_gen.write_text(''.join(locale_gen_lines))
lang_value = uncommented_line.split()[0]
break
if lang_value is None:
else:
error(f"Invalid locale: language '{locale_config.sys_lang}', encoding '{locale_config.sys_enc}'")
return False
@ -679,7 +675,7 @@ class Installer:
error(f'Failed to run locale-gen on target: {e}')
return False
(self.target / 'etc/locale.conf').write_text(f'LANG={lang_value}\n')
(self.target / 'etc/locale.conf').write_text(f'LANG={lang}.{encoding}{modifier}\n')
return True
def set_timezone(self, zone: str) -> bool:
@ -778,6 +774,15 @@ class Installer:
with open(f'{self.target}/etc/systemd/network/10-{nic.iface}.network', 'a') as netconf:
netconf.write(str(conf))
def systemd_resolved_stub_mode(self) -> None:
"""
Enable systemd-resolved stub mode by (forcefully) setting a symlink
For further details see https://wiki.archlinux.org/title/Systemd-resolved#DNS
"""
resolv = self.target / 'etc/resolv.conf'
resolv.unlink(missing_ok=True)
resolv.symlink_to('/run/systemd/resolve/stub-resolv.conf')
def copy_iso_network_config(self, enable_services: bool = False) -> bool:
# Copy (if any) iwd password and config files
iwd_dir = LPath('/var/lib/iwd')
@ -806,11 +811,7 @@ class Installer:
self.pacman.strap('iwd')
self.enable_service('iwd')
# Enable systemd-resolved by (forcefully) setting a symlink
# For further details see https://wiki.archlinux.org/title/Systemd-resolved#DNS
resolv_config_path = self.target / 'etc/resolv.conf'
resolv_config_path.unlink(missing_ok=True)
resolv_config_path.symlink_to('/run/systemd/resolve/stub-resolv.conf')
self.systemd_resolved_stub_mode()
# Copy (if any) systemd-networkd config files
network_dir = LPath('/etc/systemd/network')
@ -875,11 +876,7 @@ class Installer:
return vendor.get_ucode()
return None
def _prepare_fs_type(
self,
fs_type: FilesystemType,
mountpoint: Path | None,
) -> None:
def _prepare_fs_type(self, fs_type: FilesystemType) -> None:
if (pkg := fs_type.installation_pkg) is not None:
self._base_packages.append(pkg)
@ -914,7 +911,7 @@ class Installer:
for vg in self._disk_config.lvm_config.vol_groups:
for vol in vg.volumes:
if vol.fs_type is not None:
self._prepare_fs_type(vol.fs_type, vol.mountpoint)
self._prepare_fs_type(vol.fs_type)
types = (EncryptionType.LVM_ON_LUKS, EncryptionType.LUKS_ON_LVM)
if self._disk_encryption.encryption_type in types:
@ -925,7 +922,7 @@ class Installer:
if part.fs_type is None:
continue
self._prepare_fs_type(part.fs_type, part.mountpoint)
self._prepare_fs_type(part.fs_type)
if part in self._disk_encryption.partitions:
self._prepare_encrypt()
@ -1359,7 +1356,7 @@ class Installer:
boot_dir = boot_partition.mountpoint
add_options = [
f'--target={platform.machine()}-efi',
f'--target={"arm64" if platform.machine() == "aarch64" else platform.machine()}-efi',
f'--efi-directory={efi_partition.mountpoint}',
*boot_dir_arg,
'--bootloader-id=GRUB',
@ -1480,28 +1477,32 @@ class Installer:
parent_dev_path = get_parent_device_path(efi_partition.safe_dev_path)
efi_dir_path = self.target / efi_partition.mountpoint.relative_to('/') / 'EFI'
efi_dir_path_target = efi_partition.mountpoint / 'EFI'
if bootloader_removable:
efi_dir_path = efi_dir_path / 'BOOT'
efi_dir_path_target = efi_dir_path_target / 'BOOT'
else:
efi_dir_path = efi_dir_path / 'arch-limine'
efi_dir_path_target = efi_dir_path_target / 'arch-limine'
config_path = efi_dir_path / 'limine.conf'
efi_dir_path.mkdir(parents=True, exist_ok=True)
efi_binaries: tuple[str, ...]
if platform.machine() == 'aarch64':
efi_binaries = ('BOOTAA64.EFI',)
else:
efi_binaries = ('BOOTIA32.EFI', 'BOOTX64.EFI')
try:
efi_dir_path = self.target / efi_partition.mountpoint.relative_to('/') / 'EFI'
efi_dir_path_target = efi_partition.mountpoint / 'EFI'
if bootloader_removable:
efi_dir_path = efi_dir_path / 'BOOT'
efi_dir_path_target = efi_dir_path_target / 'BOOT'
else:
efi_dir_path = efi_dir_path / 'arch-limine'
efi_dir_path_target = efi_dir_path_target / 'arch-limine'
config_path = efi_dir_path / 'limine.conf'
efi_dir_path.mkdir(parents=True, exist_ok=True)
for file in ('BOOTIA32.EFI', 'BOOTX64.EFI'):
for file in efi_binaries:
(limine_path / file).copy_into(efi_dir_path)
except Exception as err:
raise DiskError(f'Failed to install Limine in {self.target}{efi_partition.mountpoint}: {err}')
hook_command = (
f'/usr/bin/cp /usr/share/limine/BOOTIA32.EFI {efi_dir_path_target}/ && /usr/bin/cp /usr/share/limine/BOOTX64.EFI {efi_dir_path_target}/'
)
hook_command = ' && '.join(f'/usr/bin/cp /usr/share/limine/{file} {efi_dir_path_target}/' for file in efi_binaries)
if not bootloader_removable:
# Create EFI boot menu entry for Limine.
@ -1512,7 +1513,7 @@ class Installer:
raise OSError(f'Could not open or read /sys/firmware/efi/fw_platform_size to determine EFI bitness: {err}')
if efi_bitness == '64':
loader_path = '\\EFI\\arch-limine\\BOOTX64.EFI'
loader_path = f'\\EFI\\arch-limine\\{"BOOTAA64.EFI" if platform.machine() == "aarch64" else "BOOTX64.EFI"}'
elif efi_bitness == '32':
loader_path = '\\EFI\\arch-limine\\BOOTIA32.EFI'
else:
@ -2131,13 +2132,12 @@ def accessibility_tools_in_use() -> bool:
def run_custom_user_commands(commands: list[str], installation: Installer) -> None:
for index, command in enumerate(commands):
script_path = f'/var/tmp/user-command.{index}.sh'
chroot_path = f'{installation.target}/{script_path}'
script_path = LPath(f'/var/tmp/user-command.{index}.sh')
chroot_path = installation.target / script_path.relative_to_root()
info(f'Executing custom command "{command}" ...')
with open(chroot_path, 'w') as user_script:
user_script.write(command)
chroot_path.write_text(command)
SysCommand(f'arch-chroot -S {installation.target} bash {script_path}')
os.unlink(chroot_path)
chroot_path.unlink()

View File

@ -19,14 +19,15 @@ async def get_password(
def password_hint(value: str) -> InputInfo | None:
if not value:
return None
strength = PasswordStrength.strength(value)
if strength in (PasswordStrength.VERY_WEAK, PasswordStrength.WEAK):
return InputInfo(message=tr('Password strength: Weak'), msg_level=MsgLevelType.MsgError)
elif strength == PasswordStrength.MODERATE:
return InputInfo(message=tr('Password strength: Moderate'), msg_level=MsgLevelType.MsgWarning)
elif strength == PasswordStrength.STRONG:
return InputInfo(message=tr('Password strength: Strong'), msg_level=MsgLevelType.MsgInfo)
return None
match strength:
case PasswordStrength.VERY_WEAK | PasswordStrength.WEAK:
return InputInfo(message=tr('Password strength: Weak'), msg_level=MsgLevelType.MsgError)
case PasswordStrength.MODERATE:
return InputInfo(message=tr('Password strength: Moderate'), msg_level=MsgLevelType.MsgWarning)
case PasswordStrength.STRONG:
return InputInfo(message=tr('Password strength: Strong'), msg_level=MsgLevelType.MsgInfo)
while True:
result = await Input(

View File

@ -56,8 +56,8 @@ class MirrorListHandler:
for attempt_nr in range(attempts):
try:
mirrorlist = fetch_data_from_url(url)
self._status_mappings = self._parse_remote_mirror_list(mirrorlist)
data = fetch_data_from_url(url)
self._status_mappings = self._parse_remote_mirror_list(data)
return True
except Exception as e:
debug(f'Error while fetching mirror list: {e}')
@ -85,9 +85,9 @@ class MirrorListHandler:
# just return as-is without sorting?
return region_list
def _parse_remote_mirror_list(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]]:
def _parse_remote_mirror_list(self, data: bytes) -> dict[str, list[MirrorStatusEntryV3]]:
context = {'verbose': self.verbose}
mirror_status = MirrorStatusListV3.model_validate_json(mirrorlist, context=context)
mirror_status = MirrorStatusListV3.model_validate_json(data, context=context)
sorting_placeholder: dict[str, list[MirrorStatusEntryV3]] = {}

View File

@ -36,14 +36,14 @@ class Bootloader(Enum):
def json(self) -> str:
return self.value
@classmethod
def get_default(cls, uefi: bool, skip_boot: bool = False) -> Self:
@staticmethod
def get_default(uefi: bool, skip_boot: bool = False) -> Bootloader:
if skip_boot:
return cls.NO_BOOTLOADER
return Bootloader.NO_BOOTLOADER
elif uefi:
return cls.Systemd
return Bootloader.Systemd
else:
return cls.Grub
return Bootloader.Grub
@classmethod
def from_arg(cls, bootloader: str, skip_boot: bool) -> Self:

View File

@ -11,7 +11,6 @@ import parted
from parted import Disk, Geometry, Partition
from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator
from archinstall.lib.hardware import SysInfo
from archinstall.lib.log import debug
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.users import Password
@ -257,10 +256,6 @@ class PartitionTable(Enum):
def is_mbr(self) -> bool:
return self == PartitionTable.MBR
@classmethod
def default(cls) -> Self:
return cls.GPT if SysInfo.has_uefi() else cls.MBR
class Units(Enum):
BINARY = 'binary'
@ -758,13 +753,13 @@ class PartitionType(StrEnum):
PRIMARY = auto()
_UNKNOWN = 'unknown'
@classmethod
def get_type_from_code(cls, code: int) -> Self:
@staticmethod
def get_type_from_code(code: int) -> PartitionType:
if code == parted.PARTITION_NORMAL:
return cls.PRIMARY
return PartitionType.PRIMARY
else:
debug(f'Partition code not supported: {code}')
return cls._UNKNOWN
return PartitionType._UNKNOWN
def get_partition_code(self) -> int | None:
if self == PartitionType.PRIMARY:
@ -805,9 +800,11 @@ class PartitionFlag(PartitionFlagDataMixin, Enum):
class PartitionGUID(Enum):
"""
A list of Partition type GUIDs (lsblk -o+PARTTYPE) can be found here: https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs
A list of Partition type GUIDs (lsblk -o+PARTTYPE) can be found here:
https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs
"""
LINUX_ROOT_AARCH64 = 'B921B045-1DF0-41C3-AF44-4C6F280D3FAE'
LINUX_ROOT_X86_64 = '4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709'
@property
@ -1618,7 +1615,7 @@ class LsblkInfo(BaseModel):
fsver: str | None
fsavail: int | None
fsuse_percentage: str | None = Field(alias='fsuse%')
type: str | None # may be None for strange behavior with md devices
type: str
mountpoint: Path | None
mountpoints: list[Path]
fsroots: list[Path]

View File

@ -101,8 +101,8 @@ class MirrorStatusEntryV3(BaseModel):
@classmethod
@field_validator('score', mode='before')
def validate_score(cls, value: float) -> int | None:
if value is not None:
def validate_score(cls, value: Any) -> Any:
if isinstance(value, float):
value = round(value)
debug(f' score: {value}')
@ -127,12 +127,10 @@ class MirrorStatusListV3(BaseModel):
@model_validator(mode='before')
@classmethod
def check_model(
cls,
data: dict[str, int | datetime.datetime | list[MirrorStatusEntryV3]],
) -> dict[str, int | datetime.datetime | list[MirrorStatusEntryV3]]:
if data.get('version') == 3:
return data
def check_model(cls, data: Any) -> Any:
if isinstance(data, dict):
if data.get('version') == 3:
return data
raise ValueError('MirrorStatusListV3 only accepts version 3 data from https://archlinux.org/mirrors/status/json/')

View File

@ -178,12 +178,24 @@ class WifiNetwork:
if not line:
continue
parts = line.split()
if len(parts) != 5:
if line.lower().startswith('bssid'):
continue
wifi = cls(bssid=parts[0], frequency=parts[1], signal_level=parts[2], flags=parts[3], ssid=parts[4])
entries.append(wifi)
parts = line.split(None, 4)
if len(parts) < 4:
continue
ssid = parts[4] if len(parts) == 5 else ''
entries.append(
cls(
bssid=parts[0],
frequency=parts[1],
signal_level=parts[2],
flags=parts[3],
ssid=ssid,
)
)
return entries

View File

@ -7,6 +7,8 @@ class Kernel(StrEnum):
LINUX_LTS = 'linux-lts'
LINUX_ZEN = 'linux-zen'
LINUX_HARDENED = 'linux-hardened'
LINUX_RT = 'linux-rt'
LINUX_RT_LTS = 'linux-rt-lts'
DEFAULT_KERNEL: Final = Kernel.LINUX

View File

@ -15,10 +15,6 @@ class PacmanConfiguration(SubConfig):
parallel_downloads: int = 5
color: bool = True
@classmethod
def default(cls) -> Self:
return cls()
@override
def json(self) -> PacmanConfigSerialization:
return {
@ -40,7 +36,7 @@ class PacmanConfiguration(SubConfig):
@classmethod
def parse_arg(cls, args: PacmanConfigSerialization) -> Self:
config = cls.default()
config = cls()
if 'parallel_downloads' in args:
config.parallel_downloads = int(args['parallel_downloads'])

View File

@ -12,9 +12,8 @@ def install_network_config(
) -> None:
match network_config.type:
case NicType.ISO:
_ = installation.copy_iso_network_config(
enable_services=True, # Sources the ISO network configuration to the install medium.
)
# Sources the ISO network configuration to the install medium.
installation.copy_iso_network_config(enable_services=True)
case NicType.NM | NicType.NM_IWD:
packages = ['networkmanager']
@ -53,7 +52,7 @@ def _configure_nm_iwd(installation: Installer) -> None:
nm_conf_dir.mkdir(parents=True, exist_ok=True)
iwd_backend_conf = nm_conf_dir / 'wifi_backend.conf'
_ = iwd_backend_conf.write_text('[device]\nwifi.backend=iwd\n')
iwd_backend_conf.write_text('[device]\nwifi.backend=iwd\n')
def _configure_iwd_standalone(installation: Installer) -> None:
@ -69,7 +68,7 @@ def _configure_iwd_standalone(installation: Installer) -> None:
[Network]
NameResolvingService=systemd
""")
_ = main_conf.write_text(main_conf_content)
main_conf.write_text(main_conf_content)
networkd_dir = installation.target / 'etc/systemd/network'
networkd_dir.mkdir(parents=True, exist_ok=True)
@ -82,8 +81,6 @@ def _configure_iwd_standalone(installation: Installer) -> None:
[Network]
DHCP=yes
""")
_ = wired_conf.write_text(wired_conf_content)
wired_conf.write_text(wired_conf_content)
resolv = installation.target / 'etc/resolv.conf'
resolv.unlink(missing_ok=True)
resolv.symlink_to('/run/systemd/resolve/stub-resolv.conf')
installation.systemd_resolved_stub_mode()

View File

@ -1,5 +1,4 @@
from dataclasses import dataclass
from pathlib import Path
from typing import assert_never, override
from archinstall.lib.command import SysCommand
@ -7,6 +6,7 @@ from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.models.network import WifiConfiguredNetwork, WifiNetwork
from archinstall.lib.network.wpa_supplicant import WpaSupplicantConfig
from archinstall.lib.networking import SYS_NET
from archinstall.lib.translationhandler import tr
from archinstall.tui.components import ConfirmationScreen, InputScreen, InstanceRunnable, LoadingScreen, NotifyScreen, TableSelectionScreen, tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
@ -58,7 +58,7 @@ class WifiHandler(InstanceRunnable[bool]):
async def _enable_supplicant(self, wifi_iface: str) -> bool:
self._wpa_config.load_config()
result = self._wpa_cli('status') # if it it's running it will blow up
result = self._wpa_cli('status')
if result.success:
debug('wpa_supplicant already running')
@ -72,24 +72,22 @@ class WifiHandler(InstanceRunnable[bool]):
try:
SysCommand(f'wpa_supplicant -B -i {wifi_iface} -c {self._wpa_config.config_file}')
result = self._wpa_cli('status') # if it it's running it will blow up
if result.success:
debug('successfully enabled wpa_supplicant')
return True
else:
debug(f'failed to enable wpa_supplicant: {result.error}')
return False
except SysCallError as err:
debug(f'failed to enable wpa_supplicant: {err}')
return False
def _find_wifi_interface(self) -> str | None:
net_path = Path('/sys/class/net')
result = self._wpa_cli('status')
for iface in net_path.iterdir():
maybe_wireless_path = net_path / iface / 'wireless'
if maybe_wireless_path.is_dir():
if result.success:
debug('successfully enabled wpa_supplicant')
return True
else:
debug(f'failed to enable wpa_supplicant: {result.error}')
return False
def _find_wifi_interface(self) -> str | None:
for iface in SYS_NET.iterdir():
if (iface / 'wireless').is_dir():
return iface.name
return None
@ -197,15 +195,6 @@ class WifiHandler(InstanceRunnable[bool]):
try:
result = SysCommand(cmd).decode()
if 'FAIL' in result:
debug(f'wpa_cli returned FAIL: {result}')
return WpaCliResult(
success=False,
error=f'wpa_cli returned a failure: {result}',
)
return WpaCliResult(success=True, response=result)
except SysCallError as err:
debug(f'error running wpa_cli command: {err}')
return WpaCliResult(
@ -213,6 +202,15 @@ class WifiHandler(InstanceRunnable[bool]):
error=f'Error running wpa_cli command: {err}',
)
if 'FAIL' in result:
debug(f'wpa_cli returned FAIL: {result}')
return WpaCliResult(
success=False,
error=f'wpa_cli returned a failure: {result}',
)
return WpaCliResult(success=True, response=result)
def _find_network_id(self, ssid: str, iface: str) -> int | None:
result = self._wpa_cli('list_networks', iface)
@ -254,18 +252,18 @@ class WifiHandler(InstanceRunnable[bool]):
try:
result = self._wpa_cli('scan_results', iface)
if not result.success:
debug(f'Failed to retrieve scan results: {result.error}')
return []
if not result.response:
debug('No wifi networks found')
return []
networks = WifiNetwork.from_wpa(result.response)
return networks
except SysCallError as err:
debug('Unable to retrieve wifi results')
raise err
if not result.success:
debug(f'Failed to retrieve scan results: {result.error}')
return []
if not result.response:
debug('No wifi networks found')
return []
networks = WifiNetwork.from_wpa(result.response)
return networks

View File

@ -6,8 +6,9 @@ import socket
import ssl
import struct
import time
from pathlib import Path
from types import FrameType, TracebackType
from typing import Self
from typing import Final, Self
from urllib.error import URLError
from urllib.parse import urlencode
from urllib.request import urlopen
@ -16,6 +17,8 @@ from archinstall.lib.exceptions import DownloadTimeout, SysCallError
from archinstall.lib.log import debug, error, info
from archinstall.lib.pacman.pacman import Pacman
SYS_NET: Final = Path('/sys/class/net')
class DownloadTimer:
"""
@ -105,13 +108,14 @@ def enrich_iface_types(interfaces: list[str]) -> dict[str, str]:
result = {}
for iface in interfaces:
if os.path.isdir(f'/sys/class/net/{iface}/bridge/'):
path = SYS_NET / iface
if (path / 'bridge').is_dir():
result[iface] = 'BRIDGE'
elif os.path.isfile(f'/sys/class/net/{iface}/tun_flags'):
elif (path / 'tun_flags').is_file():
# ethtool -i {iface}
result[iface] = 'TUN/TAP'
elif os.path.isdir(f'/sys/class/net/{iface}/device'):
if os.path.isdir(f'/sys/class/net/{iface}/wireless/'):
elif (path / 'device').is_dir():
if (path / 'wireless').is_dir():
result[iface] = 'WIRELESS'
else:
result[iface] = 'PHYSICAL'
@ -121,7 +125,7 @@ def enrich_iface_types(interfaces: list[str]) -> dict[str, str]:
return result
def fetch_data_from_url(url: str, params: dict[str, str] | None = None, timeout: int = 30) -> str:
def fetch_data_from_url(url: str, params: dict[str, str] | None = None, timeout: int = 30) -> bytes:
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
@ -134,8 +138,7 @@ def fetch_data_from_url(url: str, params: dict[str, str] | None = None, timeout:
try:
response = urlopen(full_url, context=ssl_context, timeout=timeout)
data = response.read().decode('UTF-8')
return data
return response.read()
except URLError as e:
raise ValueError(f'Unable to fetch data from url: {url}\n{e}')
except Exception as e:

View File

@ -50,7 +50,7 @@ class PacmanMenu(AbstractSubMenu[PacmanConfiguration]):
config = await super().show()
if config is None:
return PacmanConfiguration.default()
return PacmanConfiguration()
_apply_to_live(config.parallel_downloads)

View File

@ -258,18 +258,16 @@ class ProfileHandler:
"""
try:
data = fetch_data_from_url(url)
b_data = bytes(data, 'utf-8')
except ValueError:
err = tr('Unable to fetch profile from specified url: {}').format(url)
error(err)
else:
with NamedTemporaryFile(delete=False, suffix='.py') as fp:
fp.write(b_data)
fp.write(data)
filepath = Path(fp.name)
profiles = self._process_profile_file(filepath)
self.remove_custom_profiles(profiles)
self.add_custom_profiles(profiles)
except ValueError:
err = tr('Unable to fetch profile from specified url: {}').format(url)
error(err)
def _load_profile_class(self, module: ModuleType) -> list[Profile]:
"""

View File

@ -44,6 +44,7 @@ _ENV_FONT = os.environ.get('FONT')
class TranslationHandler:
def __init__(self) -> None:
self._locales_dir = Path(__file__).parent.parent / 'locales'
self._base_pot = 'base.pot'
self._languages = 'languages.json'
self._active_language: Language | None = None
@ -82,13 +83,14 @@ class TranslationHandler:
if not running_from_iso():
return
font_fd, font_path = tempfile.mkstemp(prefix='archinstall_font_')
cmap_fd, cmap_path = tempfile.mkstemp(prefix='archinstall_cmap_')
os.close(font_fd)
os.close(cmap_fd)
self._font_backup = Path(font_path)
self._cmap_backup = Path(cmap_path)
try:
font_fd, font_path = tempfile.mkstemp(prefix='archinstall_font_')
cmap_fd, cmap_path = tempfile.mkstemp(prefix='archinstall_cmap_')
os.close(font_fd)
os.close(cmap_fd)
self._font_backup = Path(font_path)
self._cmap_backup = Path(cmap_path)
SysCommand(['setfont', '-O', str(self._font_backup), '-om', str(self._cmap_backup)])
except SysCallError as err:
debug(f'Failed to save console font: {err}')
@ -135,32 +137,29 @@ class TranslationHandler:
try:
# get a translation for a specific language
translation = gettext.translation('base', localedir=self._get_locales_dir(), languages=(abbr, lang))
# calculate the percentage of total translated text to total number of messages
if abbr == 'en':
percent = 100
else:
num_translations = self._get_catalog_size(translation)
percent = int((num_translations / self._total_messages) * 100)
# prevent cases where the .pot file is out of date and the percentage is above 100
percent = min(100, percent)
language = Language(abbr, lang, translation, percent, translated_lang, console_font)
languages.append(language)
translation = gettext.translation('base', localedir=self._locales_dir, languages=(abbr, lang))
except FileNotFoundError as err:
raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}")
# calculate the percentage of total translated text to total number of messages
if abbr == 'en':
percent = 100
else:
num_translations = self._get_catalog_size(translation)
percent = int((num_translations / self._total_messages) * 100)
# prevent cases where the .pot file is out of date and the percentage is above 100
percent = min(100, percent)
language = Language(abbr, lang, translation, percent, translated_lang, console_font)
languages.append(language)
return languages
def _load_language_mappings(self) -> list[dict[str, str]]:
"""
Load the mapping table of all known languages
"""
locales_dir = self._get_locales_dir()
languages = Path.joinpath(locales_dir, self._languages)
with open(languages) as fp:
with (self._locales_dir / self._languages).open() as fp:
return json.load(fp)
def _get_catalog_size(self, translation: gettext.NullTranslations) -> int:
@ -177,8 +176,7 @@ class TranslationHandler:
"""
Get total messages that could be translated
"""
locales = self._get_locales_dir()
with open(f'{locales}/{self._base_pot}') as fp:
with (self._locales_dir / self._base_pot).open() as fp:
lines = fp.readlines()
msgid_lines = [line for line in lines if 'msgid' in line]
@ -236,23 +234,12 @@ class TranslationHandler:
self._set_font(self.active_font)
debug(f'Console font set from language mapping: {self.active_font}')
def _get_locales_dir(self) -> Path:
"""
Get the locales directory path
"""
cur_path = Path(__file__).parent.parent
locales_dir = Path.joinpath(cur_path, 'locales')
return locales_dir
def _provided_translations(self) -> list[str]:
"""
Get a list of all known languages
"""
locales_dir = self._get_locales_dir()
filenames = os.listdir(locales_dir)
translation_files = []
for filename in filenames:
for filename in os.listdir(self._locales_dir):
if len(filename) == 2 or filename in ['pt_BR', 'zh-CN', 'zh-TW']:
translation_files.append(filename)
@ -277,6 +264,15 @@ def tr(message: str) -> str:
return str(_DeferredTranslation(message))
def tr_noop(message: str) -> str:
"""Mark a string for xgettext extraction without translating it here.
Use for strings that are translated later from a variable, e.g.
binding descriptions passed through tr() at runtime.
"""
return message
builtins._ = _DeferredTranslation # type: ignore[attr-defined]

View File

@ -9,6 +9,14 @@ if TYPE_CHECKING:
from _typeshed import DataclassInstance
def _sentence_case(text: str) -> str:
# Only capitalize the first letter of the label. The source strings and
# their translations are already written with the correct casing for each
# language, so title-casing every word is wrong outside English (it turned
# "Ім'я хоста" into "Ім'Я Хоста" and "(NTP)" into "(Ntp)").
return text[:1].upper() + text[1:]
def as_key_value_pair(
entries: dict[str, str | list[str] | bool],
ignore_empty: bool = True,
@ -33,28 +41,11 @@ def as_key_value_pair(
if isinstance(value, list):
value = '\n '.join(str(val) for val in value)
table.add_row(label.title(), f': {value}')
table.add_row(_sentence_case(label), f': {value}')
return table.stringify()
def as_columns(entries: list[str], cols: int) -> str:
"""
Will format a list into a given number of columns
"""
chunks: list[list[str]] = []
output = ''
for i in range(0, len(entries), cols):
chunks.append(entries[i : i + cols])
for row in chunks:
out_fmt = '{: <30} ' * len(row)
output += out_fmt.format(*row) + '\n'
return output
def _get_values(
o: DataclassInstance,
class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]

View File

@ -1,10 +1,8 @@
import secrets
import string
from datetime import UTC, datetime
from pathlib import Path
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
from archinstall.lib.utils.format import as_columns
def timestamp() -> str:
@ -25,29 +23,3 @@ def running_from_iso() -> bool:
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 format_cols(items: list[str], header: str | None = None) -> str:
if header:
text = f'{header}:\n'
else:
text = ''
nr_items = len(items)
if nr_items <= 4:
col = 1
elif nr_items <= 8:
col = 2
elif nr_items <= 12:
col = 3
else:
col = 4
text += as_columns(items, col)
# remove whitespaces on each row
text = '\n'.join(t.strip() for t in text.split('\n'))
return text
def is_valid_path(path: Path) -> bool:
return path.exists() and path.is_dir()

View File

@ -58,3 +58,17 @@ msgstr "Wollen sie wirklich abbrechen?"
After the translations have been written, run the script once more `./locales_generator.sh <lang_abbr>` and it will auto-generate the `base.mo` file with the included translations.
After that you're all ready to go and enjoy Archinstall in the new language :)
## Key binding descriptions
`base.po` also contains the key binding descriptions shown in the footer and the help panel, for example `Scroll Up` or `Toggle option`. Translate them like any other string, but keep them short so they still fit in the footer.
Most of them come from the `textual` library, where `xgettext` cannot see them, so they are listed with `tr_noop` in `archinstall/tui/binding_descriptions.py`, a module that exists only to make them extractable and is never imported at runtime. Archinstall's own descriptions are taken from the third positional argument of `Binding()`, so that argument must never be passed as a keyword.
The list has to be verified after a `textual` upgrade, or after a new textual widget is introduced in `archinstall/tui/components.py` (the widget also has to be added to `TEXTUAL_CLASSES` in the checker). Nothing in CI checks it, so run the following from the repository root
```
python3 test_tooling/check_binding_descriptions.py
```
It prints every missing or stale entry, and an `OK` line when the list matches the installed `textual`. Fix `binding_descriptions.py` until it passes, then run `./locales_generator.sh <lang_abbr>` again.

View File

@ -344,6 +344,10 @@ msgstr ""
msgid "Iteration time"
msgstr ""
msgid ""
"No disks were detected. A disk is required to be able to install Arch Linux"
msgstr ""
msgid "Select disks for the installation"
msgstr ""
@ -1220,12 +1224,192 @@ msgstr ""
msgid "Starting device modifications in "
msgstr ""
msgid "Bottom"
msgstr ""
msgid "Copy selected text"
msgstr ""
msgid "Cursor down"
msgstr ""
msgid "Cursor left"
msgstr ""
msgid "Cursor right"
msgstr ""
msgid "Cursor up"
msgstr ""
msgid "Cut selected text"
msgstr ""
msgid "Delete all to the left"
msgstr ""
msgid "Delete all to the right"
msgstr ""
msgid "Delete character left"
msgstr ""
msgid "Delete character right"
msgstr ""
msgid "Delete left to start of word"
msgstr ""
msgid "Delete right to start of word"
msgstr ""
msgid "Down"
msgstr ""
msgid "End"
msgstr ""
msgid "First"
msgstr ""
msgid "Focus Next"
msgstr ""
msgid "Focus Previous"
msgstr ""
msgid "Go to end"
msgstr ""
msgid "Go to start"
msgstr ""
msgid "Home"
msgstr ""
msgid "Last"
msgstr ""
msgid "Move cursor left"
msgstr ""
msgid "Move cursor left a word"
msgstr ""
msgid "Move cursor left a word and select"
msgstr ""
msgid "Move cursor left and select"
msgstr ""
msgid "Move cursor right a word"
msgstr ""
msgid "Move cursor right a word and select"
msgstr ""
msgid "Move cursor right and select"
msgstr ""
msgid "Move cursor right or accept the completion suggestion"
msgstr ""
msgid "Page Down"
msgstr ""
msgid "Page Left"
msgstr ""
msgid "Page Right"
msgstr ""
msgid "Page Up"
msgstr ""
msgid "Page down"
msgstr ""
msgid "Page up"
msgstr ""
msgid "Paste text from the clipboard"
msgstr ""
msgid "Press button"
msgstr ""
msgid "Quit"
msgstr ""
msgid "Scroll Down"
msgstr ""
msgid "Scroll End"
msgstr ""
msgid "Scroll Home"
msgstr ""
msgid "Scroll Left"
msgstr ""
msgid "Scroll Right"
msgstr ""
msgid "Scroll Up"
msgstr ""
msgid "Select"
msgstr ""
msgid "Select all"
msgstr ""
msgid "Select line end"
msgstr ""
msgid "Select line start"
msgstr ""
msgid "Submit"
msgstr ""
msgid "Toggle option"
msgstr ""
msgid "Top"
msgstr ""
msgid "Up"
msgstr ""
msgid "Reset"
msgstr ""
msgid "Search"
msgstr ""
msgid "Toggle"
msgstr ""
msgid "Confirm"
msgstr ""
msgid "Focus right"
msgstr ""
msgid "Focus left"
msgstr ""
msgid "Ok"
msgstr ""
msgid "Input cannot be empty"
msgstr ""
msgid "Show/Hide help"
msgstr ""
msgid "Yes"
msgstr ""

View File

@ -1423,7 +1423,7 @@ msgid "Yes"
msgstr "ναι"
msgid "No"
msgstr ""
msgstr "όχι"
#, fuzzy
msgid "Archinstall help"
@ -1434,7 +1434,7 @@ msgid " (default)"
msgstr "(προκαθορισμένο)"
msgid "Press Ctrl+h for help"
msgstr ""
msgstr "Πατήστε Ctrl+h για βοήθεια"
#, fuzzy
msgid "Choose an option to give Sway access to your hardware"
@ -1621,7 +1621,7 @@ msgid "Select execution mode"
msgstr "Επιλέξτε μία ενέργεια για '{}'"
msgid "Press ? for help"
msgstr ""
msgstr "Πατήστε ? για βοήθεια"
#, fuzzy
msgid "Choose an option to give Hyprland access to your hardware"
@ -1754,31 +1754,31 @@ msgid "Only ASCII characters are supported"
msgstr ""
msgid "Show help"
msgstr ""
msgstr "Εμφάνιση βοήθειας"
msgid "Exit help"
msgstr ""
msgstr "Έξοδος από τη βοήθεια"
msgid "Preview scroll up"
msgstr ""
msgstr "Κύλιση προεπισκόπησης πάνω"
msgid "Preview scroll down"
msgstr ""
msgstr "Κύλιση προεπισκόπησης κάτω"
msgid "Move up"
msgstr ""
msgstr "Μετακίνηση πάνω"
msgid "Move down"
msgstr ""
msgstr "Μετακίνηση κάτω"
msgid "Move right"
msgstr ""
msgstr "Μετακίνηση δεξιά"
msgid "Move left"
msgstr ""
msgstr "Μετακίνηση αριστερά"
msgid "Jump to entry"
msgstr ""
msgstr "Μετάβαση σε καταχώρηση"
msgid "Skip selection (if available)"
msgstr ""
@ -1795,20 +1795,20 @@ msgid "Select on multi select"
msgstr "Επιλέξτε μία ζώνη ώρας"
msgid "Reset"
msgstr ""
msgstr "Επαναφορά"
#, fuzzy
msgid "Skip selection menu"
msgstr "Επιλέξτε μία ενέργεια για '{}'"
msgid "Start search mode"
msgstr ""
msgstr "Έναρξη λειτουργίας αναζήτησης"
msgid "Exit search mode"
msgstr ""
msgstr "Έξοδος από τη λειτουργία αναζήτησης"
msgid "General"
msgstr ""
msgstr "Γενικά"
#, fuzzy
msgid "Navigation"
@ -1819,115 +1819,115 @@ msgid "Selection"
msgstr "Επιλέξτε διεπαφή προς προσθήκη"
msgid "Search"
msgstr ""
msgstr "Αναζήτηση"
msgid "Down"
msgstr ""
msgstr "Κάτω"
msgid "Up"
msgstr ""
msgstr "Πάνω"
#, fuzzy
msgid "Confirm"
msgstr "Επιβεβαίωση και έξοδος"
msgid "Focus right"
msgstr ""
msgstr "Εστίαση δεξιά"
msgid "Focus left"
msgstr ""
msgstr "Εστίαση αριστερά"
msgid "Toggle"
msgstr ""
msgstr "Εναλλαγή"
msgid "Show/Hide help"
msgstr ""
msgstr "Εμφάνιση/Απόκρυψη βοήθειας"
msgid "Quit"
msgstr ""
msgstr "Έξοδος"
msgid "First"
msgstr ""
msgstr "Πρώτο"
msgid "Last"
msgstr ""
msgstr "Τελευταίο"
#, fuzzy
msgid "Select"
msgstr "Επιλέξτε διεπαφή προς προσθήκη"
msgid "Page Up"
msgstr ""
msgstr "Σελίδα πάνω"
msgid "Page Down"
msgstr ""
msgstr "Σελίδα κάτω"
msgid "Page up"
msgstr ""
msgstr "Σελίδα πάνω"
msgid "Page down"
msgstr ""
msgstr "Σελίδα κάτω"
msgid "Page Left"
msgstr ""
msgstr "Σελίδα αριστερά"
msgid "Page Right"
msgstr ""
msgstr "Σελίδα δεξιά"
msgid "Cursor up"
msgstr ""
msgstr "Δρομέας πάνω"
msgid "Cursor down"
msgstr ""
msgstr "Δρομέας κάτω"
msgid "Cursor right"
msgstr ""
msgstr "Δρομέας δεξιά"
msgid "Cursor left"
msgstr ""
msgstr "Δρομέας αριστερά"
msgid "Top"
msgstr ""
msgstr "Κορυφή"
msgid "Bottom"
msgstr ""
msgstr "Βάση"
msgid "Home"
msgstr ""
msgstr "Αρχή"
msgid "End"
msgstr ""
msgstr "Τέλος"
#, fuzzy
msgid "Toggle option"
msgstr "Επιλογές υποόγκου"
msgid "Scroll Up"
msgstr ""
msgstr "Κύλιση πάνω"
msgid "Scroll Down"
msgstr ""
msgstr "Κύλιση κάτω"
msgid "Scroll Left"
msgstr ""
msgstr "Κύλιση αριστερά"
msgid "Scroll Right"
msgstr ""
msgstr "Κύλιση δεξιά"
msgid "Scroll Home"
msgstr ""
msgstr "Κύλιση στην αρχή"
msgid "Scroll End"
msgstr ""
msgstr "Κύλιση στο τέλος"
msgid "Focus Next"
msgstr ""
msgstr "Εστίαση στο επόμενο"
msgid "Focus Previous"
msgstr ""
msgstr "Εστίαση στο προηγούμενο"
msgid "Copy selected text"
msgstr ""
msgstr "Αντιγραφή επιλεγμένου κειμένου"
msgid "labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
msgstr ""
@ -2115,7 +2115,7 @@ msgid "Enter wifi password"
msgstr "Εισάγετε κωδικό: "
msgid "Ok"
msgstr ""
msgstr "Εντάξει"
msgid "Removable"
msgstr ""

View File

@ -221,12 +221,27 @@ msgstr "Imágenes del núcleo unificadas"
msgid "Install to removable location"
msgstr "Instalar a ubicación desmontable"
msgid "Plymouth"
msgstr "Plymouth"
msgid "Will install to /EFI/BOOT/ (removable location, safe default)"
msgstr "Instalará a /EFI/BOOT/ (ubicación desmontable, segura por defecto)"
msgid "Will install to custom location with NVRAM entry"
msgstr "Instalará a ubicación personalizada con entrada NVRAM"
msgid "Plymouth adds a cosmetic boot splash but can cause boot problems on some setups:"
msgstr "Plymouth añade una pantalla de inicio cosmética, pero puede causar problemas de arranque en algunas configuraciones:"
msgid "black screen with the NVIDIA driver"
msgstr "pantalla negra con el controlador NVIDIA"
msgid "hidden password prompt with disk encryption (LUKS)"
msgstr "solicitud de contraseña oculta con cifrado de disco (LUKS)"
msgid "Would you like to enable it?"
msgstr "¿Desea habilitarlo?"
msgid "Would you like to use unified kernel images?"
msgstr "¿Le gustaría utilizar imágenes del núcleo unificadas?"
@ -254,6 +269,9 @@ msgstr "Seleccione el gestor de arranque que desea instalar"
msgid "UEFI is not detected and some options are disabled"
msgstr "No se detecta UEFI y algunas opciones están desactivadas"
msgid "Select Plymouth theme"
msgstr "Seleccionar tema Plymouth"
msgid "The specified configuration will be applied"
msgstr "Se aplicará la configuración especificada"
@ -263,9 +281,6 @@ msgstr "¿Quiere continuar?"
msgid "Configuration preview"
msgstr "Vista previa de la configuración"
msgid "Warning: no network configuration selected. Network will need to be set up manually on the installed system."
msgstr "Advertencia: no se ha seleccionado ninguna configuración de red. La red deberá configurarse manualmente en el sistema instalado."
msgid "No configuration"
msgstr "Sin configuración"
@ -332,6 +347,9 @@ msgstr "Contraseña"
msgid "Iteration time"
msgstr "Tiempo de iteración"
msgid "No disks were detected. A disk is required to be able to install Arch Linux"
msgstr "No se detectaron discos. Se requiere un disco para poder instalar Arch Linux"
msgid "Select disks for the installation"
msgstr "Seleccione discos para la instalación"
@ -627,16 +645,21 @@ msgstr "Color"
msgid "Kernel"
msgstr "Núcleo"
msgid "Missing configurations:\n"
msgstr "Configuraciones que faltan:\n"
msgid "No network configuration selected. Network will need to be set up manually on the installed system."
msgstr "No se ha seleccionado ninguna configuración de red. La red deberá configurarse manualmente en el sistema instalado."
#, python-brace-format
msgid "Invalid configuration: {}"
msgstr "Configuración no válida: {}"
msgid "Missing configurations:"
msgstr "Configuraciones faltantes:"
msgid "Invalid configuration:"
msgstr "Configuración no válida:"
msgid "Ready to install"
msgstr "Listo para instalar"
msgid "Warnings:"
msgstr "Advertencias:"
msgid "Profiles"
msgstr "Perfiles"
@ -858,6 +881,10 @@ msgstr "UKI activado"
msgid "Removable"
msgstr "Desmontable"
#, python-brace-format
msgid "Plymouth \"{}\""
msgstr "Plymouth «{}»"
msgid "Use a best-effort default partition layout"
msgstr "Utlizar un diseño de partición predeterminado de mejor esfuerzo"
@ -965,8 +992,8 @@ msgstr "Grupo de paquetes:"
msgid "Color enabled"
msgstr "Color activado"
#, python-brace-format
msgid "{} grphics driver"
#, fuzzy, python-brace-format
msgid "{} graphics driver"
msgstr "{} controlador de gráficos"
#, python-brace-format
@ -1032,15 +1059,15 @@ msgstr "¿Desea conectarse al Wi-Fi?"
msgid "No wifi interface found"
msgstr "No se encontró ninguna interfaz Wi-Fi"
msgid "No wifi networks found"
msgstr "No se encontró redes Wi-Fi"
msgid "Select wifi network to connect to"
msgstr "Seleccione una red Wi-Fi para conectarse"
msgid "Scanning wifi networks..."
msgstr "Buscando redes Wi-Fi..."
msgid "No wifi networks found"
msgstr "No se encontró redes Wi-Fi"
msgid "Failed setting up wifi"
msgstr "Error al configurar el Wi-Fi"
@ -1608,9 +1635,6 @@ msgstr " (predeterminado)"
#~ msgid "Subvolume name :"
#~ msgstr "Nombre del subvolumen :"
#~ msgid "Select a mount point :"
#~ msgstr "Seleccione un punto de montaje :"
#~ msgid "Select the desired subvolume options "
#~ msgstr "Seleccione las opciones de subvolumen deseadas "

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -230,6 +230,18 @@ msgstr "/EFI/BOOT/(リムーバブルな場所、安全なデフォルト)
msgid "Will install to custom location with NVRAM entry"
msgstr "NVRAM エントリーを使用してカスタムした場所にインストール"
msgid "Plymouth adds a cosmetic boot splash but can cause boot problems on some setups:"
msgstr "Plymouth は表面上のブートスプラッシュを追加しますが、構成によっては起動に問題が生じる可能性があります:"
msgid "black screen with the NVIDIA driver"
msgstr "NVIDIA ドライバーでのブラックスクリーン"
msgid "hidden password prompt with disk encryption (LUKS)"
msgstr "ディスク暗号化LUKS使用時の非表示パスワードプロンプト"
msgid "Would you like to enable it?"
msgstr "有効にしますか?"
msgid "Would you like to use unified kernel images?"
msgstr "Unified カーネルイメージを使用しますか?"
@ -335,6 +347,9 @@ msgstr "パスワード"
msgid "Iteration time"
msgstr "イテレーション時間"
msgid "No disks were detected. A disk is required to be able to install Arch Linux"
msgstr "ディスクが検出されませんでした。Arch Linux をインストールするにはディスクが必要です"
msgid "Select disks for the installation"
msgstr "インストール用のディスクを選択"
@ -1174,17 +1189,197 @@ msgstr "新しいバージョンが利用可能"
msgid "Starting device modifications in "
msgstr "次の場所でデバイスの変更を開始しています "
msgid "Bottom"
msgstr "一番下へ"
msgid "Copy selected text"
msgstr "選択したテキストをコピー"
msgid "Cursor down"
msgstr "カースルを下へ"
msgid "Cursor left"
msgstr "カーソルを左へ"
msgid "Cursor right"
msgstr "カーソルを右へ"
msgid "Cursor up"
msgstr "カーソルを上へ"
msgid "Cut selected text"
msgstr "選択したテキストを切り取る"
msgid "Delete all to the left"
msgstr "左側をすべて削除"
msgid "Delete all to the right"
msgstr "右側をすべて削除"
msgid "Delete character left"
msgstr "左の文字を削除"
msgid "Delete character right"
msgstr "右の文字を削除"
msgid "Delete left to start of word"
msgstr "単語の先頭まで左側に削除"
msgid "Delete right to start of word"
msgstr "単語の先頭まで右側に削除"
msgid "Down"
msgstr "下へ"
msgid "End"
msgstr "最後へ"
msgid "First"
msgstr "最初"
msgid "Focus Next"
msgstr "次にフォーカス"
msgid "Focus Previous"
msgstr "前にフォーカス"
msgid "Go to end"
msgstr "末尾へ移動"
msgid "Go to start"
msgstr "先頭へ移動"
msgid "Home"
msgstr "ホーム"
msgid "Last"
msgstr "最後"
msgid "Move cursor left"
msgstr "カーソルを左に移動"
msgid "Move cursor left a word"
msgstr "カーソルを1語左に移動"
msgid "Move cursor left a word and select"
msgstr "カーソルを1語左に移動して選択"
msgid "Move cursor left and select"
msgstr "カーソルを左に移動して選択"
msgid "Move cursor right a word"
msgstr "カーソルを1語右に移動"
msgid "Move cursor right a word and select"
msgstr "カーソルを右に1語移動して選択"
msgid "Move cursor right and select"
msgstr "カーソルを右に移動して選択"
msgid "Move cursor right or accept the completion suggestion"
msgstr "カーソルを右に移動するか補完候補を受け入れる"
msgid "Page Down"
msgstr "下のページへ"
msgid "Page Left"
msgstr "左のページへ"
msgid "Page Right"
msgstr "右のページへ"
msgid "Page Up"
msgstr "上のページへ"
msgid "Page down"
msgstr "下のページへ"
msgid "Page up"
msgstr "上のページへ"
msgid "Paste text from the clipboard"
msgstr "クリップボードからテキストを貼り付ける"
msgid "Press button"
msgstr "ボタンを押す"
msgid "Quit"
msgstr "終了"
msgid "Scroll Down"
msgstr "下にスクロール"
msgid "Scroll End"
msgstr "一番下にスクロール"
msgid "Scroll Home"
msgstr "ホームにスクロール"
msgid "Scroll Left"
msgstr "左にスクロール"
msgid "Scroll Right"
msgstr "右にスクロール"
msgid "Scroll Up"
msgstr "上にスクロール"
msgid "Select"
msgstr "選択"
msgid "Select all"
msgstr "すべて選択"
msgid "Select line end"
msgstr "行末を選択"
msgid "Select line start"
msgstr "行頭を選択"
msgid "Submit"
msgstr "決定"
msgid "Toggle option"
msgstr "オプションを切り替え"
msgid "Top"
msgstr "一番上へ"
msgid "Up"
msgstr "上へ"
msgid "Reset"
msgstr "リセット"
msgid "Search"
msgstr "検索"
msgid "Toggle"
msgstr "切り替え"
msgid "Confirm"
msgstr "確認"
msgid "Focus right"
msgstr "右にフォーカス"
msgid "Focus left"
msgstr "左にフォーカス"
msgid "Ok"
msgstr "OK"
msgid "Input cannot be empty"
msgstr "入力は空にできません"
msgid "Show/Hide help"
msgstr "ヘルプを表示/隠す"
msgid "Yes"
msgstr "Yes"
msgstr "はい"
msgid "No"
msgstr "No"
msgstr "いいえ"
msgid " (default)"
msgstr " (デフォルト)"
@ -1656,9 +1851,6 @@ msgstr " (デフォルト)"
#~ msgid "Manual nic configuration with no auto DHCP requires an IP address"
#~ msgstr "自動 DHCP を使用しない手動 NIC 設定には、IP アドレスが必要です"
#~ msgid "Select interface to add"
#~ msgstr "追加するインターフェースを選択"
#~ msgid "Mark/Unmark a partition as compressed (btrfs only)"
#~ msgstr "圧縮するパーティションとしてマーク/マーク解除Btrfs のみ)"
@ -2125,9 +2317,6 @@ msgstr " (デフォルト)"
#~ msgid "Root mount directory"
#~ msgstr "ルートマウントディレクトリ"
#~ msgid "Select language"
#~ msgstr "言語を選択"
#~ msgid "Write additional packages to install (space separated, leave blank to skip)"
#~ msgstr "追加でインストールするパッケージを書く(スペースで区切る。無記入でスキップ)"
@ -2234,9 +2423,6 @@ msgstr " (デフォルト)"
#~ msgid "Select on multi select"
#~ msgstr "複数で選択"
#~ msgid "Reset"
#~ msgstr "リセット"
#~ msgid "Skip selection menu"
#~ msgstr "選択メニューをスキップ"
@ -2255,114 +2441,6 @@ msgstr " (デフォルト)"
#~ msgid "Selection"
#~ msgstr "選択"
#~ msgid "Search"
#~ msgstr "検索"
#~ msgid "Down"
#~ msgstr "下へ"
#~ msgid "Up"
#~ msgstr "上へ"
#~ msgid "Confirm"
#~ msgstr "確認"
#~ msgid "Focus right"
#~ msgstr "右にフォーカス"
#~ msgid "Focus left"
#~ msgstr "左にフォーカス"
#~ msgid "Toggle"
#~ msgstr "切り替え"
#~ msgid "Show/Hide help"
#~ msgstr "ヘルプを表示/隠す"
#~ msgid "Quit"
#~ msgstr "終了"
#~ msgid "First"
#~ msgstr "最初"
#~ msgid "Last"
#~ msgstr "最後"
#~ msgid "Select"
#~ msgstr "選択"
#~ msgid "Page Up"
#~ msgstr "上のページへ"
#~ msgid "Page Down"
#~ msgstr "下のページへ"
#~ msgid "Page up"
#~ msgstr "上のページへ"
#~ msgid "Page down"
#~ msgstr "下のページへ"
#~ msgid "Page Left"
#~ msgstr "左のページへ"
#~ msgid "Page Right"
#~ msgstr "右のページへ"
#~ msgid "Cursor up"
#~ msgstr "カーソルを上へ"
#~ msgid "Cursor down"
#~ msgstr "カースルを下へ"
#~ msgid "Cursor right"
#~ msgstr "カーソルを右へ"
#~ msgid "Cursor left"
#~ msgstr "カーソルを左へ"
#~ msgid "Top"
#~ msgstr "一番上へ"
#~ msgid "Bottom"
#~ msgstr "一番下へ"
#~ msgid "Home"
#~ msgstr "ホーム"
#~ msgid "End"
#~ msgstr "最後へ"
#~ msgid "Toggle option"
#~ msgstr "オプションを切り替え"
#~ msgid "Scroll Up"
#~ msgstr "上にスクロール"
#~ msgid "Scroll Down"
#~ msgstr "下にスクロール"
#~ msgid "Scroll Left"
#~ msgstr "左にスクロール"
#~ msgid "Scroll Right"
#~ msgstr "右にスクロール"
#~ msgid "Scroll Home"
#~ msgstr "ホームにスクロール"
#~ msgid "Scroll End"
#~ msgstr "一番下にスクロール"
#~ msgid "Focus Next"
#~ msgstr "次にフォーカス"
#~ msgid "Focus Previous"
#~ msgstr "前にフォーカス"
#~ msgid "Copy selected text"
#~ msgstr "選択したテキストをコピー"
#~ msgid "labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
#~ msgstr "labwc はお使いの Seatキーボード、マウスなどのハードウェアデバイス群にアクセスする必要があります"

View File

@ -116,7 +116,7 @@
{"abbr": "ne", "lang": "Nepali", "translated_lang": "नेपाली"},
{"abbr": "nl", "lang": "Dutch", "translated_lang": "Nederlands"},
{"abbr": "nn", "lang": "Norwegian Nynorsk"},
{"abbr": "nb", "lang": "Norwegian Bokmål"},
{"abbr": "nb", "lang": "Norwegian Bokmål", "translated_lang": "Norsk bokmål"},
{"abbr": "no", "lang": "Norwegian"},
{"abbr": "ny", "lang": "Nyanja"},
{"abbr": "oc", "lang": "Occitan (post 1500)"},
@ -173,7 +173,7 @@
{"abbr": "ur", "lang": "Urdu", "translated_lang": "اردو"},
{"abbr": "uz", "lang": "Uzbek", "translated_lang": "O'zbek"},
{"abbr": "ve", "lang": "Venda"},
{"abbr": "vi", "lang": "Vietnamese"},
{"abbr": "vi", "lang": "Vietnamese", "translated_lang": "Tiếng Việt"},
{"abbr": "vo", "lang": "Volapük"},
{"abbr": "wa", "lang": "Walloon"},
{"abbr": "wo", "lang": "Wolof"},

View File

@ -14,8 +14,14 @@ usage() {
}
generate_pot() {
# tr - regular translation calls
# tr_noop - extraction-only marker for strings translated later from a variable
# Binding:3 - the description argument of textual Binding() definitions,
# translated at runtime by _translate_bindings(). The description
# must be passed positionally - xgettext cannot see keyword arguments.
find . -type f -iname '*.py' | sort \
| xargs xgettext --no-location --omit-header --keyword='tr' \
| xargs xgettext --no-location --omit-header \
--keyword='tr' --keyword='tr_noop' --keyword='Binding:3' \
-d base -o locales/base.pot
}
@ -61,19 +67,9 @@ cmd_check_po_syntax() {
echo "All .po files passed syntax check."
}
cmd_check_no_tr_fstring() {
echo "Checking for tr(f-string) anti-pattern..."
if grep -rnE "tr\(\s*f['\"]" . --include='*.py'; then
echo "ERROR: use tr('...{}').format(...) instead of tr(f'...')" >&2
return 1
fi
echo "No tr(f-string) anti-pattern found."
}
cmd_check() {
local failed=0
cmd_check_po_syntax || failed=1
cmd_check_no_tr_fstring || failed=1
if [ "$failed" -eq 1 ]; then
echo "Some translation checks failed." >&2
exit 1

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -2389,3 +2389,33 @@ msgstr "набір апаратних пристроїв, напр. клавіа
msgid "No network configuration selected. Network will need to be set up manually on the installed system."
msgstr "Конфігурацію мережі не обрано. Мережу доведеться налаштувати вручну на встановленій системі."
msgid "Plymouth"
msgstr "Plymouth"
msgid ""
"Plymouth adds a cosmetic boot splash but can cause boot problems on some "
"setups:"
msgstr ""
"Plymouth додає косметичну заставку завантаження, але на деяких конфігураціях "
"може спричинити проблеми із завантаженням:"
msgid "black screen with the NVIDIA driver"
msgstr "чорний екран із драйвером NVIDIA"
msgid "hidden password prompt with disk encryption (LUKS)"
msgstr "прихований запит пароля при шифруванні диска (LUKS)"
msgid "Would you like to enable it?"
msgstr "Увімкнути Plymouth?"
msgid "Select Plymouth theme"
msgstr "Оберіть тему Plymouth"
#, python-brace-format
msgid "Plymouth \"{}\""
msgstr "Plymouth \"{}\""
#, python-brace-format
msgid "{} graphics driver"
msgstr "Графічний драйвер {}"

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ from pathlib import Path
from archinstall.lib.args import ArchConfigHandler, SubCommand
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.hardware import SysInfo
from archinstall.lib.hardware import MemInfo, SysInfo, read_meminfo
from archinstall.lib.log import debug, error, info, logger, share_install_log, warn
from archinstall.lib.menu.helpers import Confirmation
from archinstall.lib.network.wifi_handler import WifiHandler
@ -23,11 +23,11 @@ from archinstall.tui.components import tui
from archinstall.tui.menu_item import MenuItemGroup
def _log_sys_info() -> None:
def _log_sys_info(meminfo: MemInfo) -> 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'Memory statistics: {meminfo.mem_available} kB available out of {meminfo.mem_total} kB total installed')
debug(f'Virtualization detected: {SysInfo.virtualization()}; is VM: {SysInfo.is_vm()}')
debug(f'Graphics devices detected: {SysInfo._graphics_devices().keys()}')
@ -138,7 +138,7 @@ def run() -> int:
translation_handler.save_console_font()
_log_sys_info()
_log_sys_info(read_meminfo())
if not arch_config_handler.args.offline:
if not arch_config_handler.args.skip_wifi_check:

View File

@ -0,0 +1,77 @@
"""Textual's built-in key binding descriptions, listed for xgettext.
These strings live inside the textual package, so xgettext cannot find them
when scanning this code base. At runtime _translate_bindings() in
components.py translates every binding description with tr(), including the
ones inherited from textual widgets - this module only makes those strings
visible to the extraction in locales_generator.sh (via the tr_noop keyword).
The list covers the MRO-merged bindings of the textual classes used in
components.py: _translate_bindings() operates on the merged bindings map, so
descriptions inherited from ancestors (e.g. Scroll Up from ScrollView) are
translated at runtime too and must be listed here.
Verify or regenerate the list after a textual upgrade or after introducing
a new textual widget in components.py:
python3 test_tooling/check_binding_descriptions.py
"""
from archinstall.lib.translationhandler import tr_noop
# textual 8.2.8
TEXTUAL_BINDING_DESCRIPTIONS: tuple[str, ...] = (
tr_noop('Bottom'),
tr_noop('Copy selected text'),
tr_noop('Cursor down'),
tr_noop('Cursor left'),
tr_noop('Cursor right'),
tr_noop('Cursor up'),
tr_noop('Cut selected text'),
tr_noop('Delete all to the left'),
tr_noop('Delete all to the right'),
tr_noop('Delete character left'),
tr_noop('Delete character right'),
tr_noop('Delete left to start of word'),
tr_noop('Delete right to start of word'),
tr_noop('Down'),
tr_noop('End'),
tr_noop('First'),
tr_noop('Focus Next'),
tr_noop('Focus Previous'),
tr_noop('Go to end'),
tr_noop('Go to start'),
tr_noop('Home'),
tr_noop('Last'),
tr_noop('Move cursor left'),
tr_noop('Move cursor left a word'),
tr_noop('Move cursor left a word and select'),
tr_noop('Move cursor left and select'),
tr_noop('Move cursor right a word'),
tr_noop('Move cursor right a word and select'),
tr_noop('Move cursor right and select'),
tr_noop('Move cursor right or accept the completion suggestion'),
tr_noop('Page Down'),
tr_noop('Page Left'),
tr_noop('Page Right'),
tr_noop('Page Up'),
tr_noop('Page down'),
tr_noop('Page up'),
tr_noop('Paste text from the clipboard'),
tr_noop('Press button'),
tr_noop('Quit'),
tr_noop('Scroll Down'),
tr_noop('Scroll End'),
tr_noop('Scroll Home'),
tr_noop('Scroll Left'),
tr_noop('Scroll Right'),
tr_noop('Scroll Up'),
tr_noop('Select'),
tr_noop('Select all'),
tr_noop('Select line end'),
tr_noop('Select line start'),
tr_noop('Submit'),
tr_noop('Toggle option'),
tr_noop('Top'),
tr_noop('Up'),
)

View File

@ -150,6 +150,19 @@ class LoadingScreen(BaseScreen[ValueT]):
_ = self.dismiss()
class _Input(Input):
@override
def on_mount(self) -> None:
_translate_bindings(self._merged_bindings, self._bindings)
class _Button(Button):
# no @override: Button has no on_mount in its MRO,
# textual dispatches the handler by naming convention
def on_mount(self) -> None:
_translate_bindings(self._merged_bindings, self._bindings)
class _OptionList(OptionList):
BINDINGS: ClassVar = [
Binding('down', 'cursor_down', 'Down', show=True),
@ -309,7 +322,7 @@ class OptionListScreen(BaseScreen[ValueT]):
yield ScrollableContainer(preview_label)
if self._filter:
yield Input(placeholder='/filter', id='filter-input')
yield _Input(placeholder='/filter', id='filter-input')
yield Footer()
@ -540,7 +553,7 @@ class SelectListScreen(BaseScreen[ValueT]):
yield ScrollableContainer(preview_label)
if self._filter:
yield Input(placeholder='/filter', id='filter-input')
yield _Input(placeholder='/filter', id='filter-input')
yield Footer()
@ -702,12 +715,12 @@ class ConfirmationScreen(BaseScreen[ValueT]):
with Vertical(classes='content-container'):
with Horizontal(classes='buttons-container'):
for item in self._group.items:
yield Button(item.text, id=item.key)
yield _Button(item.text, id=item.key)
else:
with Vertical():
with Horizontal(classes='buttons-container'):
for item in self._group.items:
yield Button(item.text, id=item.key)
yield _Button(item.text, id=item.key)
yield Rule(orientation='horizontal')
if self._preview_header is not None:
@ -848,7 +861,7 @@ class InputScreen(BaseScreen[str]):
with Center(classes='container-wrapper'):
with Vertical(classes='input-content'):
yield Input(
yield _Input(
placeholder=self._placeholder,
password=self._password,
value=self._default_value,

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable, Iterable
from dataclasses import dataclass, field
from enum import Enum, StrEnum, auto

View File

@ -17,12 +17,16 @@
"partitions": [
{
"btrfs": [],
"dev_path": null,
"flags": [
"boot"
],
"fs_type": "fat32",
"size": {
"sector_size": null,
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 512
},
@ -30,7 +34,10 @@
"mountpoint": "/boot",
"obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0",
"start": {
"sector_size": null,
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 1
},
@ -39,10 +46,14 @@
},
{
"btrfs": [],
"dev_path": null,
"flags": [],
"fs_type": "ext4",
"size": {
"sector_size": null,
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "GiB",
"value": 20
},
@ -50,7 +61,10 @@
"mountpoint": "/",
"obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208",
"start": {
"sector_size": null,
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 513
},
@ -59,20 +73,27 @@
},
{
"btrfs": [],
"dev_path": null,
"flags": [],
"fs_type": "ext4",
"size": {
"sector_size": null,
"unit": "Percent",
"value": 100
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "GiB",
"value": 10
},
"mount_options": [],
"mountpoint": "/home",
"obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f",
"start": {
"sector_size": null,
"unit": "GiB",
"value": 20
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 20993
},
"status": "create",
"type": "primary"
@ -138,7 +159,7 @@
"parallel_downloads": 5
},
"profile_config": {
"gfx_driver": "All open-source (default)",
"gfx_driver": "All open-source",
"greeter": "sddm",
"profile": {
"details": [

View File

@ -18,12 +18,12 @@ classifiers = [
"Operating System :: POSIX :: Linux",
]
dependencies = [
"pyparted==3.13.0",
"pydantic==2.13.4",
"cryptography==48.0.1",
"textual==8.2.7",
"markdown-it-py==4.0.0",
"linkify-it-py==2.1.0",
"pyparted>=3.13.0",
"pydantic>=2.13.4",
"cryptography>=50.0.0",
"textual>=8.2.8",
"markdown-it-py>=4.2.0",
"linkify-it-py>=2.1.0",
]
[project.urls]
@ -32,14 +32,14 @@ Documentation = "https://archinstall.readthedocs.io/"
Source = "https://github.com/archlinux/archinstall"
[project.optional-dependencies]
log = ["systemd_python==235"]
log = ["systemd_python>=236"]
dev = [
"mypy==2.1.0",
"flake8==7.3.0",
"pre-commit==4.6.0",
"ruff==0.15.17",
"pylint==4.0.5",
"pytest==9.0.3",
"mypy>=2.3.0",
"flake8>=7.3.0",
"pre-commit>=4.6.1",
"ruff>=0.16.2",
"pylint>=4.0.7",
"pytest>=9.1.1",
"hypothesis>=6.152.4",
]
doc = ["sphinx"]
@ -72,7 +72,6 @@ python_version = "3.14"
files = "."
exclude = [
"^build/",
"^test_tooling/",
]
disallow_any_explicit = false
disallow_any_expr = false
@ -203,9 +202,6 @@ indent-style = "tab"
quote-style = "single"
docstring-code-format = true
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
[tool.ruff.lint]
select = [
"ASYNC", # flake8-async
@ -221,6 +217,7 @@ select = [
"G", # flake8-logging-format
"I", # isort
"ICN", # flake8-import-conventions
"INT", # flake8-gettext
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PGH", # pygrep-hooks
@ -233,10 +230,10 @@ select = [
"RUF", # Ruff-specific rules
"SLOT", # flake8-slot
"T10", # flake8-debugger
"TID", # flake8-tidy-imports (Enables TID252)
"UP", # pyupgrade
"W", # pycodestyle warnings
"YTT", # flake8-2020
"TID", # flake8-tidy-imports (Enables TID252)
]
ignore = [
@ -254,9 +251,17 @@ ignore = [
"RUF005", # collection-literal-concatenation
"RUF015", # unnecessary-iterable-allocation-for-first-element
"RUF039", # unraw-re-pattern
"RUF067", # non-empty-init-module
"W191", # tab-indentation
]
[tool.ruff.lint.flake8-gettext]
function-names = [
"tr",
"tr_noop",
]
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
[tool.ruff.lint.mccabe]
max-complexity = 40

View File

@ -14,7 +14,7 @@
"description": "Track runtime Python deps via Arch Linux repos instead of PyPI",
"managerFilePatterns": ["**/pyproject.toml"],
"matchStrings": [
"\"(?<depName>pyparted|pydantic|cryptography|textual|markdown-it-py|linkify-it-py)==(?<currentValue>[^\"]+)\""
"\"(?<depName>pyparted|pydantic|cryptography|textual|markdown-it-py|linkify-it-py)>=(?<currentValue>[^\"]+)\""
],
"depNameTemplate": "arch/python-{{{depName}}}",
"datasourceTemplate": "repology",
@ -26,7 +26,7 @@
"description": "Track systemd_python via Arch Linux repos (different package name)",
"managerFilePatterns": ["**/pyproject.toml"],
"matchStrings": [
"\"systemd_python==(?<currentValue>[^\"]+)\""
"\"systemd_python>=(?<currentValue>[^\"]+)\""
],
"depNameTemplate": "arch/python-systemd",
"datasourceTemplate": "repology",

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Verify archinstall/tui/binding_descriptions.py against the installed textual.
Run manually after a textual upgrade or after introducing a new textual
widget in archinstall/tui/components.py:
python3 test_tooling/check_binding_descriptions.py
The script parses the tr_noop() entries from binding_descriptions.py (no
archinstall import needed) and compares them with the MRO-merged binding
descriptions of the textual classes used in components.py. Exits non-zero
on any difference and prints the exact missing/stale entries.
"""
import re
import sys
from pathlib import Path
from textual.app import App
from textual.screen import Screen
from textual.widgets import Button, DataTable, HelpPanel, Input, OptionList, SelectionList
# Must mirror the textual widget classes used in archinstall/tui/components.py.
# Extend when a new widget is introduced there.
TEXTUAL_CLASSES = (App, Screen, Button, DataTable, HelpPanel, Input, OptionList, SelectionList)
MODULE_PATH = Path(__file__).parent.parent / 'archinstall/tui/binding_descriptions.py'
def live_descriptions() -> set[str]:
descriptions: set[str] = set()
# Walk the full MRO: _translate_bindings() operates on the merged bindings
# map, so descriptions inherited from ancestors are translated too.
for cls in TEXTUAL_CLASSES:
for klass in cls.__mro__:
for binding in klass.__dict__.get('BINDINGS', []):
description = getattr(binding, 'description', None)
if not description and isinstance(binding, tuple) and len(binding) > 2:
description = binding[2]
if description:
descriptions.add(str(description))
return descriptions
def listed_descriptions() -> set[str]:
content = MODULE_PATH.read_text(encoding='utf-8')
return set(re.findall(r"tr_noop\('([^']+)'\)", content))
def main() -> int:
live = live_descriptions()
listed = listed_descriptions()
if live == listed:
print(f'OK: {len(listed)} descriptions in sync with installed textual')
return 0
for description in sorted(live - listed):
print(f'missing from binding_descriptions.py: {description}')
for description in sorted(listed - live):
print(f'stale in binding_descriptions.py: {description}')
return 1
if __name__ == '__main__':
sys.exit(main())

View File

@ -11,6 +11,8 @@ import sys
import time
from argparse import ArgumentParser
from collections.abc import Iterator
from functools import cache
from pathlib import Path
from select import EPOLLHUP, EPOLLIN, epoll
from shutil import which
from types import TracebackType
@ -25,25 +27,32 @@ class ArgumentError(Exception):
pass
def get_master(interface):
def cpu_count() -> int:
count = os.cpu_count()
if not count:
return 1
return count - 1
def get_master(interface: str) -> str | None:
master_path = pathlib.Path(f'/sys/class/net/{interface}/master')
return master_path.readlink().name if master_path.exists() else None
def gray(text):
def gray(text: str) -> str:
return f'\033[38;5;246m{text}\033[0m'
def orange(text):
def orange(text: str) -> str:
return f'\033[38;5;208m{text}\033[0m'
def red(text):
def red(text: str) -> str:
return f'\033[31m{text}\033[0m'
sudo_password = None # Gets populated later
harddrives = {}
harddrives: dict[Path, str] = {}
username = getpass.getuser()
groupname = grp.getgrgid(os.getgid()).gr_name
@ -71,37 +80,37 @@ hardware = parser.add_argument_group('Hardware', 'General hardware specs for the
hardware.add_argument('--bios', action='store_true', help='Disables EFI (edk2/ovmf) and uses BIOS support instead', default=False)
hardware.add_argument('--memory', nargs='?', help='Ammount of memory to supply the machine', default=8192)
hardware.add_argument('--harddrive', action='append', help='Sets up one or more virtio-scsi-pci, size is defined by --harddrive test.qcow2:15G', type=str)
hardware.add_argument('--cpu', help='Sets the number of cores to allocate (default nproc -1)', type=str, default=os.cpu_count() - 1 if os.cpu_count() else 1)
hardware.add_argument('--cpu', help='Sets the number of cores to allocate (default nproc -1)', type=str, default=cpu_count())
hardware.add_argument('--resolution', help="Sets Qemu's VGA resolution", type=str, default='1920x1107')
kernel = parser.add_argument_group('Kernel', '--kernel specific arguments')
kernel.add_argument('--initrd', nargs='?', help='Defines which ISO to run (skips build all together)', default=None, type=pathlib.Path)
args, unknowns = parser.parse_known_args() # pylint: disable=redefined-outer-name
cli_args, _ = parser.parse_known_args()
if args.bios and args.uki:
if cli_args.bios and cli_args.uki:
raise ArgumentError('Cannot boot a --uki image with --bios mode (at least not that I know of).')
if args.uki is None and args.kernel is None and args.iso is None and args.harddrive is None:
if cli_args.uki is None and cli_args.kernel is None and cli_args.iso is None and cli_args.harddrive is None:
raise ArgumentError('Cannot boot this machine, define at least one of: --uki, --kernel, --iso, --harddrive')
if args.bridge is None and args.bridge_master:
if cli_args.bridge is None and cli_args.bridge_master:
raise ArgumentError('Cannot use --bridge-master without defining --bridge')
if args.bridge is None and args.bridge_mac:
if cli_args.bridge is None and cli_args.bridge_mac:
raise ArgumentError('Cannot use --bridge-mac without defining --bridge')
elif args.bridge and args.bridge_mac is None:
args.bridge_mac = '52:54:00:00:00:1'
elif cli_args.bridge and cli_args.bridge_mac is None:
cli_args.bridge_mac = '52:54:00:00:00:1'
if args.tap and not args.bridge and get_master(args.tap) is None:
if cli_args.tap and not cli_args.bridge and get_master(cli_args.tap) is None:
# We'll allow it, because maybe we're tesing what happens without networking, but the NIC exists.
# Or the user has some creative iptables/nftables forwarding.
print(orange('--tap does not have a master, consider adding --bridge or manual set a master using ip-link(8).'))
if args.tap is None and args.bridge:
if cli_args.tap is None and cli_args.bridge:
print(orange("--bridge* arguments will be ignored since there's no --tap defined"))
elif args.tap and args.tap_mac is None:
args.tap_mac = '52:54:00:00:00:2'
elif cli_args.tap and cli_args.tap_mac is None:
cli_args.tap_mac = '52:54:00:00:00:2'
class SysCallError(Exception):
@ -176,7 +185,7 @@ class SysCommandWorker:
return False
def __iter__(self, *args: str, **kwargs: dict[str, Any]) -> Iterator[bytes]: # pylint: disable=redefined-outer-name
def __iter__(self, *args: str, **kwargs: dict[str, Any]) -> Iterator[bytes]:
last_line = self._trace_log.rfind(b'\n')
lines = filter(None, self._trace_log[self._trace_log_pos : last_line].splitlines())
for line in lines:
@ -334,81 +343,82 @@ class SysCommandWorker:
return self._trace_log.decode(encoding)
def ensure_sudo():
global sudo_password # pylint: disable=global-statement
@cache
def get_sudo_password() -> str:
sudo_password = getpass.getpass(f'[sudo] password for {username}: ')
if sudo_password == '':
raise ValueError('Certain commands need sudo to work and no sudo password was given.')
if sudo_password is None:
if (sudo_password := getpass.getpass(f'[sudo] password for {username}: ')) == '':
raise ValueError('Certain commands need sudo to work and no sudo password was given.')
return sudo_password
def setup_networking():
if args.tap:
if pathlib.Path(f'/sys/class/net/{args.tap}').exists() is False:
print(gray(f'Creating {args.tap} for user {username} and group {groupname}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip tuntap add dev {args.tap} mode tap user {username} group {groupname}'), False
def setup_networking() -> None:
if cli_args.tap:
if pathlib.Path(f'/sys/class/net/{cli_args.tap}').exists() is False:
print(gray(f'Creating {cli_args.tap} for user {username} and group {groupname}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip tuntap add dev {cli_args.tap} mode tap user {username} group {groupname}'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
sudo_pw = get_sudo_password()
handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True
if args.bridge:
if pathlib.Path(f'/sys/class/net/{args.bridge}').exists() is False:
print(gray(f'Creating {args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link add name {args.bridge} type bridge'), False
if cli_args.bridge:
if pathlib.Path(f'/sys/class/net/{cli_args.bridge}').exists() is False:
print(gray(f'Creating {cli_args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link add name {cli_args.bridge} type bridge'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
sudo_pw = get_sudo_password()
handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True
if args.bridge_mac:
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge} address {args.bridge_mac}'), False
print(gray(f'Setting bridge {args.bridge} MAC address to {args.bridge_mac}'))
if cli_args.bridge_mac:
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {cli_args.bridge} address {cli_args.bridge_mac}'), False
print(gray(f'Setting bridge {cli_args.bridge} MAC address to {cli_args.bridge_mac}'))
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
sudo_pw = get_sudo_password()
handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True
if args.bridge_master and get_master(args.bridge) != args.bridge_master:
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge_master} master {args.bridge}'), False
print(gray(f'Setting interface {args.bridge_master} master to {args.bridge}'))
if cli_args.bridge_master and get_master(cli_args.bridge) != cli_args.bridge_master:
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {cli_args.bridge_master} master {cli_args.bridge}'), False
print(gray(f'Setting interface {cli_args.bridge_master} master to {cli_args}'))
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
sudo_pw = get_sudo_password()
handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True
print(gray(f'Setting interface {args.tap} master to {args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.tap} master {args.bridge}'), False
print(gray(f'Setting interface {cli_args.tap} master to {cli_args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {cli_args.tap} master {cli_args.bridge}'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
sudo_pw = get_sudo_password()
handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True
print(gray(f'Bringing up bridge {args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge} up'), False
print(gray(f'Bringing up bridge {cli_args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {cli_args.bridge} up'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
sudo_pw = get_sudo_password()
handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True
print(gray(f'Bringing interface {args.tap} up'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.tap} up'), False
print(gray(f'Bringing interface {cli_args.tap} up'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {cli_args.tap} up'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
sudo_pw = get_sudo_password()
handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True
def setup_disks():
if args.harddrive:
for harddrive_arg in args.harddrive:
def setup_disks() -> None:
if cli_args.harddrive:
for harddrive_arg in cli_args.harddrive:
path, size = harddrive_arg.split(':')
path = pathlib.Path(path.strip()).expanduser().resolve().absolute()
harddrives[path] = size.strip()
@ -425,7 +435,8 @@ def setup_disks():
setup_networking()
setup_disks()
if args.uki or args.bios is False:
disk_paths_hash = ''
if cli_args.uki or cli_args.bios is False:
disk_paths_hash = hashlib.sha1((''.join(sorted([str(x) for x in harddrives.keys()]))).encode()).hexdigest()
shutil.copy2('/usr/share/ovmf/x64/OVMF_CODE.secboot.4m.fd', f'./OVMF_CODE.secboot.4m.fd.{disk_paths_hash}')
@ -439,16 +450,16 @@ qemu += ' -machine q35,accel=kvm'
qemu += ' -object rng-random,filename=/dev/urandom,id=rng0'
qemu += ' -device virtio-rng-pci,rng=rng0'
qemu += ' -global driver=cfi.pflash01,property=secure,value=on'
qemu += f' -smp {args.cpu},sockets=1,dies=1,cores={args.cpu},threads=1'
qemu += f' -smp {cli_args.cpu},sockets=1,dies=1,cores={cli_args.cpu},threads=1'
# qemu += f' -vga vga'
qemu += f' -device VGA,edid=on,xres={args.resolution.split("x")[0]},yres={args.resolution.split("x")[1]}'
qemu += f' -device VGA,edid=on,xres={cli_args.resolution.split("x")[0]},yres={cli_args.resolution.split("x")[1]}'
qemu += ' -device intel-iommu,device-iotlb=on,caching-mode=on'
qemu += f' -m {args.memory}'
if args.bios is False:
qemu += f' -m {cli_args.memory}'
if cli_args.bios is False:
qemu += f' -drive if=pflash,format=raw,readonly=on,file=./OVMF_CODE.secboot.4m.fd.{disk_paths_hash}'
qemu += f' -drive if=pflash,format=raw,file=./OVMF_VARS.4m.fd.{disk_paths_hash}'
if args.uki:
qemu += f' -kernel {args.uki}'
if cli_args.uki:
qemu += f' -kernel {cli_args.uki}'
boot_index += 1
scsi_index = 0
for scsi_index, hdd in enumerate(harddrives.keys()):
@ -460,18 +471,18 @@ for scsi_index, hdd in enumerate(harddrives.keys()):
qemu += f' -blockdev \'{{"driver":"file","filename":"{hdd}","aio":"threads","node-name":"libvirt-{scsi_index}-storage","cache":{{"direct":false,"no-flush":false}},"auto-read-only":true,"discard":"unmap"}}\'' # noqa: E501
qemu += f' -blockdev \'{{"node-name":"libvirt-{scsi_index}-format","read-only":false,"discard":"unmap","cache":{{"direct":true,"no-flush":false}},"driver":"qcow2","file":"libvirt-{scsi_index}-storage","backing":null}}\'' # noqa: E501
boot_index += 1
if args.iso:
if cli_args.iso:
qemu += f' -device virtio-scsi-pci,bus=pcie.0,id=scsi{scsi_index + 1}'
qemu += f' -device scsi-cd,drive=cdrom0,bus=scsi{scsi_index + 1}.0,bootindex={boot_index}'
qemu += f' -drive file={args.iso},media=cdrom,if=none,format=raw,cache=none,id=cdrom0'
qemu += f' -drive file={cli_args.iso},media=cdrom,if=none,format=raw,cache=none,id=cdrom0'
boot_index += 1
# if args.vfio:
# qemu += f' -drive file={args.vfio},index=2,media=cdrom'
# if cli_args.vfio:
# qemu += f' -drive file={cli_args.vfio},index=2,media=cdrom'
if args.tap:
qemu += f' -device virtio-net-pci,mac={args.tap_mac},id=network0,netdev=network0.0,status=on,bus=pcie.0'
qemu += f' -netdev tap,ifname={args.tap},id=network0.0,script=no,downscript=no'
if cli_args.tap:
qemu += f' -device virtio-net-pci,mac={cli_args.tap_mac},id=network0,netdev=network0.0,status=on,bus=pcie.0'
qemu += f' -netdev tap,ifname={cli_args.tap},id=network0.0,script=no,downscript=no'
print(gray(qemu))

View File

@ -8,6 +8,16 @@ def config_fixture() -> Path:
return Path(__file__).parent / 'data' / 'test_config.json'
@pytest.fixture(scope='session')
def example_config_fixture() -> Path:
return Path(__file__).parent.parent / 'examples' / 'config-sample.json'
@pytest.fixture(scope='session')
def example_creds_fixture() -> Path:
return Path(__file__).parent.parent / 'examples' / 'creds-sample.json'
@pytest.fixture(scope='session')
def btrfs_config_fixture() -> Path:
return Path(__file__).parent / 'data' / 'test_config_btrfs.json'

View File

@ -1,3 +1,4 @@
import json
import os
from importlib.metadata import version
from pathlib import Path
@ -17,7 +18,7 @@ from archinstall.lib.models.application import (
)
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, Size
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import CustomRepository, CustomServer, MirrorConfiguration, MirrorRegion, SignCheck, SignOption
from archinstall.lib.models.network import NetworkConfiguration, Nic, NicType
@ -388,3 +389,57 @@ def test_encrypted_creds_with_env_var(
groups=[],
),
]
def test_example_config_parsing(
monkeypatch: MonkeyPatch,
example_config_fixture: Path,
example_creds_fixture: Path,
) -> None:
monkeypatch.setattr(
'sys.argv',
[
'archinstall',
'--config',
str(example_config_fixture),
'--creds',
str(example_creds_fixture),
],
)
handler = ArchConfigHandler()
arch_config = handler.config
assert arch_config.disk_config is not None
assert arch_config.profile_config is not None
assert arch_config.auth_config is not None
assert arch_config.auth_config.users
def test_example_config_partitions(example_config_fixture: Path) -> None:
# partition entries are only parsed when the configured device is present on
# the machine, which is never the case in CI, so read them here directly
config = json.loads(example_config_fixture.read_text())
device_modifications = config['disk_config']['device_modifications']
assert device_modifications
for device in device_modifications:
partitions = device['partitions']
assert partitions
previous_end = None
for partition in partitions:
assert 'dev_path' in partition
start = Size.parse_args(partition['start'])
end = start + Size.parse_args(partition['size'])
assert start.is_valid_start()
if previous_end is not None:
assert start >= previous_end
previous_end = end

View File

@ -0,0 +1,60 @@
from archinstall.lib.models.network import WifiNetwork
SAMPLE_SCAN_RESULTS = """bssid / frequency / signal level / flags / ssid
aa:bb:cc:dd:ee:01 2412 -40 [WPA2-PSK-CCMP][ESS] NoSpacesSSID
aa:bb:cc:dd:ee:02 2412 -50 [WPA2-PSK-CCMP][ESS] My Home Network
aa:bb:cc:dd:ee:03 5180 -60 [WPA2-PSK-CCMP][WPS][ESS] Free Public WiFi
aa:bb:cc:dd:ee:04 2412 -70 [WPA2-PSK-CCMP][ESS]
"""
def test_from_wpa_ssid_without_spaces() -> None:
networks = WifiNetwork.from_wpa(SAMPLE_SCAN_RESULTS)
ssids = [network.ssid for network in networks]
assert 'NoSpacesSSID' in ssids
def test_from_wpa_ssid_with_spaces() -> None:
networks = WifiNetwork.from_wpa(SAMPLE_SCAN_RESULTS)
ssids = [network.ssid for network in networks]
assert 'My Home Network' in ssids
def test_from_wpa_ssid_with_multiple_spaces() -> None:
networks = WifiNetwork.from_wpa(SAMPLE_SCAN_RESULTS)
ssids = [network.ssid for network in networks]
assert 'Free Public WiFi' in ssids
def test_from_wpa_empty_ssid() -> None:
networks = WifiNetwork.from_wpa(SAMPLE_SCAN_RESULTS)
empty = [network for network in networks if network.bssid == 'aa:bb:cc:dd:ee:04']
assert len(empty) == 1
assert empty[0].ssid == ''
def test_from_wpa_skips_header_and_blank_lines() -> None:
results = """
bssid / frequency / signal level / flags / ssid
aa:bb:cc:dd:ee:01 2412 -40 [WPA2-PSK-CCMP][ESS] SimpleNet
"""
networks = WifiNetwork.from_wpa(results)
assert len(networks) == 1
assert networks[0].ssid == 'SimpleNet'
assert networks[0].bssid == 'aa:bb:cc:dd:ee:01'
def test_from_wpa_preserves_flags_and_fields() -> None:
networks = WifiNetwork.from_wpa(SAMPLE_SCAN_RESULTS)
network = next(n for n in networks if n.ssid == 'My Home Network')
assert network.bssid == 'aa:bb:cc:dd:ee:02'
assert network.frequency == '2412'
assert network.signal_level == '-50'
assert network.flags == '[WPA2-PSK-CCMP][ESS]'