Compare commits

...

26 Commits
4.4 ... master

Author SHA1 Message Date
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
30 changed files with 5151 additions and 2199 deletions

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
- name: Install pre-dependencies
run: |
pacman -Sy --noconfirm tree git python-pyparted python-setuptools python-sphinx python-sphinx_rtd_theme python-build python-installer python-wheel

View File

@ -26,7 +26,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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.15.22
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

@ -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

@ -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,7 +41,7 @@ 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()

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 ""

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 ""

File diff suppressed because it is too large Load Diff

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"},

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

@ -20,8 +20,8 @@ classifiers = [
dependencies = [
"pyparted==3.13.0",
"pydantic==2.13.4",
"cryptography==48.0.1",
"textual==8.2.7",
"cryptography==49.0.0",
"textual==8.2.8",
"markdown-it-py==4.0.0",
"linkify-it-py==2.1.0",
]
@ -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",
"mypy==2.3.0",
"flake8==7.3.0",
"pre-commit==4.6.0",
"ruff==0.15.17",
"pylint==4.0.5",
"pytest==9.0.3",
"ruff==0.15.22",
"pylint==4.0.6",
"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

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))