Full mypy compliance and small fixes (#1777)

* Fix mypy compliance

---------

Co-authored-by: Daniel Girtler <girtler.daniel@gmail.com>
This commit is contained in:
Daniel Girtler 2023-05-04 00:36:46 +10:00 committed by GitHub
parent e78ddb03e1
commit ec4ecbcb7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 453 additions and 428 deletions

View File

@ -15,4 +15,4 @@ jobs:
# one day this will be enabled
# run: mypy --strict --module archinstall || exit 0
- name: run mypy
run: mypy --config-file mypy.ini
run: mypy --config-file pyproject.toml

View File

@ -233,7 +233,8 @@ def post_process_arguments(arguments):
log(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!", fg="red", level=logging.WARNING)
if arguments.get('plugin', None):
load_plugin(arguments['plugin'])
path = arguments['plugin']
load_plugin(path)
load_config()

View File

@ -269,13 +269,13 @@ class DeviceHandler(object):
# partition will be encrypted
if enc_conf is not None and part_mod in enc_conf.partitions:
self._perform_enc_formatting(
part_mod.real_dev_path,
part_mod.safe_dev_path,
part_mod.mapper_name,
part_mod.fs_type,
enc_conf
)
else:
self._perform_formatting(part_mod.fs_type, part_mod.real_dev_path)
self._perform_formatting(part_mod.fs_type, part_mod.safe_dev_path)
def _perform_partitioning(
self,
@ -287,11 +287,11 @@ class DeviceHandler(object):
# when we require a delete and the partition to be (re)created
# already exists then we have to delete it first
if requires_delete and part_mod.status in [ModificationStatus.Modify, ModificationStatus.Delete]:
log(f'Delete existing partition: {part_mod.real_dev_path}', level=logging.INFO)
part_info = self.find_partition(part_mod.real_dev_path)
log(f'Delete existing partition: {part_mod.safe_dev_path}', level=logging.INFO)
part_info = self.find_partition(part_mod.safe_dev_path)
if not part_info:
raise DiskError(f'No partition for dev path found: {part_mod.real_dev_path}')
raise DiskError(f'No partition for dev path found: {part_mod.safe_dev_path}')
disk.deletePartition(part_info.partition)
disk.commit()
@ -375,7 +375,7 @@ class DeviceHandler(object):
part_mod: PartitionModification,
enc_conf: Optional['DiskEncryption'] = None
):
log(f'Creating subvolumes: {part_mod.real_dev_path}', level=logging.INFO)
log(f'Creating subvolumes: {part_mod.safe_dev_path}', level=logging.INFO)
luks_handler = None
@ -385,7 +385,7 @@ class DeviceHandler(object):
raise ValueError('No device path specified for modification')
luks_handler = self.unlock_luks2_dev(
part_mod.real_dev_path,
part_mod.safe_dev_path,
part_mod.mapper_name,
enc_conf.encryption_password
)
@ -395,7 +395,7 @@ class DeviceHandler(object):
self.mount(luks_handler.mapper_dev, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
else:
self.mount(part_mod.real_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
self.mount(part_mod.safe_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
for sub_vol in part_mod.btrfs_subvols:
log(f'Creating subvolume: {sub_vol.name}', level=logging.DEBUG)
@ -419,7 +419,7 @@ class DeviceHandler(object):
self.umount(luks_handler.mapper_dev)
luks_handler.lock()
else:
self.umount(part_mod.real_dev_path)
self.umount(part_mod.safe_dev_path)
def unlock_luks2_dev(self, dev_path: Path, mapper_name: str, enc_password: str) -> Luks2:
luks_handler = Luks2(dev_path, mapper_name=mapper_name, password=enc_password)

View File

@ -603,7 +603,7 @@ class PartitionModification:
return ''
@property
def real_dev_path(self) -> Path:
def safe_dev_path(self) -> Path:
if self.dev_path is None:
raise ValueError('Device path was not set')
return self.dev_path

View File

@ -2,7 +2,8 @@ from __future__ import annotations
import getpass
import logging
from typing import List
from pathlib import Path
from typing import List, Optional
from .device_model import PartitionModification, Fido2Device
from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes
@ -36,12 +37,12 @@ class Fido2:
# to prevent continous reloading which will slow
# down moving the cursor in the menu
if not cls._loaded or reload:
ret = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8')
ret: Optional[str] = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8')
if not ret:
log('Unable to retrieve fido2 devices', level=logging.ERROR)
return []
fido_devices = clear_vt100_escape_codes(ret)
fido_devices: str = clear_vt100_escape_codes(ret) # type: ignore
manufacturer_pos = 0
product_pos = 0
@ -58,7 +59,7 @@ class Fido2:
product = line[product_pos:]
devices.append(
Fido2Device(path, manufacturer, product)
Fido2Device(Path(path), manufacturer, product)
)
cls._loaded = True

View File

@ -19,9 +19,15 @@ import pathlib
from datetime import datetime, date
from typing import Callable, Optional, Dict, Any, List, Union, Iterator, TYPE_CHECKING
from .exceptions import RequirementError, SysCallError
from .output import log
from .storage import storage
if TYPE_CHECKING:
from .installer import Installer
if sys.platform == 'linux':
from select import epoll, EPOLLIN, EPOLLHUP
else:
@ -53,30 +59,15 @@ else:
except OSError:
return []
from .exceptions import RequirementError, SysCallError
from .output import log
from .storage import storage
def gen_uid(entropy_length :int = 256) -> str:
return hashlib.sha512(os.urandom(entropy_length)).hexdigest()
def generate_password(length :int = 64) -> str:
haystack = string.printable # digits, ascii_letters, punctiation (!"#$[] etc) and whitespace
return ''.join(secrets.choice(haystack) for i in range(length))
def multisplit(s :str, splitters :List[str]) -> str:
s = [s, ]
for key in splitters:
ns = []
for obj in s:
x = obj.split(key)
for index, part in enumerate(x):
if len(part):
ns.append(part)
if index < len(x) - 1:
ns.append(key)
s = ns
return s
def locate_binary(name :str) -> str:
for PATH in os.environ['PATH'].split(':'):
@ -88,20 +79,20 @@ def locate_binary(name :str) -> str:
raise RequirementError(f"Binary {name} does not exist.")
def clear_vt100_escape_codes(data :Union[bytes, str]):
def clear_vt100_escape_codes(data :Union[bytes, str]) -> Union[bytes, str]:
# https://stackoverflow.com/a/43627833/929999
if type(data) == bytes:
vt100_escape_regex = bytes(r'\x1B\[[?0-9;]*[a-zA-Z]', 'UTF-8')
else:
byte_vt100_escape_regex = bytes(r'\x1B\[[?0-9;]*[a-zA-Z]', 'UTF-8')
data = re.sub(byte_vt100_escape_regex, b'', data)
elif type(data) == str:
vt100_escape_regex = r'\x1B\[[?0-9;]*[a-zA-Z]'
for match in re.findall(vt100_escape_regex, data, re.IGNORECASE):
data = data.replace(match, '' if type(data) == str else b'')
data = re.sub(vt100_escape_regex, '', data)
else:
raise ValueError(f'Unsupported data type: {type(data)}')
return data
def json_dumps(*args :str, **kwargs :str) -> str:
return json.dumps(*args, **{**kwargs, 'cls': JSON})
class JsonEncoder:
@staticmethod
@ -245,10 +236,12 @@ class SysCommandWorker:
def __iter__(self, *args :str, **kwargs :Dict[str, Any]) -> Iterator[bytes]:
for line in self._trace_log[self._trace_log_pos:self._trace_log.rfind(b'\n')].split(b'\n'):
if line:
if self.remove_vt100_escape_codes_from_lines:
line = clear_vt100_escape_codes(line)
escaped_line: bytes = line
yield line + b'\n'
if self.remove_vt100_escape_codes_from_lines:
escaped_line = clear_vt100_escape_codes(line) # type: ignore
yield escaped_line + b'\n'
self._trace_log_pos = self._trace_log.rfind(b'\n')
@ -279,7 +272,11 @@ class SysCommandWorker:
log(args[1], level=logging.DEBUG, fg='red')
if self.exit_code != 0:
raise SysCallError(f"{self.cmd} exited with abnormal exit code [{self.exit_code}]: {self._trace_log[-500:]}", self.exit_code, worker=self)
raise SysCallError(
f"{self.cmd} exited with abnormal exit code [{self.exit_code}]: {str(self._trace_log[-500:])}",
self.exit_code,
worker=self
)
def is_alive(self) -> bool:
self.poll()
@ -328,7 +325,7 @@ class SysCommandWorker:
change_perm = True
with peak_logfile.open("a") as peek_output_log:
peek_output_log.write(output)
peek_output_log.write(str(output))
if change_perm:
os.chmod(str(peak_logfile), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
@ -497,7 +494,7 @@ class SysCommand:
clears any printed output if ``.peek_output=True``.
"""
if self.session:
return self.session
return True
with SysCommandWorker(
self.cmd,

View File

@ -2,7 +2,7 @@ import os
import logging
from functools import partial
from pathlib import Path
from typing import Iterator, Optional, Union
from typing import Iterator, Optional, Dict
from .general import SysCommand
from .networking import list_interfaces, enrich_iface_types
@ -61,15 +61,15 @@ AVAILABLE_GFX_DRIVERS = {
"VMware / VirtualBox (open-source)": ["mesa", "xf86-video-vmware"],
}
CPUINFO = Path("/proc/cpuinfo")
MEMINFO = Path("/proc/meminfo")
def cpuinfo() -> Iterator[dict[str, str]]:
"""Yields information about the CPUs of the system."""
cpu = {}
"""
Yields information about the CPUs of the system
"""
cpu_info_path = Path("/proc/cpuinfo")
cpu: Dict[str, str] = {}
with CPUINFO.open() as file:
with cpu_info_path.open() as file:
for line in file:
if not (line := line.strip()):
yield cpu
@ -80,24 +80,31 @@ def cpuinfo() -> Iterator[dict[str, str]]:
cpu[key.strip()] = value.strip()
def meminfo(key: Optional[str] = None) -> Union[dict[str, int], Optional[int]]:
"""Returns a dict with memory info if called with no args
def all_meminfo() -> Dict[str, int]:
"""
Returns a dict with memory info if called with no args
or the value of the given key of said dict.
"""
with MEMINFO.open() as file:
mem_info = {
(columns := line.strip().split())[0].rstrip(':'): int(columns[1])
for line in file
}
mem_info_path = Path("/proc/meminfo")
mem_info: Dict[str, int] = {}
if key is None:
return mem_info
with mem_info_path.open() as file:
for line in file:
key, value = line.strip().split(':')
num = value.split()[0]
mem_info[key] = int(num)
return mem_info.get(key)
return mem_info
def meminfo_for_key(key: str) -> int:
info = all_meminfo()
return info[key]
def has_wifi() -> bool:
return 'WIRELESS' in enrich_iface_types(list_interfaces().values()).values()
ifaces = list(list_interfaces().values())
return 'WIRELESS' in enrich_iface_types(ifaces).values()
def has_cpu_vendor(vendor_id: str) -> bool:
@ -160,15 +167,15 @@ def product_name() -> Optional[str]:
def mem_available() -> Optional[int]:
return meminfo('MemAvailable')
return meminfo_for_key('MemAvailable')
def mem_free() -> Optional[int]:
return meminfo('MemFree')
return meminfo_for_key('MemFree')
def mem_total() -> Optional[int]:
return meminfo('MemTotal')
return meminfo_for_key('MemTotal')
def virtualization() -> Optional[str]:
@ -182,9 +189,9 @@ def virtualization() -> Optional[str]:
def is_vm() -> bool:
try:
return b"none" not in b"".join(SysCommand("systemd-detect-virt")).lower()
result = SysCommand("systemd-detect-virt")
return b"none" not in b"".join(result).lower()
except SysCallError as error:
log(f"System is not running in a VM: {error}", level=logging.DEBUG)
return None
# TODO: Add more identifiers
return False

View File

@ -7,7 +7,7 @@ import shutil
import subprocess
import time
from pathlib import Path
from typing import Any, Iterator, List, Mapping, Optional, TYPE_CHECKING, Union, Dict
from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable, Iterable
from . import disk
from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError
@ -36,32 +36,6 @@ __packages__ = ["base", "base-devel", "linux-firmware", "linux", "linux-lts", "l
__accessibility_packages__ = ["brltty", "espeakup", "alsa-utils"]
class InstallationFile:
def __init__(self, installation :'Installer', filename :str, owner :str, mode :str = "w"):
self.installation = installation
self.filename = filename
self.owner = owner
self.mode = mode
self.fh = None
def __enter__(self) -> 'InstallationFile':
self.fh = open(self.filename, self.mode)
return self
def __exit__(self, *args :str) -> None:
self.fh.close()
self.installation.chown(self.owner, self.filename)
def write(self, data: Union[str, bytes]) -> int:
return self.fh.write(data)
def read(self, *args) -> Union[str, bytes]:
return self.fh.read(*args)
# def poll(self, *args) -> bool:
# return self.fh.poll(*args)
def accessibility_tools_in_use() -> bool:
return os.system('systemctl is-active --quiet espeakup.service') == 0
@ -106,15 +80,17 @@ class Installer:
self.kernels = kernels
self._disk_config = disk_config
self._disk_encryption = disk_encryption
if self._disk_encryption is None:
if disk_encryption is None:
self._disk_encryption = disk.DiskEncryption(disk.EncryptionType.NoEncryption)
else:
self._disk_encryption = disk_encryption
self.target: Path = target
self.target = target
self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S')
self.milliseconds = int(str(time.time()).split('.')[1])
self.helper_flags = {'base': False, 'bootloader': False}
self.helper_flags: Dict[str, Any] = {'base': False, 'bootloader': None}
self.base_packages = base_packages
for kernel in self.kernels:
@ -124,31 +100,33 @@ class Installer:
if accessibility_tools_in_use():
self.base_packages.extend(__accessibility_packages__)
self.post_base_install = []
self.post_base_install: List[Callable] = []
# TODO: Figure out which one of these two we'll use.. But currently we're mixing them..
storage['session'] = self
storage['installation_session'] = self
self.MODULES = []
self.BINARIES = []
self.FILES = []
self.modules: List[str] = []
self._binaries: List[str] = []
self._files: List[str] = []
# systemd, sd-vconsole and sd-encrypt will be replaced by udev, keymap and encrypt
# if HSM is not used to encrypt the root volume. Check mkinitcpio() function for that override.
self.HOOKS = ["base", "systemd", "autodetect", "keyboard", "sd-vconsole", "modconf", "block", "filesystems", "fsck"]
self.KERNEL_PARAMS = []
self.FSTAB_ENTRIES = []
self._hooks: List[str] = [
"base", "systemd", "autodetect", "keyboard",
"sd-vconsole", "modconf", "block", "filesystems", "fsck"
]
self._kernel_params: List[str] = []
self._fstab_entries: List[str] = []
self._zram_enabled = False
def __enter__(self, *args: str, **kwargs: str) -> 'Installer':
def __enter__(self) -> 'Installer':
return self
def __exit__(self, *args :str, **kwargs :str) -> bool:
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if len(args) >= 2 and args[1]:
self.log(args[1], level=logging.ERROR, fg='red')
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
log(exc_val, fg='red', level=logging.ERROR)
self.sync_log_to_install_medium()
@ -156,7 +134,7 @@ class Installer:
# and then reboot, and a identical log file will be found in the ISO medium anyway.
print(_("[!] A log file has been created here: {}").format(os.path.join(storage['LOG_PATH'], storage['LOG_FILE'])))
print(_(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues"))
raise args[1]
raise exc_val
if not (missing_steps := self.post_install_check()):
self.log('Installation completed without any errors. You may now reboot.', fg='green', level=logging.INFO)
@ -164,6 +142,7 @@ class Installer:
return True
else:
self.log('Some required steps were not successfully installed/configured before leaving the installer:', fg='red', level=logging.WARNING)
for step in missing_steps:
self.log(f' - {step}', fg='red', level=logging.WARNING)
@ -247,31 +226,32 @@ class Installer:
luks_handlers = {}
for part_mod in partitions:
luks_handler = disk.device_handler.unlock_luks2_dev(
part_mod.dev_path,
part_mod.mapper_name,
self._disk_encryption.encryption_password
)
luks_handlers[part_mod] = luks_handler
if part_mod.mapper_name and part_mod.dev_path:
luks_handler = disk.device_handler.unlock_luks2_dev(
part_mod.dev_path,
part_mod.mapper_name,
self._disk_encryption.encryption_password
)
luks_handlers[part_mod] = luks_handler
return luks_handlers
def _mount_partition(self, part_mod: disk.PartitionModification):
# it would be none if it's btrfs as the subvolumes will have the mountpoints defined
if part_mod.mountpoint is not None:
if part_mod.mountpoint and part_mod.dev_path:
target = self.target / part_mod.relative_mountpoint
disk.device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options)
if part_mod.fs_type == disk.FilesystemType.Btrfs:
if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.dev_path:
self._mount_btrfs_subvol(part_mod.dev_path, part_mod.btrfs_subvols)
def _mount_luks_partiton(self, part_mod: disk.PartitionModification, luks_handler: Luks2):
# it would be none if it's btrfs as the subvolumes will have the mountpoints defined
if part_mod.mountpoint is not None:
if part_mod.mountpoint and luks_handler.mapper_dev:
target = self.target / part_mod.relative_mountpoint
disk.device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options)
if part_mod.fs_type == disk.FilesystemType.Btrfs:
if part_mod.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev:
self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols)
def _mount_btrfs_subvol(self, dev_path: Path, subvolumes: List[disk.SubvolumeModification]):
@ -346,15 +326,15 @@ class Installer:
SysCommand(f'chmod 0600 {self.target}{file}')
SysCommand(f'mkswap {self.target}{file}')
self.FSTAB_ENTRIES.append(f'{file} none swap defaults 0 0')
self._fstab_entries.append(f'{file} none swap defaults 0 0')
if enable_resume:
resume_uuid = SysCommand(f'findmnt -no UUID -T {self.target}{file}').decode('UTF-8').strip()
resume_offset = SysCommand(f'/usr/bin/filefrag -v {self.target}{file}').decode('UTF-8').split('0:', 1)[1].split(":", 1)[1].split("..", 1)[0].strip()
self.HOOKS.append('resume')
self.KERNEL_PARAMS.append(f'resume=UUID={resume_uuid}')
self.KERNEL_PARAMS.append(f'resume_offset={resume_offset}')
self._hooks.append('resume')
self._kernel_params.append(f'resume=UUID={resume_uuid}')
self._kernel_params.append(f'resume_offset={resume_offset}')
def post_install_check(self, *args :str, **kwargs :str) -> List[str]:
return [step for step, flag in self.helper_flags.items() if flag is False]
@ -411,7 +391,7 @@ class Installer:
else:
pacman_conf.write(line)
def pacstrap(self, *packages: Union[str, List[str]], **kwargs :str) -> bool:
def _pacstrap(self, packages: Union[str, List[str]]) -> bool:
if type(packages[0]) in (list, tuple):
packages = packages[0]
@ -430,9 +410,9 @@ class Installer:
if storage['arguments'].get('silent', False) is False:
if input('Would you like to re-try this download? (Y/n): ').lower().strip() in ('', 'y'):
return self.pacstrap(*packages, **kwargs)
return self._pacstrap(packages)
raise RequirementError(f'Could not sync mirrors: {error}', level=logging.ERROR, fg="red")
raise RequirementError(f'Could not sync mirrors: {error}')
try:
SysCommand(f'/usr/bin/pacstrap -C /etc/pacman.conf -K {self.target} {" ".join(packages)} --noconfirm', peek_output=True)
@ -442,40 +422,44 @@ class Installer:
if storage['arguments'].get('silent', False) is False:
if input('Would you like to re-try this download? (Y/n): ').lower().strip() in ('', 'y'):
return self.pacstrap(*packages, **kwargs)
return self._pacstrap(packages)
raise RequirementError("Pacstrap failed. See /var/log/archinstall/install.log or above message for error details.")
def set_mirrors(self, mirrors :Mapping[str, Iterator[str]]) -> None:
def set_mirrors(self, mirrors: Dict[str, Iterable[str]]):
for plugin in plugins.values():
if hasattr(plugin, 'on_mirrors'):
if result := plugin.on_mirrors(mirrors):
mirrors = result
return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist')
destination = f'{self.target}/etc/pacman.d/mirrorlist'
use_mirrors(mirrors, destination=destination)
def genfstab(self, flags :str = '-pU'):
self.log(f"Updating {self.target}/etc/fstab", level=logging.INFO)
try:
fstab = SysCommand(f'/usr/bin/genfstab {flags} {self.target}')
gen_fstab = SysCommand(f'/usr/bin/genfstab {flags} {self.target}').decode()
except SysCallError as error:
raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {error}')
with open(f"{self.target}/etc/fstab", 'a') as fstab_fh:
fstab_fh.write(fstab.decode())
if not gen_fstab:
raise RequirementError(f'Genrating fstab returned empty value')
with open(f"{self.target}/etc/fstab", 'a') as fp:
fp.write(gen_fstab)
if not os.path.isfile(f'{self.target}/etc/fstab'):
raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {fstab}')
raise RequirementError(f'Could not create fstab file')
for plugin in plugins.values():
if hasattr(plugin, 'on_genfstab'):
if plugin.on_genfstab(self) is True:
break
with open(f"{self.target}/etc/fstab", 'a') as fstab_fh:
for entry in self.FSTAB_ENTRIES:
fstab_fh.write(f'{entry}\n')
with open(f"{self.target}/etc/fstab", 'a') as fp:
for entry in self._fstab_entries:
fp.write(f'{entry}\n')
for mod in self._disk_config.device_modifications:
for part_mod in mod.partitions:
@ -583,7 +567,7 @@ class Installer:
# fstrim is owned by util-linux, a dependency of both base and systemd.
self.enable_service("fstrim.timer")
def enable_service(self, *services: Union[str, List[str]]) -> None:
def enable_service(self, services: Union[str, List[str]]) -> None:
if type(services[0]) in (list, tuple):
services = services[0]
@ -611,19 +595,7 @@ class Installer:
subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True)
def configure_nic(self, network_config: NetworkConfiguration) -> None:
from .systemd import Networkd
if network_config.dhcp:
conf = Networkd(Match={"Name": network_config.iface}, Network={"DHCP": "yes"})
else:
network = {"Address": network_config.ip}
if network_config.gateway:
network["Gateway"] = network_config.gateway
if network_config.dns:
dns = network_config.dns
network["DNS"] = dns if isinstance(dns, list) else [dns]
conf = Networkd(Match={"Name": network_config.iface}, Network=network)
conf = network_config.as_systemd_config()
for plugin in plugins.values():
if hasattr(plugin, 'on_configure_nic'):
@ -663,7 +635,7 @@ class Installer:
# Otherwise, we can go ahead and add the required package
# and enable it's service:
else:
self.pacstrap('iwd')
self._pacstrap('iwd')
self.enable_service('iwd')
for psk in psk_files:
@ -682,12 +654,12 @@ class Installer:
if self.helper_flags.get('base', False) is False:
def post_install_enable_networkd_resolved(*args :str, **kwargs :str):
self.enable_service('systemd-networkd', 'systemd-resolved')
self.enable_service(['systemd-networkd', 'systemd-resolved'])
self.post_base_install.append(post_install_enable_networkd_resolved)
# Otherwise, we can go ahead and enable the services
else:
self.enable_service('systemd-networkd', 'systemd-resolved')
self.enable_service(['systemd-networkd', 'systemd-resolved'])
return True
@ -704,9 +676,9 @@ class Installer:
fh.write(f"KEYMAP={storage['arguments']['keyboard-layout']}\n")
with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit:
mkinit.write(f"MODULES=({' '.join(self.MODULES)})\n")
mkinit.write(f"BINARIES=({' '.join(self.BINARIES)})\n")
mkinit.write(f"FILES=({' '.join(self.FILES)})\n")
mkinit.write(f"MODULES=({' '.join(self.modules)})\n")
mkinit.write(f"BINARIES=({' '.join(self._binaries)})\n")
mkinit.write(f"FILES=({' '.join(self._files)})\n")
if not self._disk_encryption.hsm_device:
# For now, if we don't use HSM we revert to the old
@ -714,9 +686,9 @@ class Installer:
# This is purely for stability reasons, we're going away from this.
# * systemd -> udev
# * sd-vconsole -> keymap
self.HOOKS = [hook.replace('systemd', 'udev').replace('sd-vconsole', 'keymap') for hook in self.HOOKS]
self._hooks = [hook.replace('systemd', 'udev').replace('sd-vconsole', 'keymap') for hook in self._hooks]
mkinit.write(f"HOOKS=({' '.join(self.HOOKS)})\n")
mkinit.write(f"HOOKS=({' '.join(self._hooks)})\n")
try:
SysCommand(f'/usr/bin/arch-chroot {self.target} mkinitcpio {" ".join(flags)}')
@ -736,25 +708,25 @@ class Installer:
if (pkg := part.fs_type.installation_pkg) is not None:
self.base_packages.append(pkg)
if (module := part.fs_type.installation_module) is not None:
self.MODULES.append(module)
self.modules.append(module)
if (binary := part.fs_type.installation_binary) is not None:
self.BINARIES.append(binary)
self._binaries.append(binary)
# There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed.
if part.fs_type.fs_type_mount == 'ntfs3' and part.mountpoint == self.target:
if 'fsck' in self.HOOKS:
self.HOOKS.remove('fsck')
if 'fsck' in self._hooks:
self._hooks.remove('fsck')
if part in self._disk_encryption.partitions:
if self._disk_encryption.hsm_device:
# Required bby mkinitcpio to add support for fido2-device options
self.pacstrap('libfido2')
self._pacstrap('libfido2')
if 'sd-encrypt' not in self.HOOKS:
self.HOOKS.insert(self.HOOKS.index('filesystems'), 'sd-encrypt')
if 'sd-encrypt' not in self._hooks:
self._hooks.insert(self._hooks.index('filesystems'), 'sd-encrypt')
else:
if 'encrypt' not in self.HOOKS:
self.HOOKS.insert(self.HOOKS.index('filesystems'), 'encrypt')
if 'encrypt' not in self._hooks:
self._hooks.insert(self._hooks.index('filesystems'), 'encrypt')
if not has_uefi():
self.base_packages.append('grub')
@ -786,7 +758,7 @@ class Installer:
else:
self.log("The testing flag is not set. This system will be installed without testing repositories enabled.")
self.pacstrap(self.base_packages)
self._pacstrap(self.base_packages)
self.helper_flags['base-strapped'] = True
# This handles making sure that the repositories we enabled persist on the installed system
@ -826,7 +798,7 @@ class Installer:
def setup_swap(self, kind :str = 'zram'):
if kind == 'zram':
self.log(f"Setting up swap on zram")
self.pacstrap('zram-generator')
self._pacstrap('zram-generator')
# We could use the default example below, but maybe not the best idea: https://github.com/archlinux/archinstall/pull/678#issuecomment-962124813
# zram_example_location = '/usr/share/doc/zram-generator/zram-generator.conf.example'
@ -853,7 +825,7 @@ class Installer:
return None
def _add_systemd_bootloader(self, root_partition: disk.PartitionModification):
self.pacstrap('efibootmgr')
self._pacstrap('efibootmgr')
if not has_uefi():
raise HardwareIncompatibilityError
@ -919,7 +891,7 @@ class Installer:
# blkid doesn't trigger on loopback devices really well,
# so we'll use the old manual method until we get that sorted out.
options_entry = f'rw rootfstype={root_partition.fs_type.fs_type_mount} {" ".join(self.KERNEL_PARAMS)}\n'
options_entry = f'rw rootfstype={root_partition.fs_type.fs_type_mount} {" ".join(self._kernel_params)}\n'
for sub_vol in root_partition.btrfs_subvols:
if sub_vol.is_root():
@ -958,7 +930,7 @@ class Installer:
boot_partition: disk.PartitionModification,
root_partition: disk.PartitionModification
):
self.pacstrap('grub') # no need?
self._pacstrap('grub') # no need?
_file = "/etc/default/grub"
@ -977,7 +949,7 @@ class Installer:
log(f"GRUB boot partition: {boot_partition.dev_path}", level=logging.INFO)
if has_uefi():
self.pacstrap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead?
self._pacstrap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead?
try:
SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable', peek_output=True)
@ -987,8 +959,20 @@ class Installer:
except SysCallError as error:
raise DiskError(f"Could not install GRUB to {self.target}/boot: {error}")
else:
device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path)
if not device:
raise ValueError(f'Can not find block device: {boot_partition.safe_dev_path}')
try:
SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=i386-pc --recheck {boot_partition.parent}', peek_output=True)
cmd = f'/usr/bin/arch-chroot' \
f' {self.target}' \
f' grub-install' \
f' --debug' \
f' --target=i386-pc' \
f' --recheck {device.device_info.path}'
SysCommand(cmd, peek_output=True)
except SysCallError as error:
raise DiskError(f"Failed to install GRUB boot on {boot_partition.dev_path}: {error}")
@ -1004,7 +988,7 @@ class Installer:
boot_partition: disk.PartitionModification,
root_partition: disk.PartitionModification
):
self.pacstrap('efibootmgr')
self._pacstrap('efibootmgr')
if not has_uefi():
raise HardwareIncompatibilityError
@ -1038,17 +1022,30 @@ class Installer:
# TODO: We need to detect if the encrypted device is a whole disk encryption,
# or simply a partition encryption. Right now we assume it's a partition (and we always have)
log(f'Identifying root partition by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG)
kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.fs_type.value} {" ".join(self.KERNEL_PARAMS)}')
kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}')
else:
log(f'Root partition is an encrypted device identifying by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG)
kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.fs_type.value} {" ".join(self.KERNEL_PARAMS)}')
kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}')
device = disk.device_handler.get_device_by_partition_path(boot_partition.dev_path)
SysCommand(f'efibootmgr --disk {device.path} --part {device.path} --create --label "{label}" --loader {loader} --unicode \'{" ".join(kernel_parameters)}\' --verbose')
device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path)
if not device:
raise ValueError(f'Unable to find block device: {boot_partition.safe_dev_path}')
cmd = f'efibootmgr ' \
f'--disk {device.device_info.path} ' \
f'--part {boot_partition.safe_dev_path} ' \
f'--create ' \
f'--label "{label}" ' \
f'--loader {loader} ' \
f'--unicode \'{" ".join(kernel_parameters)}\' ' \
f'--verbose'
SysCommand(cmd)
self.helper_flags['bootloader'] = "efistub"
def add_bootloader(self, bootloader: Bootloader) -> bool:
def add_bootloader(self, bootloader: Bootloader):
"""
Adds a bootloader to the installation instance.
Archinstall supports one of three types:
@ -1056,8 +1053,7 @@ class Installer:
* grub
* efistub (beta)
:param bootloader: Can be one of the three strings
'systemd-bootctl', 'grub' or 'efistub' (beta)
:param bootloader: Type of bootloader to be added
"""
for plugin in plugins.values():
@ -1089,8 +1085,8 @@ class Installer:
case Bootloader.Efistub:
self._add_efistub_bootloader(boot_partition, root_partition)
def add_additional_packages(self, *packages: Union[str, List[str]]) -> bool:
return self.pacstrap(*packages)
def add_additional_packages(self, packages: Union[str, List[str]]) -> bool:
return self._pacstrap(packages)
def _enable_users(self, service: str, users: List[User]):
for user in users:
@ -1201,9 +1197,6 @@ class Installer:
except SysCallError:
return False
def create_file(self, filename :str, owner :Optional[str] = None) -> InstallationFile:
return InstallationFile(self, filename, owner)
def set_keyboard_language(self, language: str) -> bool:
log(f"Setting keyboard language to {language}", level=logging.INFO)
if len(language.strip()):

View File

@ -482,9 +482,9 @@ class AbstractMenu:
if item in self._menus_to_enable():
yield item
def _select_archinstall_language(self, preset_value: Language) -> Language:
def _select_archinstall_language(self, preset: Language) -> Language:
from ..user_interaction.general_conf import select_archinstall_language
language = select_archinstall_language(self.translation_handler.translated_languages, preset_value)
language = select_archinstall_language(self.translation_handler.translated_languages, preset)
self._translation_handler.activate(language)
return language

View File

@ -3,7 +3,7 @@ from enum import Enum, auto
from os import system
from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional, Callable
from simple_term_menu import TerminalMenu
from simple_term_menu import TerminalMenu # type: ignore
from ..exceptions import RequirementError
from ..output import log
@ -29,11 +29,11 @@ class MenuSelection:
@property
def single_value(self) -> Any:
return self.value
return self.value # type: ignore
@property
def multi_value(self) -> List[Any]:
return self.value
return self.value # type: ignore
class Menu(TerminalMenu):
@ -67,7 +67,7 @@ class Menu(TerminalMenu):
preview_command: Optional[Callable] = None,
preview_size: float = 0.0,
preview_title: str = 'Info',
header: Union[List[str],str] = None,
header: Union[List[str], str] = [],
allow_reset: bool = False,
allow_reset_warning_msg: Optional[str] = None,
clear_screen: bool = True,
@ -141,8 +141,6 @@ class Menu(TerminalMenu):
log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING)
raise RequirementError("Menu() requires an iterable as option.")
self._default_str = str(_('(default)'))
if isinstance(p_options,dict):
options = list(p_options.keys())
else:
@ -193,8 +191,7 @@ class Menu(TerminalMenu):
if default_option:
# if a default value was specified we move that one
# to the top of the list and mark it as default as well
default = f'{default_option} {self._default_str}'
self._menu_options = [default] + [o for o in self._menu_options if default_option != o]
self._menu_options = [self._default_menu_value] + [o for o in self._menu_options if default_option != o]
if display_back_option and not multi and skip:
skip_empty_entries = True
@ -204,7 +201,18 @@ class Menu(TerminalMenu):
skip_empty_entries = True
self._menu_options += ['']
self._preselection(preset_values,cursor_index)
preset_list: Optional[List[str]] = None
if preset_values and isinstance(preset_values, str):
preset_list = [preset_values]
calc_cursor_idx = self._determine_cursor_pos(preset_list, cursor_index)
# when we're not in multi selection mode we don't care about
# passing the pre-selection list to the menu as the position
# of the cursor is the one determining the pre-selection
if not self._multi:
preset_values = None
cursor = "> "
main_menu_cursor_style = ("fg_cyan", "bold")
@ -217,8 +225,8 @@ class Menu(TerminalMenu):
menu_cursor_style=main_menu_cursor_style,
menu_highlight_style=main_menu_style,
multi_select=multi,
preselected_entries=self.preset_values,
cursor_index=self.cursor_index,
preselected_entries=preset_values,
cursor_index=calc_cursor_idx,
preview_command=lambda x: self._show_preview(preview_command, x),
preview_size=preview_size,
preview_title=preview_title,
@ -231,12 +239,17 @@ class Menu(TerminalMenu):
skip_empty_entries=skip_empty_entries
)
@property
def _default_menu_value(self) -> str:
default_str = str(_('(default)'))
return f'{self._default_option} {default_str}'
def _show_preview(self, preview_command: Optional[Callable], selection: str) -> Optional[str]:
if selection == self.back():
return None
if preview_command:
if self._default_option is not None and f'{self._default_option} {self._default_str}' == selection:
if self._default_option is not None and self._default_menu_value == selection:
selection = self._default_option
return preview_command(selection)
@ -249,7 +262,7 @@ class Menu(TerminalMenu):
return MenuSelection(type_=MenuSelectionType.Reset)
def check_default(elem):
if self._default_option is not None and f'{self._default_option} {self._default_str}' in elem:
if self._default_option is not None and self._default_menu_value in elem:
return self._default_option
else:
return elem
@ -297,31 +310,44 @@ class Menu(TerminalMenu):
pos = self._menu_entries.index(value)
self.set_cursor_pos(pos)
def _preselection(self,preset_values :Union[str, List[str]] = [], cursor_index : Optional[int] = None):
def from_preset_to_cursor():
if preset_values:
# if the value is not extant return 0 as cursor index
def _determine_cursor_pos(
self,
preset: Optional[List[str]] = None,
cursor_index: Optional[int] = None
) -> Optional[int]:
"""
The priority order to determine the cursor position is:
1. A static cursor position was provided
2. Preset values have been provided so the cursor will be
positioned on those
3. A default value for a selection is given so the cursor
will be placed on such
"""
if cursor_index:
return cursor_index
if preset:
indexes = []
for p in preset:
try:
if isinstance(preset_values,str):
self.cursor_index = self._menu_options.index(self.preset_values)
else: # should return an error, but this is smoother
self.cursor_index = self._menu_options.index(self.preset_values[0])
except ValueError:
self.cursor_index = 0
# the options of the table selection menu
# are already escaped so we have to escape
# the preset values as well for the comparison
if '|' in p:
p = p.replace('|', '\\|')
self.cursor_index = cursor_index
if not preset_values:
self.preset_values = None
return
idx = self._menu_options.index(p)
indexes.append(idx)
except (IndexError, ValueError):
log(f'Error finding index of {p}: {self._menu_options}', level=logging.DEBUG)
if len(indexes) == 0:
indexes.append(0)
return indexes[0]
self.preset_values = preset_values
if self._default_option:
if isinstance(preset_values,str) and self._default_option == preset_values:
self.preset_values = f"{preset_values} {self._default_str}"
elif isinstance(preset_values,(list,tuple)) and self._default_option in preset_values:
idx = preset_values.index(self._default_option)
self.preset_values[idx] = f"{preset_values[idx]} {self._default_str}"
if cursor_index is None or not self._multi:
from_preset_to_cursor()
if not self._multi: # Not supported by the infraestructure
self.preset_values = None
return self._menu_options.index(self._default_menu_value)
return None

View File

@ -3,12 +3,22 @@ import pathlib
import urllib.error
import urllib.request
from typing import Union, Iterable, Dict, Any, List
from dataclasses import dataclass
from .general import SysCommand
from .output import log
from .storage import storage
def sort_mirrorlist(raw_data :bytes, sort_order=["https", "http"]) -> bytes:
@dataclass
class CustomMirror:
url: str
signcheck: str
signoptions: str
name: str
def sort_mirrorlist(raw_data :bytes, sort_order: List[str] = ['https', 'http']) -> bytes:
"""
This function can sort /etc/pacman.d/mirrorlist according to the
mirror's URL prefix. By default places HTTPS before HTTP but it also
@ -28,8 +38,9 @@ def sort_mirrorlist(raw_data :bytes, sort_order=["https", "http"]) -> bytes:
from server url definitions (commented or uncommented).
"""
comments_and_whitespaces = b""
sort_order += ['Unknown']
categories: Dict[str, List] = {key: [] for key in sort_order}
categories = {key: [] for key in sort_order + ["Unknown"]}
for line in raw_data.split(b"\n"):
if line[0:2] in (b'##', b''):
comments_and_whitespaces += line + b'\n'
@ -82,18 +93,18 @@ def filter_mirrors_by_region(regions :str,
return new_list.decode('UTF-8')
def add_custom_mirrors(mirrors: List[str], *args :str, **kwargs :str) -> bool:
def add_custom_mirrors(mirrors: List[CustomMirror]) -> bool:
"""
This will append custom mirror definitions in pacman.conf
:param mirrors: A list of mirror data according to: `{'url': 'http://url.com', 'signcheck': 'Optional', 'signoptions': 'TrustAll', 'name': 'testmirror'}`
:type mirrors: dict
:param mirrors: A list of custom mirrors
:type mirrors: List[CustomMirror]
"""
with open('/etc/pacman.conf', 'a') as pacman:
for mirror in mirrors:
pacman.write(f"[{mirror['name']}]\n")
pacman.write(f"SigLevel = {mirror['signcheck']} {mirror['signoptions']}\n")
pacman.write(f"Server = {mirror['url']}\n")
pacman.write(f"[{mirror.name}]\n")
pacman.write(f"SigLevel = {mirror.signcheck} {mirror.signoptions}\n")
pacman.write(f"Server = {mirror.url}\n")
return True
@ -123,7 +134,7 @@ def insert_mirrors(mirrors :Dict[str, Any], *args :str, **kwargs :str) -> bool:
def use_mirrors(
regions: Dict[str, Iterable[str]],
destination: str = '/etc/pacman.d/mirrorlist'
) -> None:
):
log(f'A new package mirror-list has been created: {destination}', level=logging.INFO)
with open(destination, 'w') as mirrorlist:
for region, mirrors in regions.items():
@ -146,7 +157,7 @@ def re_rank_mirrors(
def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]:
regions = {}
regions: Dict[str, Dict[str, Any]] = {}
if storage['arguments']['offline']:
with pathlib.Path('/etc/pacman.d/mirrorlist').open('rb') as fh:
@ -170,18 +181,19 @@ def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]:
if len(line.strip()) == 0:
continue
line = line.decode('UTF-8').strip('\n').strip('\r')
if line[:3] == '## ':
region = line[3:]
elif line[:10] == '#Server = ':
clean_line = line.decode('UTF-8').strip('\n').strip('\r')
if clean_line[:3] == '## ':
region = clean_line[3:]
elif clean_line[:10] == '#Server = ':
regions.setdefault(region, {})
url = line.lstrip('#Server = ')
url = clean_line.lstrip('#Server = ')
regions[region][url] = True
elif line.startswith('Server = '):
elif clean_line.startswith('Server = '):
regions.setdefault(region, {})
url = line.lstrip('Server = ')
url = clean_line.lstrip('Server = ')
regions[region][url] = True
return regions

View File

@ -1,8 +1,9 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING
from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING, Tuple
from ..output import log
from ..storage import storage
@ -24,7 +25,7 @@ class NetworkConfiguration:
ip: Optional[str] = None
dhcp: bool = True
gateway: Optional[str] = None
dns: Union[None, List[str]] = None
dns: List[str] = field(default_factory=list)
def __str__(self):
if self.is_iso():
@ -53,6 +54,33 @@ class NetworkConfiguration:
return data
def as_systemd_config(self) -> str:
match: List[Tuple[str, str]] = []
network: List[Tuple[str, str]] = []
if self.iface:
match.append(('Name', self.iface))
if self.dhcp:
network.append(('DHCP', 'yes'))
else:
if self.ip:
network.append(('Address', self.ip))
if self.gateway:
network.append(('Gateway', self.gateway))
for dns in self.dns:
network.append(('DNS', dns))
config = {'Match': match, 'Network': network}
config_str = ''
for top, entries in config.items():
config_str += f'[{top}]\n'
config_str += '\n'.join([f'{k}={v}' for k, v in entries])
config_str += '\n\n'
return config_str
def json(self) -> Dict:
# for json serialization when calling json.dumps(...) on this class
return self.__dict__
@ -90,41 +118,14 @@ class NetworkConfigurationHandler:
# Perform a copy of the config
if self._configuration.is_iso():
installation.copy_iso_network_config(
enable_services=True) # Sources the ISO network configuration to the install medium.
enable_services=True # Sources the ISO network configuration to the install medium.
)
elif self._configuration.is_network_manager():
installation.add_additional_packages(["networkmanager"])
if (profile := storage['arguments'].get('profile_config')) and profile.is_desktop_type_profile:
installation.add_additional_packages(["network-manager-applet"])
installation.enable_service('NetworkManager.service')
def _backwards_compability_config(self, config: Union[str,Dict[str, str]]) -> Union[List[NetworkConfiguration], NetworkConfiguration, None]:
def get(config: Dict[str, str], key: str) -> List[str]:
if (value := config.get(key, None)) is not None:
return [value]
return []
if isinstance(config, str): # is a ISO network
return NetworkConfiguration(NicType.ISO)
elif config.get('NetworkManager'): # is a network manager configuration
return NetworkConfiguration(NicType.NM)
elif 'ip' in config:
return [NetworkConfiguration(
NicType.MANUAL,
iface=config.get('nic', ''),
ip=config.get('ip'),
gateway=config.get('gateway', ''),
dns=get(config, 'dns'),
dhcp=False
)]
elif 'nic' in config:
return [NetworkConfiguration(
NicType.MANUAL,
iface=config.get('nic', ''),
dhcp=True
)]
else: # not recognized
return None
def _parse_manual_config(self, configs: List[Dict[str, Any]]) -> Optional[List[NetworkConfiguration]]:
configurations = []
@ -145,13 +146,17 @@ class NetworkConfigurationHandler:
log(_('Manual nic configuration with no auto DHCP requires an IP address'), fg='red')
exit(1)
dns = manual_config.get('dns', [])
if not isinstance(dns, list):
dns = [dns]
configurations.append(
NetworkConfiguration(
NicType.MANUAL,
iface=iface,
ip=ip,
gateway=manual_config.get('gateway', ''),
dns=manual_config.get('dns', []),
dns=dns,
dhcp=False
)
)
@ -176,8 +181,5 @@ class NetworkConfigurationHandler:
self._configuration = NetworkConfiguration(type_)
else: # manual configuration settings
self._configuration = self._parse_manual_config([config])
else: # old style definitions
network_config = self._backwards_compability_config(config)
if network_config:
return network_config
return None
else:
log(f'Unable to parse network configuration: {config}', level=logging.DEBUG)

View File

@ -3,77 +3,86 @@ import importlib
import logging
import os
import sys
import pathlib
import urllib.parse
import urllib.request
from importlib import metadata
from pathlib import Path
from typing import Optional, List
from types import ModuleType
from .output import log
from .storage import storage
plugins = {}
# 1: List archinstall.plugin definitions
# 2: Load the plugin entrypoint
# 3: Initiate the plugin and store it as .name in plugins
for plugin_definition in metadata.entry_points().select(group='archinstall.plugin'):
plugin_entrypoint = plugin_definition.load()
try:
plugins[plugin_definition.name] = plugin_entrypoint()
except Exception as err:
log(err, level=logging.ERROR)
log(f'Error: {err}', level=logging.ERROR)
log(f"The above error was detected when loading the plugin: {plugin_definition}", fg="red", level=logging.ERROR)
# The following functions and core are support structures for load_plugin()
def localize_path(profile_path :str) -> str:
if (url := urllib.parse.urlparse(profile_path)).scheme and url.scheme in ('https', 'http'):
converted_path = f"/tmp/{os.path.basename(profile_path).replace('.py', '')}_{hashlib.md5(os.urandom(12)).hexdigest()}.py"
def localize_path(path: Path) -> Path:
"""
Support structures for load_plugin()
"""
url = urllib.parse.urlparse(str(path))
if url.scheme and url.scheme in ('https', 'http'):
converted_path = Path(f'/tmp/{path.stem}_{hashlib.md5(os.urandom(12)).hexdigest()}.py')
with open(converted_path, "w") as temp_file:
temp_file.write(urllib.request.urlopen(url.geturl()).read().decode('utf-8'))
return converted_path
else:
return profile_path
return path
def import_via_path(path :str, namespace :Optional[str] = None) -> ModuleType:
def import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str]:
if not namespace:
namespace = os.path.basename(path)
if namespace == '__init__.py':
path = pathlib.PurePath(path)
namespace = path.parent.name
try:
spec = importlib.util.spec_from_file_location(namespace, path)
imported = importlib.util.module_from_spec(spec)
sys.modules[namespace] = imported
spec.loader.exec_module(sys.modules[namespace])
if spec and spec.loader:
imported = importlib.util.module_from_spec(spec)
sys.modules[namespace] = imported
spec.loader.exec_module(sys.modules[namespace])
return namespace
except Exception as err:
log(err, level=logging.ERROR)
log(f'Error: {err}', level=logging.ERROR)
log(f"The above error was detected when loading the plugin: {path}", fg="red", level=logging.ERROR)
try:
del(sys.modules[namespace]) # noqa: E275
except:
del sys.modules[namespace]
except Exception:
pass
def find_nth(haystack :List[str], needle :str, n :int) -> int:
start = haystack.find(needle)
while start >= 0 and n > 1:
start = haystack.find(needle, start + len(needle))
n -= 1
return start
return namespace
def load_plugin(path :str) -> ModuleType:
parsed_url = urllib.parse.urlparse(path)
log(f"Loading plugin {parsed_url}.", fg="gray", level=logging.INFO)
def find_nth(haystack: List[str], needle: str, n: int) -> Optional[int]:
indices = [idx for idx, elem in enumerate(haystack) if elem == needle]
if n <= len(indices):
return indices[n - 1]
return None
def load_plugin(path: Path):
namespace: Optional[str] = None
parsed_url = urllib.parse.urlparse(str(path))
log(f"Loading plugin from url {parsed_url}.", level=logging.INFO)
# The Profile was not a direct match on a remote URL
if not parsed_url.scheme:
@ -81,9 +90,10 @@ def load_plugin(path :str) -> ModuleType:
if os.path.isfile(path):
namespace = import_via_path(path)
elif parsed_url.scheme in ('https', 'http'):
namespace = import_via_path(localize_path(path))
localized = localize_path(path)
namespace = import_via_path(localized)
if namespace in sys.modules:
if namespace and namespace in sys.modules:
# Version dependency via __archinstall__version__ variable (if present) in the plugin
# Any errors in version inconsistency will be handled through normal error handling if not defined.
if hasattr(sys.modules[namespace], '__archinstall__version__'):
@ -99,7 +109,7 @@ def load_plugin(path :str) -> ModuleType:
plugins[namespace] = sys.modules[namespace].Plugin()
log(f"Plugin {plugins[namespace]} has been loaded.", fg="gray", level=logging.INFO)
except Exception as err:
log(err, level=logging.ERROR)
log(f'Error: {err}', level=logging.ERROR)
log(f"The above error was detected when initiating the plugin: {path}", fg="red", level=logging.ERROR)
else:
log(f"Plugin '{path}' is missing a valid entry-point or is corrupt.", fg="yellow", level=logging.WARNING)

View File

@ -194,23 +194,23 @@ class ProfileHandler:
install_session.add_additional_packages(f"{kernel}-headers")
# I've had kernel regen fail if it wasn't installed before nvidia-dkms
install_session.add_additional_packages("dkms xorg-server xorg-xinit nvidia-dkms")
install_session.add_additional_packages(['dkms', 'xorg-server', 'xorg-xinit', 'nvidia-dkms'])
return
elif 'amdgpu' in driver_pkgs:
# The order of these two are important if amdgpu is installed #808
if 'amdgpu' in install_session.MODULES:
install_session.MODULES.remove('amdgpu')
install_session.MODULES.append('amdgpu')
if 'amdgpu' in install_session.modules:
install_session.modules.remove('amdgpu')
install_session.modules.append('amdgpu')
if 'radeon' in install_session.MODULES:
install_session.MODULES.remove('radeon')
install_session.MODULES.append('radeon')
if 'radeon' in install_session.modules:
install_session.modules.remove('radeon')
install_session.modules.append('radeon')
install_session.add_additional_packages(additional_pkg)
except Exception as err:
log(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}", level=logging.WARNING, fg="yellow")
# Prep didn't run, so there's no driver to install
install_session.add_additional_packages("xorg-server xorg-xinit")
install_session.add_additional_packages(['xorg-server', 'xorg-xinit'])
def install_profile_config(self, install_session: 'Installer', profile_config: ProfileConfiguration):
profile = profile_config.profile

View File

@ -1,6 +1,6 @@
import logging
import time
from typing import Iterator
from typing import Iterator, Optional
from .exceptions import SysCallError
from .general import SysCommand, SysCommandWorker, locate_binary
from .installer import Installer
@ -8,51 +8,11 @@ from .output import log
from .storage import storage
class Ini:
def __init__(self, *args :str, **kwargs :str):
"""
Limited INI handler for now.
Supports multiple keywords through dictionary list items.
"""
self.kwargs = kwargs
def __str__(self) -> str:
result = ''
first_row_done = False
for top_level in self.kwargs:
if first_row_done:
result += f"\n[{top_level}]\n"
else:
result += f"[{top_level}]\n"
first_row_done = True
for key, val in self.kwargs[top_level].items():
if type(val) == list:
for item in val:
result += f"{key}={item}\n"
else:
result += f"{key}={val}\n"
return result
class Systemd(Ini):
"""
Placeholder class to do systemd specific setups.
"""
class Networkd(Systemd):
"""
Placeholder class to do systemd-network specific setups.
"""
class Boot:
def __init__(self, installation: Installer):
self.instance = installation
self.container_name = 'archinstall'
self.session = None
self.session: Optional[SysCommandWorker] = None
self.ready = False
def __enter__(self) -> 'Boot':
@ -63,17 +23,18 @@ class Boot:
self.session = existing_session.session
self.ready = existing_session.ready
else:
# '-P' or --console=pipe could help us not having to do a bunch
# of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual.
self.session = SysCommandWorker([
'/usr/bin/systemd-nspawn',
'-D', self.instance.target,
'-D', str(self.instance.target),
'--timezone=off',
'-b',
'--no-pager',
'--machine', self.container_name
])
# '-P' or --console=pipe could help us not having to do a bunch of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual.
if not self.ready:
if not self.ready and self.session:
while self.session.is_alive():
if b' login:' in self.session:
self.ready = True
@ -91,25 +52,31 @@ class Boot:
log(f"The error above occurred in a temporary boot-up of the installation {self.instance}", level=logging.ERROR, fg="red")
shutdown = None
shutdown_exit_code = -1
shutdown_exit_code: Optional[int] = -1
try:
shutdown = SysCommand(f'systemd-run --machine={self.container_name} --pty shutdown now')
except SysCallError as error:
shutdown_exit_code = error.exit_code
while self.session.is_alive():
time.sleep(0.25)
if self.session:
while self.session.is_alive():
time.sleep(0.25)
if shutdown:
if shutdown and shutdown.exit_code:
shutdown_exit_code = shutdown.exit_code
if self.session.exit_code == 0 or shutdown_exit_code == 0:
if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0):
storage['active_boot'] = None
else:
raise SysCallError(f"Could not shut down temporary boot of {self.instance}: {self.session.exit_code}/{shutdown_exit_code}", exit_code=next(filter(bool, [self.session.exit_code, shutdown_exit_code])))
session_exit_code = self.session.exit_code if self.session else -1
def __iter__(self) -> Iterator[str]:
raise SysCallError(
f"Could not shut down temporary boot of {self.instance}: {session_exit_code}/{shutdown_exit_code}",
exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code]))
)
def __iter__(self) -> Iterator[bytes]:
if self.session:
for value in self.session:
yield value

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import logging
import pathlib
from typing import List, Any, Optional, Dict, TYPE_CHECKING
from typing import Union
from ..locale_helpers import list_keyboard_languages, list_timezones
from ..menu import MenuSelectionType, Menu, TextInput
@ -29,13 +28,18 @@ def ask_ntp(preset: bool = True) -> bool:
return False if choice.value == Menu.no() else True
def ask_hostname(preset: str = None) -> str:
def ask_hostname(preset: str = '') -> str:
while True:
hostname = TextInput(_('Desired hostname for the installation: '), preset).run().strip()
hostname = TextInput(
str(_('Desired hostname for the installation: ')),
preset
).run().strip()
if hostname:
return hostname
def ask_for_a_timezone(preset: str = None) -> str:
def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]:
timezones = list_timezones()
default = 'UTC'
@ -48,10 +52,12 @@ def ask_for_a_timezone(preset: str = None) -> str:
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return choice.value
case MenuSelectionType.Selection: return choice.single_value
return None
def ask_for_audio_selection(desktop: bool = True, preset: Union[str, None] = None) -> Union[str, None]:
def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]:
no_audio = str(_('No audio server'))
choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio]
default = 'pipewire' if desktop else no_audio
@ -60,10 +66,12 @@ def ask_for_audio_selection(desktop: bool = True, preset: Union[str, None] = Non
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return choice.value
case MenuSelectionType.Selection: return choice.single_value
return None
def select_language(preset_value: str = None) -> str:
def select_language(preset: Optional[str] = None) -> Optional[str]:
"""
Asks the user to select a language
Usually this is combined with :ref:`archinstall.list_keyboard_languages`.
@ -75,17 +83,18 @@ def select_language(preset_value: str = None) -> str:
# sort alphabetically and then by length
sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len)
selected_lang = Menu(
choice = Menu(
_('Select keyboard layout'),
sorted_kb_lang,
preset_values=preset_value,
preset_values=preset,
sort=False
).run()
if selected_lang.value is None:
return preset_value
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return choice.single_value
return selected_lang.value
return None
def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]:
@ -100,8 +109,10 @@ def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]:
preselected = None
else:
preselected = list(preset_values.keys())
mirrors = list_mirrors()
selected_mirror = Menu(
choice = Menu(
_('Select one of the regions to download packages from'),
list(mirrors.keys()),
preset_values=preselected,
@ -109,13 +120,18 @@ def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]:
allow_reset=True
).run()
match selected_mirror.type_:
case MenuSelectionType.Reset: return {}
case MenuSelectionType.Skip: return preset_values
case _: return {selected: mirrors[selected] for selected in selected_mirror.value}
match choice.type_:
case MenuSelectionType.Reset:
return {}
case MenuSelectionType.Skip:
return preset_values
case MenuSelectionType.Selection:
return {selected: mirrors[selected] for selected in choice.multi_value}
return {}
def select_archinstall_language(languages: List[Language], preset_value: Language) -> Language:
def select_archinstall_language(languages: List[Language], preset: Language) -> Language:
# these are the displayed language names which can either be
# the english name of a language or, if present, the
# name of the language in its own language
@ -128,15 +144,15 @@ def select_archinstall_language(languages: List[Language], preset_value: Languag
choice = Menu(
title,
list(options.keys()),
default_option=preset_value.display_name,
default_option=preset.display_name,
preview_size=0.5
).run()
match choice.type_:
case MenuSelectionType.Skip:
return preset_value
case MenuSelectionType.Selection:
return options[choice.value]
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return options[choice.single_value]
raise ValueError('Language selection not handled')
def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]:
@ -223,4 +239,6 @@ def select_additional_repositories(preset: List[str]) -> List[str]:
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Reset: return []
case MenuSelectionType.Selection: return choice.value
case MenuSelectionType.Selection: return choice.single_value
return []

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Any, TYPE_CHECKING
from typing import Any, TYPE_CHECKING, Optional
from ..locale_helpers import list_locales
from ..menu import Menu, MenuSelectionType
@ -9,33 +9,37 @@ if TYPE_CHECKING:
_: Any
def select_locale_lang(preset: str = None) -> str:
def select_locale_lang(preset: Optional[str] = None) -> Optional[str]:
locales = list_locales()
locale_lang = set([locale.split()[0] for locale in locales])
selected_locale = Menu(
choice = Menu(
_('Choose which locale language to use'),
list(locale_lang),
sort=True,
preset_values=preset
).run()
match selected_locale.type_:
case MenuSelectionType.Selection: return selected_locale.value
match choice.type_:
case MenuSelectionType.Selection: return choice.single_value
case MenuSelectionType.Skip: return preset
return None
def select_locale_enc(preset: str = None) -> str:
def select_locale_enc(preset: Optional[str] = None) -> Optional[str]:
locales = list_locales()
locale_enc = set([locale.split()[1] for locale in locales])
selected_locale = Menu(
choice = Menu(
_('Choose which locale encoding to use'),
list(locale_enc),
sort=True,
preset_values=preset
).run()
match selected_locale.type_:
case MenuSelectionType.Selection: return selected_locale.value
match choice.type_:
case MenuSelectionType.Selection: return choice.single_value
case MenuSelectionType.Skip: return preset
return None

View File

@ -223,7 +223,7 @@ def perform_installation(mountpoint: Path):
# If the user provided a list of services to be enabled, pass the list to the enable_service function.
# Note that while it's called enable_service, it can actually take a list of services and iterate it.
if archinstall.arguments.get('services', None):
installation.enable_service(*archinstall.arguments['services'])
installation.enable_service(archinstall.arguments.get('services', []))
# If the user provided custom commands to be run post-installation, execute them now.
if archinstall.arguments.get('custom-commands', None):

View File

@ -239,7 +239,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode):
handler.config_installer(installation)
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
installation.add_additional_packages(archinstall.arguments.get('packages', None))
installation.add_additional_packages(archinstall.arguments.get('packages', []))
if users := archinstall.arguments.get('!users', None):
installation.create_users(users)
@ -278,7 +278,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode):
# If the user provided a list of services to be enabled, pass the list to the enable_service function.
# Note that while it's called enable_service, it can actually take a list of services and iterate it.
if archinstall.arguments.get('services', None):
installation.enable_service(*archinstall.arguments['services'])
installation.enable_service(archinstall.arguments.get('services', []))
# If the user provided custom commands to be run post-installation, execute them now.
if archinstall.arguments.get('custom-commands', None):

View File

@ -147,7 +147,7 @@ def perform_installation(mountpoint: Path):
handler.config_installer(installation)
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
installation.add_additional_packages(archinstall.arguments.get('packages', None))
installation.add_additional_packages(archinstall.arguments.get('packages', []))
if users := archinstall.arguments.get('!users', None):
installation.create_users(users)
@ -186,7 +186,7 @@ def perform_installation(mountpoint: Path):
# If the user provided a list of services to be enabled, pass the list to the enable_service function.
# Note that while it's called enable_service, it can actually take a list of services and iterate it.
if archinstall.arguments.get('services', None):
installation.enable_service(*archinstall.arguments['services'])
installation.enable_service(archinstall.arguments.get('services', []))
# If the user provided custom commands to be run post-installation, execute them now.
if archinstall.arguments.get('custom-commands', None):

View File

@ -1,14 +0,0 @@
[mypy]
python_version = 3.10
follow_imports = silent
exclude = (?x)(^archinstall/lib/disk/btrfs/btrfssubvolumeinfo\.py$
| ^archinstall/lib/general\.py$
| ^archinstall/lib/hardware\.py$
| ^archinstall/lib/menu/menu\.py$
| ^archinstall/lib/mirrors\.py$
| ^archinstall/lib/plugins\.py$
| ^archinstall/lib/installer\.py$
| ^archinstall/lib/systemd\.py$
| ^archinstall/lib/user_interaction/general_conf\.py$
| ^archinstall/lib/user_interaction/locale_conf\.py$)
files = archinstall/

View File

@ -60,6 +60,7 @@ packages = ["archinstall"]
[tool.mypy]
python_version = "3.10"
files = "archinstall/"
exclude = "tests"
[tool.bandit]