Compare commits

..

38 Commits
4.4 ... master

Author SHA1 Message Date
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
46 changed files with 6847 additions and 2351 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.plugins import load_plugin
from archinstall.lib.translationhandler import Language, tr, translation_handler from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_key_value_pair 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.lib.version import get_version
from archinstall.tui.components import tui from archinstall.tui.components import tui
@ -404,7 +403,7 @@ class ArchConfig:
) -> None: ) -> None:
save_path = dest_path or DEFAULT_SAVE_PATH save_path = dest_path or DEFAULT_SAVE_PATH
if not is_valid_path(save_path): if not save_path.is_dir():
warn( warn(
f'Destination directory {save_path} does not exist or is not a directory\n.', f'Destination directory {save_path} does not exist or is not a directory\n.',
'Configuration files can not be saved', 'Configuration files can not be saved',
@ -416,7 +415,7 @@ class ArchConfig:
self.save_user_creds(save_path, password=password) self.save_user_creds(save_path, password=password)
def save_user_config(self, dest_path: Path) -> None: 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.') error(f'Invalid path {dest_path}. User configuration could not be saved.')
return return
@ -430,7 +429,7 @@ class ArchConfig:
dest_path: Path, dest_path: Path,
password: str | None = None, password: str | None = 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.') error(f'Invalid path {dest_path}. User credentials could not be saved.')
return return

View File

@ -1,8 +1,9 @@
from pathlib import Path from pathlib import Path
from subprocess import CalledProcessError
from pydantic import BaseModel 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.exceptions import DiskError, SysCallError
from archinstall.lib.log import debug, info, warn from archinstall.lib.log import debug, info, warn
from archinstall.lib.models.device import LsblkInfo from archinstall.lib.models.device import LsblkInfo
@ -29,19 +30,18 @@ def _fetch_lsblk_info(
cmd.append(str(dev_path)) cmd.append(str(dev_path))
try: try:
worker = SysCommand(cmd) result = run(cmd)
except SysCallError as err: except CalledProcessError as err:
# Get the output minus the message/info from lsblk if it returns a non-zero exit code. # Get the output minus the message/info from lsblk if it returns a non-zero exit code.
if err.worker_log: if stdout := err.stdout:
debug(f'Error calling lsblk: {err.worker_log.decode()}') debug(f'Error calling lsblk: {stdout.decode().rstrip()}')
if dev_path: if dev_path:
raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') raise DiskError(f'Failed to read disk "{dev_path}" with lsblk')
raise err raise err
output = worker.output(remove_cr=False) return LsblkOutput.model_validate_json(result.stdout)
return LsblkOutput.model_validate_json(output)
def get_lsblk_info( def get_lsblk_info(

View File

@ -660,16 +660,12 @@ class Installer:
# in the first column of the entry; check for both cases. # in the first column of the entry; check for both cases.
entry_re = re.compile(rf'#{lang}(\.{encoding})?{modifier} {encoding}') entry_re = re.compile(rf'#{lang}(\.{encoding})?{modifier} {encoding}')
lang_value = None
for index, line in enumerate(locale_gen_lines): for index, line in enumerate(locale_gen_lines):
if entry_re.match(line): if entry_re.match(line):
uncommented_line = line.removeprefix('#') locale_gen_lines[index] = line.removeprefix('#')
locale_gen_lines[index] = uncommented_line
locale_gen.write_text(''.join(locale_gen_lines)) locale_gen.write_text(''.join(locale_gen_lines))
lang_value = uncommented_line.split()[0]
break break
else:
if lang_value is None:
error(f"Invalid locale: language '{locale_config.sys_lang}', encoding '{locale_config.sys_enc}'") error(f"Invalid locale: language '{locale_config.sys_lang}', encoding '{locale_config.sys_enc}'")
return False return False
@ -679,7 +675,7 @@ class Installer:
error(f'Failed to run locale-gen on target: {e}') error(f'Failed to run locale-gen on target: {e}')
return False 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 return True
def set_timezone(self, zone: str) -> bool: def set_timezone(self, zone: str) -> bool:
@ -1480,20 +1476,20 @@ class Installer:
parent_dev_path = get_parent_device_path(efi_partition.safe_dev_path) 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)
try: 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 ('BOOTIA32.EFI', 'BOOTX64.EFI'):
(limine_path / file).copy_into(efi_dir_path) (limine_path / file).copy_into(efi_dir_path)
except Exception as err: except Exception as err:

View File

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

View File

@ -58,7 +58,7 @@ class WifiHandler(InstanceRunnable[bool]):
async def _enable_supplicant(self, wifi_iface: str) -> bool: async def _enable_supplicant(self, wifi_iface: str) -> bool:
self._wpa_config.load_config() 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: if result.success:
debug('wpa_supplicant already running') debug('wpa_supplicant already running')
@ -72,18 +72,19 @@ class WifiHandler(InstanceRunnable[bool]):
try: try:
SysCommand(f'wpa_supplicant -B -i {wifi_iface} -c {self._wpa_config.config_file}') 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: except SysCallError as err:
debug(f'failed to enable wpa_supplicant: {err}') debug(f'failed to enable wpa_supplicant: {err}')
return False return False
result = self._wpa_cli('status')
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: def _find_wifi_interface(self) -> str | None:
net_path = Path('/sys/class/net') net_path = Path('/sys/class/net')
@ -197,15 +198,6 @@ class WifiHandler(InstanceRunnable[bool]):
try: try:
result = SysCommand(cmd).decode() 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: except SysCallError as err:
debug(f'error running wpa_cli command: {err}') debug(f'error running wpa_cli command: {err}')
return WpaCliResult( return WpaCliResult(
@ -213,6 +205,15 @@ class WifiHandler(InstanceRunnable[bool]):
error=f'Error running wpa_cli command: {err}', 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: def _find_network_id(self, ssid: str, iface: str) -> int | None:
result = self._wpa_cli('list_networks', iface) result = self._wpa_cli('list_networks', iface)
@ -254,18 +255,18 @@ class WifiHandler(InstanceRunnable[bool]):
try: try:
result = self._wpa_cli('scan_results', iface) 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: except SysCallError as err:
debug('Unable to retrieve wifi results') debug('Unable to retrieve wifi results')
raise err 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

@ -258,6 +258,10 @@ class ProfileHandler:
""" """
try: try:
data = fetch_data_from_url(url) data = fetch_data_from_url(url)
except ValueError:
err = tr('Unable to fetch profile from specified url: {}').format(url)
error(err)
else:
b_data = bytes(data, 'utf-8') b_data = bytes(data, 'utf-8')
with NamedTemporaryFile(delete=False, suffix='.py') as fp: with NamedTemporaryFile(delete=False, suffix='.py') as fp:
@ -266,10 +270,6 @@ class ProfileHandler:
profiles = self._process_profile_file(filepath) profiles = self._process_profile_file(filepath)
self.remove_custom_profiles(profiles) 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]: def _load_profile_class(self, module: ModuleType) -> list[Profile]:
""" """

View File

@ -82,13 +82,14 @@ class TranslationHandler:
if not running_from_iso(): if not running_from_iso():
return 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: 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)]) SysCommand(['setfont', '-O', str(self._font_backup), '-om', str(self._cmap_backup)])
except SysCallError as err: except SysCallError as err:
debug(f'Failed to save console font: {err}') debug(f'Failed to save console font: {err}')
@ -136,21 +137,21 @@ class TranslationHandler:
try: try:
# get a translation for a specific language # get a translation for a specific language
translation = gettext.translation('base', localedir=self._get_locales_dir(), languages=(abbr, lang)) 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)
except FileNotFoundError as err: except FileNotFoundError as err:
raise FileNotFoundError(f"Could not locate language file for '{lang}': {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 return languages
def _load_language_mappings(self) -> list[dict[str, str]]: def _load_language_mappings(self) -> list[dict[str, str]]:
@ -277,6 +278,15 @@ def tr(message: str) -> str:
return str(_DeferredTranslation(message)) 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] builtins._ = _DeferredTranslation # type: ignore[attr-defined]

View File

@ -9,6 +9,14 @@ if TYPE_CHECKING:
from _typeshed import DataclassInstance 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( def as_key_value_pair(
entries: dict[str, str | list[str] | bool], entries: dict[str, str | list[str] | bool],
ignore_empty: bool = True, ignore_empty: bool = True,
@ -33,28 +41,11 @@ def as_key_value_pair(
if isinstance(value, list): if isinstance(value, list):
value = '\n '.join(str(val) for val in value) 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() 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( def _get_values(
o: DataclassInstance, o: DataclassInstance,
class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument] class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]

View File

@ -1,10 +1,8 @@
import secrets import secrets
import string import string
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
from archinstall.lib.utils.format import as_columns
def timestamp() -> str: def timestamp() -> str:
@ -25,29 +23,3 @@ def running_from_iso() -> bool:
def generate_password(length: int = 64) -> str: def generate_password(length: int = 64) -> str:
haystack = string.printable # digits, ascii_letters, punctuation (!"#$[] etc) and whitespace haystack = string.printable # digits, ascii_letters, punctuation (!"#$[] etc) and whitespace
return ''.join(secrets.choice(haystack) for _ in range(length)) 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 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 :) 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" msgid "Iteration time"
msgstr "" msgstr ""
msgid ""
"No disks were detected. A disk is required to be able to install Arch Linux"
msgstr ""
msgid "Select disks for the installation" msgid "Select disks for the installation"
msgstr "" msgstr ""
@ -1220,12 +1224,192 @@ msgstr ""
msgid "Starting device modifications in " msgid "Starting device modifications in "
msgstr "" 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" msgid "Ok"
msgstr "" msgstr ""
msgid "Input cannot be empty" msgid "Input cannot be empty"
msgstr "" msgstr ""
msgid "Show/Hide help"
msgstr ""
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""

View File

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

View File

@ -221,12 +221,27 @@ msgstr "Imágenes del núcleo unificadas"
msgid "Install to removable location" msgid "Install to removable location"
msgstr "Instalar a ubicación desmontable" msgstr "Instalar a ubicación desmontable"
msgid "Plymouth"
msgstr "Plymouth"
msgid "Will install to /EFI/BOOT/ (removable location, safe default)" msgid "Will install to /EFI/BOOT/ (removable location, safe default)"
msgstr "Instalará a /EFI/BOOT/ (ubicación desmontable, segura por defecto)" msgstr "Instalará a /EFI/BOOT/ (ubicación desmontable, segura por defecto)"
msgid "Will install to custom location with NVRAM entry" msgid "Will install to custom location with NVRAM entry"
msgstr "Instalará a ubicación personalizada con entrada NVRAM" 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?" msgid "Would you like to use unified kernel images?"
msgstr "¿Le gustaría utilizar imágenes del núcleo unificadas?" 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" msgid "UEFI is not detected and some options are disabled"
msgstr "No se detecta UEFI y algunas opciones están desactivadas" 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" msgid "The specified configuration will be applied"
msgstr "Se aplicará la configuración especificada" msgstr "Se aplicará la configuración especificada"
@ -263,9 +281,6 @@ msgstr "¿Quiere continuar?"
msgid "Configuration preview" msgid "Configuration preview"
msgstr "Vista previa de la configuración" 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" msgid "No configuration"
msgstr "Sin configuración" msgstr "Sin configuración"
@ -332,6 +347,9 @@ msgstr "Contraseña"
msgid "Iteration time" msgid "Iteration time"
msgstr "Tiempo de iteración" 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" msgid "Select disks for the installation"
msgstr "Seleccione discos para la instalación" msgstr "Seleccione discos para la instalación"
@ -627,16 +645,21 @@ msgstr "Color"
msgid "Kernel" msgid "Kernel"
msgstr "Núcleo" msgstr "Núcleo"
msgid "Missing configurations:\n" msgid "No network configuration selected. Network will need to be set up manually on the installed system."
msgstr "Configuraciones que faltan:\n" msgstr "No se ha seleccionado ninguna configuración de red. La red deberá configurarse manualmente en el sistema instalado."
#, python-brace-format msgid "Missing configurations:"
msgid "Invalid configuration: {}" msgstr "Configuraciones faltantes:"
msgstr "Configuración no válida: {}"
msgid "Invalid configuration:"
msgstr "Configuración no válida:"
msgid "Ready to install" msgid "Ready to install"
msgstr "Listo para instalar" msgstr "Listo para instalar"
msgid "Warnings:"
msgstr "Advertencias:"
msgid "Profiles" msgid "Profiles"
msgstr "Perfiles" msgstr "Perfiles"
@ -858,6 +881,10 @@ msgstr "UKI activado"
msgid "Removable" msgid "Removable"
msgstr "Desmontable" msgstr "Desmontable"
#, python-brace-format
msgid "Plymouth \"{}\""
msgstr "Plymouth «{}»"
msgid "Use a best-effort default partition layout" msgid "Use a best-effort default partition layout"
msgstr "Utlizar un diseño de partición predeterminado de mejor esfuerzo" msgstr "Utlizar un diseño de partición predeterminado de mejor esfuerzo"
@ -965,8 +992,8 @@ msgstr "Grupo de paquetes:"
msgid "Color enabled" msgid "Color enabled"
msgstr "Color activado" msgstr "Color activado"
#, python-brace-format #, fuzzy, python-brace-format
msgid "{} grphics driver" msgid "{} graphics driver"
msgstr "{} controlador de gráficos" msgstr "{} controlador de gráficos"
#, python-brace-format #, python-brace-format
@ -1032,15 +1059,15 @@ msgstr "¿Desea conectarse al Wi-Fi?"
msgid "No wifi interface found" msgid "No wifi interface found"
msgstr "No se encontró ninguna interfaz Wi-Fi" 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" msgid "Select wifi network to connect to"
msgstr "Seleccione una red Wi-Fi para conectarse" msgstr "Seleccione una red Wi-Fi para conectarse"
msgid "Scanning wifi networks..." msgid "Scanning wifi networks..."
msgstr "Buscando redes Wi-Fi..." msgstr "Buscando redes Wi-Fi..."
msgid "No wifi networks found"
msgstr "No se encontró redes Wi-Fi"
msgid "Failed setting up wifi" msgid "Failed setting up wifi"
msgstr "Error al configurar el Wi-Fi" msgstr "Error al configurar el Wi-Fi"
@ -1608,9 +1635,6 @@ msgstr " (predeterminado)"
#~ msgid "Subvolume name :" #~ msgid "Subvolume name :"
#~ msgstr "Nombre del subvolumen :" #~ msgstr "Nombre del subvolumen :"
#~ msgid "Select a mount point :"
#~ msgstr "Seleccione un punto de montaje :"
#~ msgid "Select the desired subvolume options " #~ msgid "Select the desired subvolume options "
#~ msgstr "Seleccione las opciones de subvolumen deseadas " #~ msgstr "Seleccione las opciones de subvolumen deseadas "

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -14,8 +14,14 @@ usage() {
} }
generate_pot() { 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 \ 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 -d base -o locales/base.pot
} }

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." msgid "No network configuration selected. Network will need to be set up manually on the installed system."
msgstr "Конфігурацію мережі не обрано. Мережу доведеться налаштувати вручну на встановленій системі." 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

@ -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() _ = 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): class _OptionList(OptionList):
BINDINGS: ClassVar = [ BINDINGS: ClassVar = [
Binding('down', 'cursor_down', 'Down', show=True), Binding('down', 'cursor_down', 'Down', show=True),
@ -309,7 +322,7 @@ class OptionListScreen(BaseScreen[ValueT]):
yield ScrollableContainer(preview_label) yield ScrollableContainer(preview_label)
if self._filter: if self._filter:
yield Input(placeholder='/filter', id='filter-input') yield _Input(placeholder='/filter', id='filter-input')
yield Footer() yield Footer()
@ -540,7 +553,7 @@ class SelectListScreen(BaseScreen[ValueT]):
yield ScrollableContainer(preview_label) yield ScrollableContainer(preview_label)
if self._filter: if self._filter:
yield Input(placeholder='/filter', id='filter-input') yield _Input(placeholder='/filter', id='filter-input')
yield Footer() yield Footer()
@ -702,12 +715,12 @@ class ConfirmationScreen(BaseScreen[ValueT]):
with Vertical(classes='content-container'): with Vertical(classes='content-container'):
with Horizontal(classes='buttons-container'): with Horizontal(classes='buttons-container'):
for item in self._group.items: for item in self._group.items:
yield Button(item.text, id=item.key) yield _Button(item.text, id=item.key)
else: else:
with Vertical(): with Vertical():
with Horizontal(classes='buttons-container'): with Horizontal(classes='buttons-container'):
for item in self._group.items: for item in self._group.items:
yield Button(item.text, id=item.key) yield _Button(item.text, id=item.key)
yield Rule(orientation='horizontal') yield Rule(orientation='horizontal')
if self._preview_header is not None: if self._preview_header is not None:
@ -848,7 +861,7 @@ class InputScreen(BaseScreen[str]):
with Center(classes='container-wrapper'): with Center(classes='container-wrapper'):
with Vertical(classes='input-content'): with Vertical(classes='input-content'):
yield Input( yield _Input(
placeholder=self._placeholder, placeholder=self._placeholder,
password=self._password, password=self._password,
value=self._default_value, value=self._default_value,

View File

@ -20,9 +20,9 @@ classifiers = [
dependencies = [ dependencies = [
"pyparted==3.13.0", "pyparted==3.13.0",
"pydantic==2.13.4", "pydantic==2.13.4",
"cryptography==48.0.1", "cryptography==49.0.0",
"textual==8.2.7", "textual==8.2.8",
"markdown-it-py==4.0.0", "markdown-it-py==4.2.0",
"linkify-it-py==2.1.0", "linkify-it-py==2.1.0",
] ]
@ -32,14 +32,14 @@ Documentation = "https://archinstall.readthedocs.io/"
Source = "https://github.com/archlinux/archinstall" Source = "https://github.com/archlinux/archinstall"
[project.optional-dependencies] [project.optional-dependencies]
log = ["systemd_python==235"] log = ["systemd_python==236"]
dev = [ dev = [
"mypy==2.1.0", "mypy==2.3.0",
"flake8==7.3.0", "flake8==7.3.0",
"pre-commit==4.6.0", "pre-commit==4.6.1",
"ruff==0.15.17", "ruff==0.15.22",
"pylint==4.0.5", "pylint==4.0.6",
"pytest==9.0.3", "pytest==9.1.1",
"hypothesis>=6.152.4", "hypothesis>=6.152.4",
] ]
doc = ["sphinx"] doc = ["sphinx"]
@ -72,7 +72,6 @@ python_version = "3.14"
files = "." files = "."
exclude = [ exclude = [
"^build/", "^build/",
"^test_tooling/",
] ]
disallow_any_explicit = false disallow_any_explicit = false
disallow_any_expr = false disallow_any_expr = false

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 import time
from argparse import ArgumentParser from argparse import ArgumentParser
from collections.abc import Iterator from collections.abc import Iterator
from functools import cache
from pathlib import Path
from select import EPOLLHUP, EPOLLIN, epoll from select import EPOLLHUP, EPOLLIN, epoll
from shutil import which from shutil import which
from types import TracebackType from types import TracebackType
@ -25,25 +27,32 @@ class ArgumentError(Exception):
pass 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') master_path = pathlib.Path(f'/sys/class/net/{interface}/master')
return master_path.readlink().name if master_path.exists() else None 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' 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' return f'\033[38;5;208m{text}\033[0m'
def red(text): def red(text: str) -> str:
return f'\033[31m{text}\033[0m' return f'\033[31m{text}\033[0m'
sudo_password = None # Gets populated later harddrives: dict[Path, str] = {}
harddrives = {}
username = getpass.getuser() username = getpass.getuser()
groupname = grp.getgrgid(os.getgid()).gr_name 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('--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('--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('--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') hardware.add_argument('--resolution', help="Sets Qemu's VGA resolution", type=str, default='1920x1107')
kernel = parser.add_argument_group('Kernel', '--kernel specific arguments') 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) 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).') 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') 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') 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') raise ArgumentError('Cannot use --bridge-mac without defining --bridge')
elif args.bridge and args.bridge_mac is None: elif cli_args.bridge and cli_args.bridge_mac is None:
args.bridge_mac = '52:54:00:00:00:1' 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. # 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. # 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).')) 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")) print(orange("--bridge* arguments will be ignored since there's no --tap defined"))
elif args.tap and args.tap_mac is None: elif cli_args.tap and cli_args.tap_mac is None:
args.tap_mac = '52:54:00:00:00:2' cli_args.tap_mac = '52:54:00:00:00:2'
class SysCallError(Exception): class SysCallError(Exception):
@ -176,7 +185,7 @@ class SysCommandWorker:
return False 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') last_line = self._trace_log.rfind(b'\n')
lines = filter(None, self._trace_log[self._trace_log_pos : last_line].splitlines()) lines = filter(None, self._trace_log[self._trace_log_pos : last_line].splitlines())
for line in lines: for line in lines:
@ -334,81 +343,82 @@ class SysCommandWorker:
return self._trace_log.decode(encoding) return self._trace_log.decode(encoding)
def ensure_sudo(): @cache
global sudo_password # pylint: disable=global-statement 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: return sudo_password
if (sudo_password := getpass.getpass(f'[sudo] password for {username}: ')) == '':
raise ValueError('Certain commands need sudo to work and no sudo password was given.')
def setup_networking(): def setup_networking() -> None:
if args.tap: if cli_args.tap:
if pathlib.Path(f'/sys/class/net/{args.tap}').exists() is False: if pathlib.Path(f'/sys/class/net/{cli_args.tap}').exists() is False:
print(gray(f'Creating {args.tap} for user {username} and group {groupname}')) print(gray(f'Creating {cli_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 handle, pw_prompted = SysCommandWorker(f'sudo ip tuntap add dev {cli_args.tap} mode tap user {username} group {groupname}'), False
while handle.is_alive(): while handle.is_alive():
if b'password for' in handle and pw_prompted is False: if b'password for' in handle and pw_prompted is False:
ensure_sudo() sudo_pw = get_sudo_password()
handle.write(bytes(sudo_password, 'UTF-8')) handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True pw_prompted = True
if args.bridge: if cli_args.bridge:
if pathlib.Path(f'/sys/class/net/{args.bridge}').exists() is False: if pathlib.Path(f'/sys/class/net/{cli_args.bridge}').exists() is False:
print(gray(f'Creating {args.bridge}')) print(gray(f'Creating {cli_args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link add name {args.bridge} type bridge'), False handle, pw_prompted = SysCommandWorker(f'sudo ip link add name {cli_args.bridge} type bridge'), False
while handle.is_alive(): while handle.is_alive():
if b'password for' in handle and pw_prompted is False: if b'password for' in handle and pw_prompted is False:
ensure_sudo() sudo_pw = get_sudo_password()
handle.write(bytes(sudo_password, 'UTF-8')) handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True pw_prompted = True
if args.bridge_mac: if cli_args.bridge_mac:
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge} address {args.bridge_mac}'), False handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {cli_args.bridge} address {cli_args.bridge_mac}'), False
print(gray(f'Setting bridge {args.bridge} MAC address to {args.bridge_mac}')) print(gray(f'Setting bridge {cli_args.bridge} MAC address to {cli_args.bridge_mac}'))
while handle.is_alive(): while handle.is_alive():
if b'password for' in handle and pw_prompted is False: if b'password for' in handle and pw_prompted is False:
ensure_sudo() sudo_pw = get_sudo_password()
handle.write(bytes(sudo_password, 'UTF-8')) handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True pw_prompted = True
if args.bridge_master and get_master(args.bridge) != args.bridge_master: 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 {args.bridge_master} master {args.bridge}'), False handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {cli_args.bridge_master} master {cli_args.bridge}'), False
print(gray(f'Setting interface {args.bridge_master} master to {args.bridge}')) print(gray(f'Setting interface {cli_args.bridge_master} master to {cli_args}'))
while handle.is_alive(): while handle.is_alive():
if b'password for' in handle and pw_prompted is False: if b'password for' in handle and pw_prompted is False:
ensure_sudo() sudo_pw = get_sudo_password()
handle.write(bytes(sudo_password, 'UTF-8')) handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True pw_prompted = True
print(gray(f'Setting interface {args.tap} master to {args.bridge}')) print(gray(f'Setting interface {cli_args.tap} master to {cli_args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.tap} master {args.bridge}'), False handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {cli_args.tap} master {cli_args.bridge}'), False
while handle.is_alive(): while handle.is_alive():
if b'password for' in handle and pw_prompted is False: if b'password for' in handle and pw_prompted is False:
ensure_sudo() sudo_pw = get_sudo_password()
handle.write(bytes(sudo_password, 'UTF-8')) handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True pw_prompted = True
print(gray(f'Bringing up bridge {args.bridge}')) print(gray(f'Bringing up bridge {cli_args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge} up'), False handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {cli_args.bridge} up'), False
while handle.is_alive(): while handle.is_alive():
if b'password for' in handle and pw_prompted is False: if b'password for' in handle and pw_prompted is False:
ensure_sudo() sudo_pw = get_sudo_password()
handle.write(bytes(sudo_password, 'UTF-8')) handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True pw_prompted = True
print(gray(f'Bringing interface {args.tap} up')) print(gray(f'Bringing interface {cli_args.tap} up'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.tap} up'), False handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {cli_args.tap} up'), False
while handle.is_alive(): while handle.is_alive():
if b'password for' in handle and pw_prompted is False: if b'password for' in handle and pw_prompted is False:
ensure_sudo() sudo_pw = get_sudo_password()
handle.write(bytes(sudo_password, 'UTF-8')) handle.write(bytes(sudo_pw, 'UTF-8'))
pw_prompted = True pw_prompted = True
def setup_disks(): def setup_disks() -> None:
if args.harddrive: if cli_args.harddrive:
for harddrive_arg in args.harddrive: for harddrive_arg in cli_args.harddrive:
path, size = harddrive_arg.split(':') path, size = harddrive_arg.split(':')
path = pathlib.Path(path.strip()).expanduser().resolve().absolute() path = pathlib.Path(path.strip()).expanduser().resolve().absolute()
harddrives[path] = size.strip() harddrives[path] = size.strip()
@ -425,7 +435,8 @@ def setup_disks():
setup_networking() setup_networking()
setup_disks() 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() 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}') 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 += ' -object rng-random,filename=/dev/urandom,id=rng0'
qemu += ' -device virtio-rng-pci,rng=rng0' qemu += ' -device virtio-rng-pci,rng=rng0'
qemu += ' -global driver=cfi.pflash01,property=secure,value=on' 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' -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 += ' -device intel-iommu,device-iotlb=on,caching-mode=on'
qemu += f' -m {args.memory}' qemu += f' -m {cli_args.memory}'
if args.bios is False: 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,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}' qemu += f' -drive if=pflash,format=raw,file=./OVMF_VARS.4m.fd.{disk_paths_hash}'
if args.uki: if cli_args.uki:
qemu += f' -kernel {args.uki}' qemu += f' -kernel {cli_args.uki}'
boot_index += 1 boot_index += 1
scsi_index = 0 scsi_index = 0
for scsi_index, hdd in enumerate(harddrives.keys()): 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 \'{{"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 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 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 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' -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 boot_index += 1
# if args.vfio: # if cli_args.vfio:
# qemu += f' -drive file={args.vfio},index=2,media=cdrom' # qemu += f' -drive file={cli_args.vfio},index=2,media=cdrom'
if args.tap: if cli_args.tap:
qemu += f' -device virtio-net-pci,mac={args.tap_mac},id=network0,netdev=network0.0,status=on,bus=pcie.0' 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={args.tap},id=network0.0,script=no,downscript=no' qemu += f' -netdev tap,ifname={cli_args.tap},id=network0.0,script=no,downscript=no'
print(gray(qemu)) print(gray(qemu))