Compare commits
37 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
a633fc5d27 | |
|
|
6dff8525fd | |
|
|
b7ad6f7514 | |
|
|
05ceb5789f | |
|
|
fc7dd17ebd | |
|
|
2bf116030b | |
|
|
389f4cfc64 | |
|
|
91d6d4dca1 | |
|
|
2f344a84ff | |
|
|
2751716d2e | |
|
|
b6d8fe25a1 | |
|
|
b0bfb43661 | |
|
|
39dbebf26c | |
|
|
caff461f0b | |
|
|
01c9432afd | |
|
|
53158a8aad | |
|
|
46320deb33 | |
|
|
12baa2e64a | |
|
|
2531825b59 | |
|
|
e3e9d43324 | |
|
|
781728b91b | |
|
|
34c299e42e | |
|
|
3f4f815a34 | |
|
|
c60385a6f7 | |
|
|
c110081a42 | |
|
|
8902d2e310 | |
|
|
aca482b691 | |
|
|
338e3cf4b8 | |
|
|
ec9295634b | |
|
|
f94c27430c | |
|
|
caa8887204 | |
|
|
8aec36aa0a | |
|
|
ebdf625e5c | |
|
|
fc3a22683e | |
|
|
faf6fe5ce2 | |
|
|
e2b8697864 | |
|
|
214e55248c |
|
|
@ -6,7 +6,7 @@ jobs:
|
|||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- run: pacman --noconfirm -Syu bandit
|
||||
- name: Security checkup with Bandit
|
||||
run: bandit -r archinstall || exit 0
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ jobs:
|
|||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: Prepare arch
|
||||
run: |
|
||||
pacman-key --init
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ jobs:
|
|||
image: archlinux/archlinux:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
||||
- name: Install pre-dependencies
|
||||
run: |
|
||||
pacman -Sy --noconfirm tree git python-pyparted python-setuptools python-sphinx python-sphinx_rtd_theme python-build python-installer python-wheel
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
image: archlinux/archlinux:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- run: pwd
|
||||
- run: find .
|
||||
- run: cat /etc/os-release
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ jobs:
|
|||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: Prepare arch
|
||||
run: |
|
||||
pacman-key --init
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ jobs:
|
|||
container:
|
||||
image: archlinux/archlinux:latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: Prepare arch
|
||||
run: |
|
||||
pacman-key --init
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ jobs:
|
|||
image: archlinux/archlinux:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: Prepare arch
|
||||
run: |
|
||||
pacman-key --init
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
image: archlinux/archlinux:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: Prepare arch
|
||||
run: |
|
||||
pacman-key --init
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
image: archlinux/archlinux:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: Prepare arch
|
||||
run: |
|
||||
pacman-key --init
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ jobs:
|
|||
ruff_format_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
|
||||
- run: ruff format --diff
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@ jobs:
|
|||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
name: Validate translations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: Install gettext
|
||||
run: sudo apt-get update && sudo apt-get install -y gettext
|
||||
- name: Run translation checks
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
image: archlinux/archlinux:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- run: pwd
|
||||
- run: find .
|
||||
- run: cat /etc/os-release
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ from archinstall.lib.models.users import Password, User, UserSerialization
|
|||
from archinstall.lib.plugins import load_plugin
|
||||
from archinstall.lib.translationhandler import Language, tr, translation_handler
|
||||
from archinstall.lib.utils.format import as_key_value_pair
|
||||
from archinstall.lib.utils.util import is_valid_path
|
||||
from archinstall.lib.version import get_version
|
||||
from archinstall.tui.components import tui
|
||||
|
||||
|
|
@ -404,7 +403,7 @@ class ArchConfig:
|
|||
) -> None:
|
||||
save_path = dest_path or DEFAULT_SAVE_PATH
|
||||
|
||||
if not is_valid_path(save_path):
|
||||
if not save_path.is_dir():
|
||||
warn(
|
||||
f'Destination directory {save_path} does not exist or is not a directory\n.',
|
||||
'Configuration files can not be saved',
|
||||
|
|
@ -416,7 +415,7 @@ class ArchConfig:
|
|||
self.save_user_creds(save_path, password=password)
|
||||
|
||||
def save_user_config(self, dest_path: Path) -> None:
|
||||
if not is_valid_path(dest_path):
|
||||
if not dest_path.is_dir():
|
||||
error(f'Invalid path {dest_path}. User configuration could not be saved.')
|
||||
return
|
||||
|
||||
|
|
@ -430,7 +429,7 @@ class ArchConfig:
|
|||
dest_path: Path,
|
||||
password: str | None = None,
|
||||
) -> None:
|
||||
if not is_valid_path(dest_path):
|
||||
if not dest_path.is_dir():
|
||||
error(f'Invalid path {dest_path}. User credentials could not be saved.')
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from pathlib import Path
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from archinstall.lib.command import SysCommand
|
||||
from archinstall.lib.command import SysCommand, run
|
||||
from archinstall.lib.exceptions import DiskError, SysCallError
|
||||
from archinstall.lib.log import debug, info, warn
|
||||
from archinstall.lib.models.device import LsblkInfo
|
||||
|
|
@ -29,19 +30,18 @@ def _fetch_lsblk_info(
|
|||
cmd.append(str(dev_path))
|
||||
|
||||
try:
|
||||
worker = SysCommand(cmd)
|
||||
except SysCallError as err:
|
||||
result = run(cmd)
|
||||
except CalledProcessError as err:
|
||||
# Get the output minus the message/info from lsblk if it returns a non-zero exit code.
|
||||
if err.worker_log:
|
||||
debug(f'Error calling lsblk: {err.worker_log.decode()}')
|
||||
if stdout := err.stdout:
|
||||
debug(f'Error calling lsblk: {stdout.decode().rstrip()}')
|
||||
|
||||
if dev_path:
|
||||
raise DiskError(f'Failed to read disk "{dev_path}" with lsblk')
|
||||
|
||||
raise err
|
||||
|
||||
output = worker.output(remove_cr=False)
|
||||
return LsblkOutput.model_validate_json(output)
|
||||
return LsblkOutput.model_validate_json(result.stdout)
|
||||
|
||||
|
||||
def get_lsblk_info(
|
||||
|
|
|
|||
|
|
@ -660,16 +660,12 @@ class Installer:
|
|||
# in the first column of the entry; check for both cases.
|
||||
entry_re = re.compile(rf'#{lang}(\.{encoding})?{modifier} {encoding}')
|
||||
|
||||
lang_value = None
|
||||
for index, line in enumerate(locale_gen_lines):
|
||||
if entry_re.match(line):
|
||||
uncommented_line = line.removeprefix('#')
|
||||
locale_gen_lines[index] = uncommented_line
|
||||
locale_gen_lines[index] = line.removeprefix('#')
|
||||
locale_gen.write_text(''.join(locale_gen_lines))
|
||||
lang_value = uncommented_line.split()[0]
|
||||
break
|
||||
|
||||
if lang_value is None:
|
||||
else:
|
||||
error(f"Invalid locale: language '{locale_config.sys_lang}', encoding '{locale_config.sys_enc}'")
|
||||
return False
|
||||
|
||||
|
|
@ -679,7 +675,7 @@ class Installer:
|
|||
error(f'Failed to run locale-gen on target: {e}')
|
||||
return False
|
||||
|
||||
(self.target / 'etc/locale.conf').write_text(f'LANG={lang_value}\n')
|
||||
(self.target / 'etc/locale.conf').write_text(f'LANG={lang}.{encoding}{modifier}\n')
|
||||
return True
|
||||
|
||||
def set_timezone(self, zone: str) -> bool:
|
||||
|
|
@ -1480,20 +1476,20 @@ class Installer:
|
|||
|
||||
parent_dev_path = get_parent_device_path(efi_partition.safe_dev_path)
|
||||
|
||||
efi_dir_path = self.target / efi_partition.mountpoint.relative_to('/') / 'EFI'
|
||||
efi_dir_path_target = efi_partition.mountpoint / 'EFI'
|
||||
if bootloader_removable:
|
||||
efi_dir_path = efi_dir_path / 'BOOT'
|
||||
efi_dir_path_target = efi_dir_path_target / 'BOOT'
|
||||
else:
|
||||
efi_dir_path = efi_dir_path / 'arch-limine'
|
||||
efi_dir_path_target = efi_dir_path_target / 'arch-limine'
|
||||
|
||||
config_path = efi_dir_path / 'limine.conf'
|
||||
|
||||
efi_dir_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
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'):
|
||||
(limine_path / file).copy_into(efi_dir_path)
|
||||
except Exception as err:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class WifiHandler(InstanceRunnable[bool]):
|
|||
async def _enable_supplicant(self, wifi_iface: str) -> bool:
|
||||
self._wpa_config.load_config()
|
||||
|
||||
result = self._wpa_cli('status') # if it it's running it will blow up
|
||||
result = self._wpa_cli('status')
|
||||
|
||||
if result.success:
|
||||
debug('wpa_supplicant already running')
|
||||
|
|
@ -72,18 +72,19 @@ class WifiHandler(InstanceRunnable[bool]):
|
|||
|
||||
try:
|
||||
SysCommand(f'wpa_supplicant -B -i {wifi_iface} -c {self._wpa_config.config_file}')
|
||||
result = self._wpa_cli('status') # if it it's running it will blow up
|
||||
|
||||
if result.success:
|
||||
debug('successfully enabled wpa_supplicant')
|
||||
return True
|
||||
else:
|
||||
debug(f'failed to enable wpa_supplicant: {result.error}')
|
||||
return False
|
||||
except SysCallError as err:
|
||||
debug(f'failed to enable wpa_supplicant: {err}')
|
||||
return False
|
||||
|
||||
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:
|
||||
net_path = Path('/sys/class/net')
|
||||
|
||||
|
|
@ -197,15 +198,6 @@ class WifiHandler(InstanceRunnable[bool]):
|
|||
|
||||
try:
|
||||
result = SysCommand(cmd).decode()
|
||||
|
||||
if 'FAIL' in result:
|
||||
debug(f'wpa_cli returned FAIL: {result}')
|
||||
return WpaCliResult(
|
||||
success=False,
|
||||
error=f'wpa_cli returned a failure: {result}',
|
||||
)
|
||||
|
||||
return WpaCliResult(success=True, response=result)
|
||||
except SysCallError as err:
|
||||
debug(f'error running wpa_cli command: {err}')
|
||||
return WpaCliResult(
|
||||
|
|
@ -213,6 +205,15 @@ class WifiHandler(InstanceRunnable[bool]):
|
|||
error=f'Error running wpa_cli command: {err}',
|
||||
)
|
||||
|
||||
if 'FAIL' in result:
|
||||
debug(f'wpa_cli returned FAIL: {result}')
|
||||
return WpaCliResult(
|
||||
success=False,
|
||||
error=f'wpa_cli returned a failure: {result}',
|
||||
)
|
||||
|
||||
return WpaCliResult(success=True, response=result)
|
||||
|
||||
def _find_network_id(self, ssid: str, iface: str) -> int | None:
|
||||
result = self._wpa_cli('list_networks', iface)
|
||||
|
||||
|
|
@ -254,18 +255,18 @@ class WifiHandler(InstanceRunnable[bool]):
|
|||
|
||||
try:
|
||||
result = self._wpa_cli('scan_results', iface)
|
||||
|
||||
if not result.success:
|
||||
debug(f'Failed to retrieve scan results: {result.error}')
|
||||
return []
|
||||
|
||||
if not result.response:
|
||||
debug('No wifi networks found')
|
||||
return []
|
||||
|
||||
networks = WifiNetwork.from_wpa(result.response)
|
||||
|
||||
return networks
|
||||
except SysCallError as err:
|
||||
debug('Unable to retrieve wifi results')
|
||||
raise err
|
||||
|
||||
if not result.success:
|
||||
debug(f'Failed to retrieve scan results: {result.error}')
|
||||
return []
|
||||
|
||||
if not result.response:
|
||||
debug('No wifi networks found')
|
||||
return []
|
||||
|
||||
networks = WifiNetwork.from_wpa(result.response)
|
||||
|
||||
return networks
|
||||
|
|
|
|||
|
|
@ -258,6 +258,10 @@ class ProfileHandler:
|
|||
"""
|
||||
try:
|
||||
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')
|
||||
|
||||
with NamedTemporaryFile(delete=False, suffix='.py') as fp:
|
||||
|
|
@ -266,10 +270,6 @@ class ProfileHandler:
|
|||
|
||||
profiles = self._process_profile_file(filepath)
|
||||
self.remove_custom_profiles(profiles)
|
||||
self.add_custom_profiles(profiles)
|
||||
except ValueError:
|
||||
err = tr('Unable to fetch profile from specified url: {}').format(url)
|
||||
error(err)
|
||||
|
||||
def _load_profile_class(self, module: ModuleType) -> list[Profile]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -82,13 +82,14 @@ class TranslationHandler:
|
|||
if not running_from_iso():
|
||||
return
|
||||
|
||||
font_fd, font_path = tempfile.mkstemp(prefix='archinstall_font_')
|
||||
cmap_fd, cmap_path = tempfile.mkstemp(prefix='archinstall_cmap_')
|
||||
os.close(font_fd)
|
||||
os.close(cmap_fd)
|
||||
self._font_backup = Path(font_path)
|
||||
self._cmap_backup = Path(cmap_path)
|
||||
|
||||
try:
|
||||
font_fd, font_path = tempfile.mkstemp(prefix='archinstall_font_')
|
||||
cmap_fd, cmap_path = tempfile.mkstemp(prefix='archinstall_cmap_')
|
||||
os.close(font_fd)
|
||||
os.close(cmap_fd)
|
||||
self._font_backup = Path(font_path)
|
||||
self._cmap_backup = Path(cmap_path)
|
||||
SysCommand(['setfont', '-O', str(self._font_backup), '-om', str(self._cmap_backup)])
|
||||
except SysCallError as err:
|
||||
debug(f'Failed to save console font: {err}')
|
||||
|
|
@ -136,21 +137,21 @@ class TranslationHandler:
|
|||
try:
|
||||
# get a translation for a specific language
|
||||
translation = gettext.translation('base', localedir=self._get_locales_dir(), languages=(abbr, lang))
|
||||
|
||||
# calculate the percentage of total translated text to total number of messages
|
||||
if abbr == 'en':
|
||||
percent = 100
|
||||
else:
|
||||
num_translations = self._get_catalog_size(translation)
|
||||
percent = int((num_translations / self._total_messages) * 100)
|
||||
# prevent cases where the .pot file is out of date and the percentage is above 100
|
||||
percent = min(100, percent)
|
||||
|
||||
language = Language(abbr, lang, translation, percent, translated_lang, console_font)
|
||||
languages.append(language)
|
||||
except FileNotFoundError as err:
|
||||
raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}")
|
||||
|
||||
# calculate the percentage of total translated text to total number of messages
|
||||
if abbr == 'en':
|
||||
percent = 100
|
||||
else:
|
||||
num_translations = self._get_catalog_size(translation)
|
||||
percent = int((num_translations / self._total_messages) * 100)
|
||||
# prevent cases where the .pot file is out of date and the percentage is above 100
|
||||
percent = min(100, percent)
|
||||
|
||||
language = Language(abbr, lang, translation, percent, translated_lang, console_font)
|
||||
languages.append(language)
|
||||
|
||||
return languages
|
||||
|
||||
def _load_language_mappings(self) -> list[dict[str, str]]:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ if TYPE_CHECKING:
|
|||
from _typeshed import DataclassInstance
|
||||
|
||||
|
||||
def _sentence_case(text: str) -> str:
|
||||
# Only capitalize the first letter of the label. The source strings and
|
||||
# their translations are already written with the correct casing for each
|
||||
# language, so title-casing every word is wrong outside English (it turned
|
||||
# "Ім'я хоста" into "Ім'Я Хоста" and "(NTP)" into "(Ntp)").
|
||||
return text[:1].upper() + text[1:]
|
||||
|
||||
|
||||
def as_key_value_pair(
|
||||
entries: dict[str, str | list[str] | bool],
|
||||
ignore_empty: bool = True,
|
||||
|
|
@ -33,28 +41,11 @@ def as_key_value_pair(
|
|||
if isinstance(value, list):
|
||||
value = '\n '.join(str(val) for val in value)
|
||||
|
||||
table.add_row(label.title(), f': {value}')
|
||||
table.add_row(_sentence_case(label), f': {value}')
|
||||
|
||||
return table.stringify()
|
||||
|
||||
|
||||
def as_columns(entries: list[str], cols: int) -> str:
|
||||
"""
|
||||
Will format a list into a given number of columns
|
||||
"""
|
||||
chunks: list[list[str]] = []
|
||||
output = ''
|
||||
|
||||
for i in range(0, len(entries), cols):
|
||||
chunks.append(entries[i : i + cols])
|
||||
|
||||
for row in chunks:
|
||||
out_fmt = '{: <30} ' * len(row)
|
||||
output += out_fmt.format(*row) + '\n'
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _get_values(
|
||||
o: DataclassInstance,
|
||||
class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import secrets
|
||||
import string
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
|
||||
from archinstall.lib.utils.format import as_columns
|
||||
|
||||
|
||||
def timestamp() -> str:
|
||||
|
|
@ -25,29 +23,3 @@ def running_from_iso() -> bool:
|
|||
def generate_password(length: int = 64) -> str:
|
||||
haystack = string.printable # digits, ascii_letters, punctuation (!"#$[] etc) and whitespace
|
||||
return ''.join(secrets.choice(haystack) for _ in range(length))
|
||||
|
||||
|
||||
def format_cols(items: list[str], header: str | None = None) -> str:
|
||||
if header:
|
||||
text = f'{header}:\n'
|
||||
else:
|
||||
text = ''
|
||||
|
||||
nr_items = len(items)
|
||||
if nr_items <= 4:
|
||||
col = 1
|
||||
elif nr_items <= 8:
|
||||
col = 2
|
||||
elif nr_items <= 12:
|
||||
col = 3
|
||||
else:
|
||||
col = 4
|
||||
|
||||
text += as_columns(items, col)
|
||||
# remove whitespaces on each row
|
||||
text = '\n'.join(t.strip() for t in text.split('\n'))
|
||||
return text
|
||||
|
||||
|
||||
def is_valid_path(path: Path) -> bool:
|
||||
return path.exists() and path.is_dir()
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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 ""
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -221,12 +221,27 @@ msgstr "Imágenes del núcleo unificadas"
|
|||
msgid "Install to removable location"
|
||||
msgstr "Instalar a ubicación desmontable"
|
||||
|
||||
msgid "Plymouth"
|
||||
msgstr "Plymouth"
|
||||
|
||||
msgid "Will install to /EFI/BOOT/ (removable location, safe default)"
|
||||
msgstr "Instalará a /EFI/BOOT/ (ubicación desmontable, segura por defecto)"
|
||||
|
||||
msgid "Will install to custom location with NVRAM entry"
|
||||
msgstr "Instalará a ubicación personalizada con entrada NVRAM"
|
||||
|
||||
msgid "Plymouth adds a cosmetic boot splash but can cause boot problems on some setups:"
|
||||
msgstr "Plymouth añade una pantalla de inicio cosmética, pero puede causar problemas de arranque en algunas configuraciones:"
|
||||
|
||||
msgid "black screen with the NVIDIA driver"
|
||||
msgstr "pantalla negra con el controlador NVIDIA"
|
||||
|
||||
msgid "hidden password prompt with disk encryption (LUKS)"
|
||||
msgstr "solicitud de contraseña oculta con cifrado de disco (LUKS)"
|
||||
|
||||
msgid "Would you like to enable it?"
|
||||
msgstr "¿Desea habilitarlo?"
|
||||
|
||||
msgid "Would you like to use unified kernel images?"
|
||||
msgstr "¿Le gustaría utilizar imágenes del núcleo unificadas?"
|
||||
|
||||
|
|
@ -254,6 +269,9 @@ msgstr "Seleccione el gestor de arranque que desea instalar"
|
|||
msgid "UEFI is not detected and some options are disabled"
|
||||
msgstr "No se detecta UEFI y algunas opciones están desactivadas"
|
||||
|
||||
msgid "Select Plymouth theme"
|
||||
msgstr "Seleccionar tema Plymouth"
|
||||
|
||||
msgid "The specified configuration will be applied"
|
||||
msgstr "Se aplicará la configuración especificada"
|
||||
|
||||
|
|
@ -263,9 +281,6 @@ msgstr "¿Quiere continuar?"
|
|||
msgid "Configuration preview"
|
||||
msgstr "Vista previa de la configuración"
|
||||
|
||||
msgid "Warning: no network configuration selected. Network will need to be set up manually on the installed system."
|
||||
msgstr "Advertencia: no se ha seleccionado ninguna configuración de red. La red deberá configurarse manualmente en el sistema instalado."
|
||||
|
||||
msgid "No configuration"
|
||||
msgstr "Sin configuración"
|
||||
|
||||
|
|
@ -332,6 +347,9 @@ msgstr "Contraseña"
|
|||
msgid "Iteration time"
|
||||
msgstr "Tiempo de iteración"
|
||||
|
||||
msgid "No disks were detected. A disk is required to be able to install Arch Linux"
|
||||
msgstr "No se detectaron discos. Se requiere un disco para poder instalar Arch Linux"
|
||||
|
||||
msgid "Select disks for the installation"
|
||||
msgstr "Seleccione discos para la instalación"
|
||||
|
||||
|
|
@ -627,16 +645,21 @@ msgstr "Color"
|
|||
msgid "Kernel"
|
||||
msgstr "Núcleo"
|
||||
|
||||
msgid "Missing configurations:\n"
|
||||
msgstr "Configuraciones que faltan:\n"
|
||||
msgid "No network configuration selected. Network will need to be set up manually on the installed system."
|
||||
msgstr "No se ha seleccionado ninguna configuración de red. La red deberá configurarse manualmente en el sistema instalado."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Invalid configuration: {}"
|
||||
msgstr "Configuración no válida: {}"
|
||||
msgid "Missing configurations:"
|
||||
msgstr "Configuraciones faltantes:"
|
||||
|
||||
msgid "Invalid configuration:"
|
||||
msgstr "Configuración no válida:"
|
||||
|
||||
msgid "Ready to install"
|
||||
msgstr "Listo para instalar"
|
||||
|
||||
msgid "Warnings:"
|
||||
msgstr "Advertencias:"
|
||||
|
||||
msgid "Profiles"
|
||||
msgstr "Perfiles"
|
||||
|
||||
|
|
@ -858,6 +881,10 @@ msgstr "UKI activado"
|
|||
msgid "Removable"
|
||||
msgstr "Desmontable"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Plymouth \"{}\""
|
||||
msgstr "Plymouth «{}»"
|
||||
|
||||
msgid "Use a best-effort default partition layout"
|
||||
msgstr "Utlizar un diseño de partición predeterminado de mejor esfuerzo"
|
||||
|
||||
|
|
@ -965,8 +992,8 @@ msgstr "Grupo de paquetes:"
|
|||
msgid "Color enabled"
|
||||
msgstr "Color activado"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{} grphics driver"
|
||||
#, fuzzy, python-brace-format
|
||||
msgid "{} graphics driver"
|
||||
msgstr "{} controlador de gráficos"
|
||||
|
||||
#, python-brace-format
|
||||
|
|
@ -1032,15 +1059,15 @@ msgstr "¿Desea conectarse al Wi-Fi?"
|
|||
msgid "No wifi interface found"
|
||||
msgstr "No se encontró ninguna interfaz Wi-Fi"
|
||||
|
||||
msgid "No wifi networks found"
|
||||
msgstr "No se encontró redes Wi-Fi"
|
||||
|
||||
msgid "Select wifi network to connect to"
|
||||
msgstr "Seleccione una red Wi-Fi para conectarse"
|
||||
|
||||
msgid "Scanning wifi networks..."
|
||||
msgstr "Buscando redes Wi-Fi..."
|
||||
|
||||
msgid "No wifi networks found"
|
||||
msgstr "No se encontró redes Wi-Fi"
|
||||
|
||||
msgid "Failed setting up wifi"
|
||||
msgstr "Error al configurar el Wi-Fi"
|
||||
|
||||
|
|
@ -1608,9 +1635,6 @@ msgstr " (predeterminado)"
|
|||
#~ msgid "Subvolume name :"
|
||||
#~ msgstr "Nombre del subvolumen :"
|
||||
|
||||
#~ msgid "Select a mount point :"
|
||||
#~ msgstr "Seleccione un punto de montaje :"
|
||||
|
||||
#~ msgid "Select the desired subvolume options "
|
||||
#~ msgstr "Seleccione las opciones de subvolumen deseadas "
|
||||
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -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
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -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
|
|
@ -20,9 +20,9 @@ classifiers = [
|
|||
dependencies = [
|
||||
"pyparted==3.13.0",
|
||||
"pydantic==2.13.4",
|
||||
"cryptography==48.0.1",
|
||||
"textual==8.2.7",
|
||||
"markdown-it-py==4.0.0",
|
||||
"cryptography==49.0.0",
|
||||
"textual==8.2.8",
|
||||
"markdown-it-py==4.2.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",
|
||||
"pre-commit==4.6.1",
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue