Merge branch 'master' into fix-3021-plugin
This commit is contained in:
commit
ef7d71bb7d
|
|
@ -41,8 +41,8 @@ body:
|
|||
attributes:
|
||||
value: >
|
||||
**Note**: Assuming you have network connectivity,
|
||||
you can easily post the installation log using the following command:
|
||||
`curl -F'file=@/var/log/archinstall/install.log' https://0x0.st`
|
||||
you can easily upload the installation log and get a shareable URL by running:
|
||||
`archinstall share-log`
|
||||
|
||||
- type: textarea
|
||||
id: freeform
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ repos:
|
|||
args: [--config=.flake8]
|
||||
fail_fast: true
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.20.2
|
||||
rev: v2.1.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
args: [
|
||||
|
|
|
|||
|
|
@ -101,9 +101,9 @@ If you come across any issues, kindly submit your issue here on GitHub or post y
|
|||
When submitting an issue, please:
|
||||
* Provide the stacktrace of the output if applicable
|
||||
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
|
||||
* To extract the log from the ISO image, one way is to use<br>
|
||||
* To upload the log from the ISO image and get a shareable URL, run<br>
|
||||
```shell
|
||||
curl -F'file=@/var/log/archinstall/install.log' https://0x0.st
|
||||
archinstall share-log
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from archinstall.tui.result import ResultType
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.models.users import User
|
||||
|
||||
|
||||
class DesktopProfile(Profile):
|
||||
|
|
@ -88,6 +89,11 @@ class DesktopProfile(Profile):
|
|||
for profile in self.current_selection:
|
||||
profile.post_install(install_session)
|
||||
|
||||
@override
|
||||
def provision(self, install_session: Installer, users: list[User]) -> None:
|
||||
for profile in self.current_selection:
|
||||
profile.provision(install_session, users)
|
||||
|
||||
@override
|
||||
def install(self, install_session: Installer) -> None:
|
||||
# Install common packages for all desktop environments
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from typing import override
|
||||
|
||||
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.models.users import User
|
||||
|
||||
|
||||
class BspwmProfile(Profile):
|
||||
|
|
@ -27,3 +29,11 @@ class BspwmProfile(Profile):
|
|||
@override
|
||||
def default_greeter_type(self) -> GreeterType:
|
||||
return GreeterType.Lightdm
|
||||
|
||||
@override
|
||||
def provision(self, install_session: Installer, users: list[User]) -> None:
|
||||
for user in users:
|
||||
install_session.arch_chroot('mkdir -p ~/.config/bspwm ~/.config/sxhkd', run_as=user.username)
|
||||
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/bspwmrc ~/.config/bspwm/', run_as=user.username)
|
||||
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/sxhkdrc ~/.config/sxhkd/', run_as=user.username)
|
||||
install_session.arch_chroot('chmod +x ~/.config/bspwm/bspwmrc', run_as=user.username)
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@ class BudgieProfile(Profile):
|
|||
return [
|
||||
'materia-gtk-theme',
|
||||
'budgie',
|
||||
'konsole',
|
||||
'dolphin',
|
||||
'mate-terminal',
|
||||
'nemo',
|
||||
'nemo-fileroller',
|
||||
'papirus-icon-theme',
|
||||
]
|
||||
|
||||
@property
|
||||
@override
|
||||
def default_greeter_type(self) -> GreeterType:
|
||||
return GreeterType.Sddm
|
||||
return GreeterType.LightdmSlick
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import urllib.error
|
|||
import urllib.parse
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import Any, Self
|
||||
from urllib.request import Request, urlopen
|
||||
|
|
@ -17,6 +18,7 @@ from archinstall.lib.menu.util import get_password
|
|||
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
|
||||
from archinstall.lib.models.authentication import AuthenticationConfiguration
|
||||
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration
|
||||
from archinstall.lib.models.locale import LocaleConfiguration
|
||||
from archinstall.lib.models.mirrors import MirrorConfiguration
|
||||
|
|
@ -57,121 +59,222 @@ class Arguments:
|
|||
verbose: bool = False
|
||||
|
||||
|
||||
class ArchConfigType(StrEnum):
|
||||
VERSION = 'version'
|
||||
SCRIPT = 'script'
|
||||
LOCALE_CONFIG = 'locale_config'
|
||||
ARCHINSTALL_LANGUAGE = 'archinstall_language'
|
||||
DISK_CONFIG = 'disk_config'
|
||||
PROFILE_CONFIG = 'profile_config'
|
||||
MIRROR_CONFIG = 'mirror_config'
|
||||
NETWORK_CONFIG = 'network_config'
|
||||
BOOTLOADER_CONFIG = 'bootloader_config'
|
||||
APP_CONFIG = 'app_config'
|
||||
AUTH_CONFIG = 'auth_config'
|
||||
SWAP = 'swap'
|
||||
USERS = 'users'
|
||||
ROOT_ENC_PASSWORD = 'root_enc_password'
|
||||
ENCRYPTION_PASSWORD = 'encryption_password'
|
||||
HOSTNAME = 'hostname'
|
||||
KERNELS = 'kernels'
|
||||
NTP = 'ntp'
|
||||
TIMEZONE = 'timezone'
|
||||
SERVICES = 'services'
|
||||
PACKAGES = 'packages'
|
||||
PACMAN_CONFIG = 'pacman_config'
|
||||
CUSTOM_COMMANDS = 'custom_commands'
|
||||
|
||||
def text(self) -> str:
|
||||
match self:
|
||||
case ArchConfigType.ARCHINSTALL_LANGUAGE:
|
||||
return tr('ArchInstall Language')
|
||||
case ArchConfigType.VERSION:
|
||||
return tr('Version')
|
||||
case ArchConfigType.SCRIPT:
|
||||
return tr('Installation Script')
|
||||
case ArchConfigType.LOCALE_CONFIG:
|
||||
return tr('Locales')
|
||||
case ArchConfigType.DISK_CONFIG:
|
||||
return tr('Disk configuration')
|
||||
case ArchConfigType.PROFILE_CONFIG:
|
||||
return tr('Profile')
|
||||
case ArchConfigType.MIRROR_CONFIG:
|
||||
return tr('Mirrors and repositories')
|
||||
case ArchConfigType.NETWORK_CONFIG:
|
||||
return tr('Network')
|
||||
case ArchConfigType.BOOTLOADER_CONFIG:
|
||||
return tr('Bootloader')
|
||||
case ArchConfigType.APP_CONFIG:
|
||||
return tr('Application')
|
||||
case ArchConfigType.AUTH_CONFIG:
|
||||
return tr('Authentication')
|
||||
case ArchConfigType.SWAP:
|
||||
return tr('Swap')
|
||||
case ArchConfigType.HOSTNAME:
|
||||
return tr('Hostname')
|
||||
case ArchConfigType.KERNELS:
|
||||
return tr('Kernels')
|
||||
case ArchConfigType.NTP:
|
||||
return tr('Automatic time sync (NTP)')
|
||||
case ArchConfigType.TIMEZONE:
|
||||
return tr('Timezone')
|
||||
case ArchConfigType.SERVICES:
|
||||
return tr('Services')
|
||||
case ArchConfigType.PACKAGES:
|
||||
return tr('Additional packages')
|
||||
case ArchConfigType.PACMAN_CONFIG:
|
||||
return tr('Pacman')
|
||||
case ArchConfigType.CUSTOM_COMMANDS:
|
||||
return tr('Custom commands')
|
||||
case ArchConfigType.USERS:
|
||||
return tr('Users')
|
||||
case ArchConfigType.ROOT_ENC_PASSWORD:
|
||||
return tr('Root encrypted password')
|
||||
case ArchConfigType.ENCRYPTION_PASSWORD:
|
||||
return tr('Disk encryption password')
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArchConfig:
|
||||
version: str | None = None
|
||||
script: str | None = None
|
||||
locale_config: LocaleConfiguration | None = None
|
||||
archinstall_language: Language = field(default_factory=lambda: translation_handler.get_language_by_abbr('en'))
|
||||
disk_config: DiskLayoutConfiguration | None = None
|
||||
profile_config: ProfileConfiguration | None = None
|
||||
mirror_config: MirrorConfiguration | None = None
|
||||
network_config: NetworkConfiguration | None = None
|
||||
bootloader_config: BootloaderConfiguration | None = None
|
||||
app_config: ApplicationConfiguration | None = None
|
||||
auth_config: AuthenticationConfiguration | None = None
|
||||
swap: ZramConfiguration | None = None
|
||||
hostname: str = 'archlinux'
|
||||
kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
|
||||
ntp: bool = True
|
||||
packages: list[str] = field(default_factory=list)
|
||||
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
|
||||
timezone: str = 'UTC'
|
||||
services: list[str] = field(default_factory=list)
|
||||
custom_commands: list[str] = field(default_factory=list)
|
||||
version: str | None = None
|
||||
script: str | None = None
|
||||
locale_config: LocaleConfiguration | None = None
|
||||
archinstall_language: Language = field(default_factory=lambda: translation_handler.get_language_by_abbr('en'))
|
||||
disk_config: DiskLayoutConfiguration | None = None
|
||||
profile_config: ProfileConfiguration | None = None
|
||||
mirror_config: MirrorConfiguration | None = None
|
||||
network_config: NetworkConfiguration | None = None
|
||||
bootloader_config: BootloaderConfiguration | None = None
|
||||
app_config: ApplicationConfiguration | None = None
|
||||
auth_config: AuthenticationConfiguration | None = None
|
||||
swap: ZramConfiguration | None = None
|
||||
hostname: str = 'archlinux'
|
||||
kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
|
||||
ntp: bool = True
|
||||
packages: list[str] = field(default_factory=list)
|
||||
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
|
||||
timezone: str = 'UTC'
|
||||
services: list[str] = field(default_factory=list)
|
||||
custom_commands: list[str] = field(default_factory=list)
|
||||
|
||||
def unsafe_config(self) -> dict[str, Any]:
|
||||
config: dict[str, list[UserSerialization] | str | None] = {}
|
||||
def unsafe_config(self) -> dict[ArchConfigType, Any]:
|
||||
config: dict[ArchConfigType, list[UserSerialization] | str | None] = {}
|
||||
|
||||
if self.auth_config:
|
||||
if self.auth_config.users:
|
||||
config['users'] = [user.json() for user in self.auth_config.users]
|
||||
if self.auth_config:
|
||||
if self.auth_config.users:
|
||||
config[ArchConfigType.USERS] = [user.json() for user in self.auth_config.users]
|
||||
|
||||
if self.auth_config.root_enc_password:
|
||||
config['root_enc_password'] = self.auth_config.root_enc_password.enc_password
|
||||
if self.auth_config.root_enc_password:
|
||||
config[ArchConfigType.ROOT_ENC_PASSWORD] = self.auth_config.root_enc_password.enc_password
|
||||
|
||||
if self.disk_config:
|
||||
disk_encryption = self.disk_config.disk_encryption
|
||||
if disk_encryption and disk_encryption.encryption_password:
|
||||
config['encryption_password'] = disk_encryption.encryption_password.plaintext
|
||||
if self.disk_config:
|
||||
disk_encryption = self.disk_config.disk_encryption
|
||||
if disk_encryption and disk_encryption.encryption_password:
|
||||
config[ArchConfigType.ENCRYPTION_PASSWORD] = disk_encryption.encryption_password.plaintext
|
||||
|
||||
return config
|
||||
return config
|
||||
|
||||
def safe_config(self) -> dict[str, Any]:
|
||||
config: Any = {
|
||||
'version': self.version,
|
||||
'script': self.script,
|
||||
'archinstall-language': self.archinstall_language.json(),
|
||||
'hostname': self.hostname,
|
||||
'kernels': self.kernels,
|
||||
'ntp': self.ntp,
|
||||
'packages': self.packages,
|
||||
'pacman_config': self.pacman_config.json(),
|
||||
'swap': self.swap,
|
||||
'timezone': self.timezone,
|
||||
'services': self.services,
|
||||
'custom_commands': self.custom_commands,
|
||||
'bootloader_config': self.bootloader_config.json() if self.bootloader_config else None,
|
||||
'app_config': self.app_config.json() if self.app_config else None,
|
||||
'auth_config': self.auth_config.json() if self.auth_config else None,
|
||||
}
|
||||
def safe_config(self) -> dict[ArchConfigType, Any]:
|
||||
base_config: dict[ArchConfigType, Any] = {
|
||||
ArchConfigType.VERSION: self.version,
|
||||
ArchConfigType.SCRIPT: self.script,
|
||||
ArchConfigType.ARCHINSTALL_LANGUAGE: self.archinstall_language.json(),
|
||||
}
|
||||
|
||||
if self.locale_config:
|
||||
config['locale_config'] = self.locale_config.json()
|
||||
base_config.update(self.plain_cfg())
|
||||
sub_config = self.sub_cfg()
|
||||
|
||||
if self.disk_config:
|
||||
config['disk_config'] = self.disk_config.json()
|
||||
for config_type, value in sub_config.items():
|
||||
if not hasattr(value, 'json'):
|
||||
raise ValueError(f'Config value for {config_type} must implement json() method')
|
||||
base_config[config_type] = value.json()
|
||||
|
||||
if self.profile_config:
|
||||
config['profile_config'] = self.profile_config.json()
|
||||
return base_config
|
||||
|
||||
if self.mirror_config:
|
||||
config['mirror_config'] = self.mirror_config.json()
|
||||
def plain_cfg(self) -> dict[ArchConfigType, str | list[str] | bool]:
|
||||
return {
|
||||
ArchConfigType.HOSTNAME: self.hostname,
|
||||
ArchConfigType.KERNELS: self.kernels,
|
||||
ArchConfigType.NTP: self.ntp,
|
||||
ArchConfigType.TIMEZONE: self.timezone,
|
||||
ArchConfigType.SERVICES: self.services,
|
||||
ArchConfigType.PACKAGES: self.packages,
|
||||
ArchConfigType.CUSTOM_COMMANDS: self.custom_commands,
|
||||
}
|
||||
|
||||
if self.network_config:
|
||||
config['network_config'] = self.network_config.json()
|
||||
def sub_cfg(self) -> dict[ArchConfigType, SubConfig]:
|
||||
cfg: dict[ArchConfigType, SubConfig] = {
|
||||
ArchConfigType.PACMAN_CONFIG: self.pacman_config,
|
||||
}
|
||||
|
||||
return config
|
||||
if self.mirror_config:
|
||||
cfg[ArchConfigType.MIRROR_CONFIG] = self.mirror_config
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
|
||||
arch_config = cls()
|
||||
if self.bootloader_config:
|
||||
cfg[ArchConfigType.BOOTLOADER_CONFIG] = self.bootloader_config
|
||||
|
||||
arch_config.locale_config = LocaleConfiguration.parse_arg(args_config)
|
||||
if self.disk_config:
|
||||
cfg[ArchConfigType.DISK_CONFIG] = self.disk_config
|
||||
|
||||
if script := args_config.get('script', None):
|
||||
arch_config.script = script
|
||||
if self.swap:
|
||||
cfg[ArchConfigType.SWAP] = self.swap
|
||||
|
||||
if archinstall_lang := args_config.get('archinstall-language', None):
|
||||
arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang)
|
||||
translation_handler.activate(arch_config.archinstall_language, set_font=False)
|
||||
if self.auth_config:
|
||||
cfg[ArchConfigType.AUTH_CONFIG] = self.auth_config
|
||||
|
||||
if disk_config := args_config.get('disk_config', {}):
|
||||
enc_password = args_config.get('encryption_password', '')
|
||||
password = Password(plaintext=enc_password) if enc_password else None
|
||||
arch_config.disk_config = DiskLayoutConfiguration.parse_arg(disk_config, password)
|
||||
if self.locale_config:
|
||||
cfg[ArchConfigType.LOCALE_CONFIG] = self.locale_config
|
||||
|
||||
# DEPRECATED
|
||||
# backwards compatibility for main level disk_encryption entry
|
||||
disk_encryption: DiskEncryption | None = None
|
||||
if self.profile_config:
|
||||
cfg[ArchConfigType.PROFILE_CONFIG] = self.profile_config
|
||||
|
||||
if args_config.get('disk_encryption', None) is not None and arch_config.disk_config is not None:
|
||||
disk_encryption = DiskEncryption.parse_arg(
|
||||
arch_config.disk_config,
|
||||
args_config['disk_encryption'],
|
||||
Password(plaintext=args_config.get('encryption_password', '')),
|
||||
)
|
||||
if self.network_config:
|
||||
cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config
|
||||
|
||||
if disk_encryption:
|
||||
arch_config.disk_config.disk_encryption = disk_encryption
|
||||
if self.app_config:
|
||||
cfg[ArchConfigType.APP_CONFIG] = self.app_config
|
||||
|
||||
if profile_config := args_config.get('profile_config', None):
|
||||
arch_config.profile_config = ProfileConfiguration.parse_arg(profile_config)
|
||||
return cfg
|
||||
|
||||
if mirror_config := args_config.get('mirror_config', None):
|
||||
backwards_compatible_repo = []
|
||||
if additional_repositories := args_config.get('additional-repositories', []):
|
||||
backwards_compatible_repo = [Repository(r) for r in additional_repositories]
|
||||
@classmethod
|
||||
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
|
||||
arch_config = cls()
|
||||
|
||||
arch_config.locale_config = LocaleConfiguration.parse_arg(args_config)
|
||||
|
||||
if script := args_config.get('script', None):
|
||||
arch_config.script = script
|
||||
|
||||
if archinstall_lang := args_config.get('archinstall-language', None):
|
||||
arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang)
|
||||
translation_handler.activate(arch_config.archinstall_language, set_font=False)
|
||||
|
||||
if disk_config := args_config.get('disk_config', {}):
|
||||
enc_password = args_config.get('encryption_password', '')
|
||||
password = Password(plaintext=enc_password) if enc_password else None
|
||||
arch_config.disk_config = DiskLayoutConfiguration.parse_arg(disk_config, password)
|
||||
|
||||
# DEPRECATED
|
||||
# backwards compatibility for main level disk_encryption entry
|
||||
disk_encryption: DiskEncryption | None = None
|
||||
|
||||
if args_config.get('disk_encryption', None) is not None and arch_config.disk_config is not None:
|
||||
disk_encryption = DiskEncryption.parse_arg(
|
||||
arch_config.disk_config,
|
||||
args_config['disk_encryption'],
|
||||
Password(plaintext=args_config.get('encryption_password', '')),
|
||||
)
|
||||
|
||||
if disk_encryption:
|
||||
arch_config.disk_config.disk_encryption = disk_encryption
|
||||
|
||||
if profile_config := args_config.get('profile_config', None):
|
||||
arch_config.profile_config = ProfileConfiguration.parse_arg(profile_config)
|
||||
|
||||
if mirror_config := args_config.get('mirror_config', None):
|
||||
backwards_compatible_repo = []
|
||||
if additional_repositories := args_config.get('additional-repositories', []):
|
||||
backwards_compatible_repo = [Repository(r) for r in additional_repositories]
|
||||
|
||||
arch_config.mirror_config = MirrorConfiguration.parse_args(
|
||||
mirror_config,
|
||||
|
|
@ -260,305 +363,298 @@ class ArchConfig:
|
|||
|
||||
|
||||
class ArchConfigHandler:
|
||||
def __init__(self) -> None:
|
||||
self._parser: ArgumentParser = self._define_arguments()
|
||||
args: Arguments = self._parse_args()
|
||||
self._args = args
|
||||
def __init__(self) -> None:
|
||||
self._parser: ArgumentParser = self._define_arguments()
|
||||
args: Arguments = self._parse_args()
|
||||
self._args = args
|
||||
|
||||
config = self._parse_config()
|
||||
config = self._parse_config()
|
||||
|
||||
try:
|
||||
self._config = ArchConfig.from_config(config, args)
|
||||
self._config.version = get_version()
|
||||
except ValueError as err:
|
||||
warn(str(err))
|
||||
sys.exit(1)
|
||||
try:
|
||||
self._config = ArchConfig.from_config(config, args)
|
||||
self._config.version = get_version()
|
||||
except ValueError as err:
|
||||
warn(str(err))
|
||||
sys.exit(1)
|
||||
|
||||
@property
|
||||
def config(self) -> ArchConfig:
|
||||
return self._config
|
||||
@property
|
||||
def config(self) -> ArchConfig:
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def args(self) -> Arguments:
|
||||
return self._args
|
||||
@property
|
||||
def args(self) -> Arguments:
|
||||
return self._args
|
||||
|
||||
def get_script(self) -> str:
|
||||
if script := self.args.script:
|
||||
return script
|
||||
def get_script(self) -> str:
|
||||
if script := self.args.script:
|
||||
return script
|
||||
|
||||
if script := self.config.script:
|
||||
return script
|
||||
if script := self.config.script:
|
||||
return script
|
||||
|
||||
return 'guided'
|
||||
return 'guided'
|
||||
|
||||
def print_help(self) -> None:
|
||||
self._parser.print_help()
|
||||
def print_help(self) -> None:
|
||||
self._parser.print_help()
|
||||
|
||||
def _define_arguments(self) -> ArgumentParser:
|
||||
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument(
|
||||
'-v',
|
||||
'--version',
|
||||
action='version',
|
||||
default=False,
|
||||
version='%(prog)s ' + get_version(),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--config',
|
||||
type=Path,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='JSON configuration file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--config-url',
|
||||
type=str,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='Url to a JSON configuration file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--creds',
|
||||
type=Path,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='JSON credentials configuration file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--creds-url',
|
||||
type=str,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='Url to a JSON credentials configuration file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--creds-decryption-key',
|
||||
type=str,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='Decryption key for credentials file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--silent',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='WARNING: Disables all prompts for input and confirmation. If no configuration is provided, this is ignored',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
'--dry_run',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Generates a configuration file and then exits instead of performing an installation',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--script',
|
||||
nargs='?',
|
||||
help='Script to run for installation',
|
||||
type=str,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--mountpoint',
|
||||
type=Path,
|
||||
nargs='?',
|
||||
default=Path('/mnt'),
|
||||
help='Define an alternate mount point for installation',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-ntp',
|
||||
action='store_true',
|
||||
help='Disables NTP checks during installation',
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-wkd',
|
||||
action='store_true',
|
||||
help='Disables checking if archlinux keyring wkd sync is complete.',
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-boot',
|
||||
action='store_true',
|
||||
help='Disables installation of a boot loader (note: only use this when problems arise with the boot loader step).',
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Adds debug info into the log',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--offline',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Disabled online upstream services such as package search and key-ring auto update.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-pkg-lookups',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Disabled package validation specifically prior to starting installation.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--plugin',
|
||||
nargs='?',
|
||||
type=str,
|
||||
default=None,
|
||||
help='File path to a plugin to load',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-version-check',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Skip the version check when running archinstall',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-wifi-check',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Skip wifi check when running archinstall',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--advanced',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Enabled advanced options',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Enabled verbose options',
|
||||
)
|
||||
def _define_arguments(self) -> ArgumentParser:
|
||||
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument(
|
||||
'-v',
|
||||
'--version',
|
||||
action='version',
|
||||
default=False,
|
||||
version='%(prog)s ' + get_version(),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--config',
|
||||
type=Path,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='JSON configuration file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--config-url',
|
||||
type=str,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='Url to a JSON configuration file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--creds',
|
||||
type=Path,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='JSON credentials configuration file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--creds-url',
|
||||
type=str,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='Url to a JSON credentials configuration file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--creds-decryption-key',
|
||||
type=str,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='Decryption key for credentials file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--silent',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='WARNING: Disables all prompts for input and confirmation. If no configuration is provided, this is ignored',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
'--dry_run',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Generates a configuration file and then exits instead of performing an installation',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--script',
|
||||
nargs='?',
|
||||
help='Script to run for installation',
|
||||
type=str,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--mountpoint',
|
||||
type=Path,
|
||||
nargs='?',
|
||||
default=Path('/mnt'),
|
||||
help='Define an alternate mount point for installation',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-ntp',
|
||||
action='store_true',
|
||||
help='Disables NTP checks during installation',
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-wkd',
|
||||
action='store_true',
|
||||
help='Disables checking if archlinux keyring wkd sync is complete.',
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-boot',
|
||||
action='store_true',
|
||||
help='Disables installation of a boot loader (note: only use this when problems arise with the boot loader step).',
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Adds debug info into the log',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--offline',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Disabled online upstream services such as package search and key-ring auto update.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-pkg-lookups',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Disabled package validation specifically prior to starting installation.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--plugin',
|
||||
nargs='?',
|
||||
type=str,
|
||||
default=None,
|
||||
help='File path to a plugin to load',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-version-check',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Skip the version check when running archinstall',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-wifi-check',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Skip wifi check when running archinstall',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--advanced',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Enabled advanced options',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Enabled verbose options',
|
||||
)
|
||||
return parser
|
||||
|
||||
return parser
|
||||
def _parse_args(self) -> Arguments:
|
||||
argparse_args = vars(self._parser.parse_args())
|
||||
args: Arguments = Arguments(**argparse_args)
|
||||
|
||||
def _parse_args(self) -> Arguments:
|
||||
argparse_args = vars(self._parser.parse_args())
|
||||
args: Arguments = Arguments(**argparse_args)
|
||||
# amend the parameters (check internal consistency)
|
||||
# Installation can't be silent if config is not passed
|
||||
if args.config is None and args.config_url is None:
|
||||
args.silent = False
|
||||
|
||||
# amend the parameters (check internal consistency)
|
||||
# Installation can't be silent if config is not passed
|
||||
if args.config is None and args.config_url is None:
|
||||
args.silent = False
|
||||
if args.debug:
|
||||
warn(f'Warning: --debug mode will write certain credentials to {logger.path}!')
|
||||
|
||||
if args.debug:
|
||||
warn(f'Warning: --debug mode will write certain credentials to {logger.path}!')
|
||||
if args.plugin:
|
||||
plugin_path = Path(args.plugin)
|
||||
load_plugin(plugin_path)
|
||||
|
||||
if args.plugin:
|
||||
# Pass plugin as string to preserve URL format (avoid Path normalization)
|
||||
load_plugin(args.plugin)
|
||||
if args.creds_decryption_key is None:
|
||||
if os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY'):
|
||||
args.creds_decryption_key = os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY')
|
||||
|
||||
if args.creds_decryption_key is None:
|
||||
if os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY'):
|
||||
args.creds_decryption_key = os.environ.get('ARCHINSTALL_CREDS_DECRYPTION_KEY')
|
||||
return args
|
||||
|
||||
return args
|
||||
def _parse_config(self) -> dict[str, Any]:
|
||||
config: dict[str, Any] = {}
|
||||
config_data: str | None = None
|
||||
creds_data: str | None = None
|
||||
|
||||
def _parse_config(self) -> dict[str, Any]:
|
||||
config: dict[str, Any] = {}
|
||||
config_data: str | None = None
|
||||
creds_data: str | None = None
|
||||
if self._args.config is not None:
|
||||
config_data = self._read_file(self._args.config)
|
||||
elif self._args.config_url is not None:
|
||||
config_data = self._fetch_from_url(self._args.config_url)
|
||||
|
||||
if self._args.config is not None:
|
||||
config_data = self._read_file(self._args.config)
|
||||
elif self._args.config_url is not None:
|
||||
config_data = self._fetch_from_url(self._args.config_url)
|
||||
if config_data is not None:
|
||||
config.update(json.loads(config_data))
|
||||
|
||||
if config_data is not None:
|
||||
config.update(json.loads(config_data))
|
||||
if self._args.creds is not None:
|
||||
creds_data = self._read_file(self._args.creds)
|
||||
elif self._args.creds_url is not None:
|
||||
creds_data = self._fetch_from_url(self._args.creds_url)
|
||||
|
||||
if self._args.creds is not None:
|
||||
creds_data = self._read_file(self._args.creds)
|
||||
elif self._args.creds_url is not None:
|
||||
creds_data = self._fetch_from_url(self._args.creds_url)
|
||||
if creds_data is not None:
|
||||
json_data = self._process_creds_data(creds_data)
|
||||
if json_data is not None:
|
||||
config.update(json_data)
|
||||
|
||||
if creds_data is not None:
|
||||
json_data = self._process_creds_data(creds_data)
|
||||
if json_data is not None:
|
||||
config.update(json_data)
|
||||
config = self._cleanup_config(config)
|
||||
|
||||
config = self._cleanup_config(config)
|
||||
return config
|
||||
|
||||
return config
|
||||
def _process_creds_data(self, creds_data: str) -> dict[str, Any] | None:
|
||||
if creds_data.startswith('$'): # encrypted data
|
||||
if self._args.creds_decryption_key is not None:
|
||||
try:
|
||||
creds_data = decrypt(creds_data, self._args.creds_decryption_key)
|
||||
return json.loads(creds_data)
|
||||
except ValueError as err:
|
||||
if 'Invalid password' in str(err):
|
||||
error(tr('Incorrect credentials file decryption password'))
|
||||
sys.exit(1)
|
||||
else:
|
||||
debug(f'Error decrypting credentials file: {err}')
|
||||
raise err from err
|
||||
else:
|
||||
header = tr('Enter credentials file decryption password')
|
||||
wrong_pwd_text = tr('Incorrect password')
|
||||
prompt = header
|
||||
|
||||
def _process_creds_data(self, creds_data: str) -> dict[str, Any] | None:
|
||||
if creds_data.startswith('$'): # encrypted data
|
||||
if self._args.creds_decryption_key is not None:
|
||||
try:
|
||||
creds_data = decrypt(creds_data, self._args.creds_decryption_key)
|
||||
return json.loads(creds_data)
|
||||
except ValueError as err:
|
||||
if 'Invalid password' in str(err):
|
||||
error(tr('Incorrect credentials file decryption password'))
|
||||
sys.exit(1)
|
||||
else:
|
||||
debug(f'Error decrypting credentials file: {err}')
|
||||
raise err from err
|
||||
else:
|
||||
header = tr('Enter credentials file decryption password')
|
||||
wrong_pwd_text = tr('Incorrect password')
|
||||
prompt = header
|
||||
while True:
|
||||
decryption_pwd: Password | None = tui.run(
|
||||
lambda p=prompt: get_password( # type: ignore[misc]
|
||||
header=p,
|
||||
allow_skip=False,
|
||||
no_confirmation=True,
|
||||
)
|
||||
)
|
||||
|
||||
while True:
|
||||
decryption_pwd: Password | None = tui.run(
|
||||
lambda p=prompt: get_password( # type: ignore[misc]
|
||||
header=p,
|
||||
allow_skip=False,
|
||||
no_confirmation=True,
|
||||
)
|
||||
)
|
||||
if not decryption_pwd:
|
||||
return None
|
||||
|
||||
if not decryption_pwd:
|
||||
return None
|
||||
try:
|
||||
creds_data = decrypt(creds_data, decryption_pwd.plaintext)
|
||||
break
|
||||
except ValueError as err:
|
||||
if 'Invalid password' in str(err):
|
||||
debug('Incorrect credentials file decryption password')
|
||||
prompt = f'{header}' + f'\n\n{wrong_pwd_text}'
|
||||
else:
|
||||
debug(f'Error decrypting credentials file: {err}')
|
||||
raise err from err
|
||||
|
||||
try:
|
||||
creds_data = decrypt(creds_data, decryption_pwd.plaintext)
|
||||
break
|
||||
except ValueError as err:
|
||||
if 'Invalid password' in str(err):
|
||||
debug('Incorrect credentials file decryption password')
|
||||
prompt = f'{header}' + f'\n\n{wrong_pwd_text}'
|
||||
else:
|
||||
debug(f'Error decrypting credentials file: {err}')
|
||||
raise err from err
|
||||
return json.loads(creds_data)
|
||||
|
||||
return json.loads(creds_data)
|
||||
def _fetch_from_url(self, url: str) -> str:
|
||||
if urllib.parse.urlparse(url).scheme:
|
||||
try:
|
||||
req = Request(url, headers={'User-Agent': 'ArchInstall'})
|
||||
with urlopen(req) as resp:
|
||||
return resp.read().decode('utf-8')
|
||||
except urllib.error.HTTPError as err:
|
||||
error(f'Could not fetch JSON from {url}: {err}')
|
||||
else:
|
||||
error('Not a valid url')
|
||||
|
||||
def _fetch_from_url(self, url: str) -> str:
|
||||
if urllib.parse.urlparse(url).scheme:
|
||||
try:
|
||||
req = Request(url, headers={'User-Agent': 'ArchInstall'})
|
||||
# FIXED: Added a 15-second timeout to prevent infinite hanging
|
||||
with urlopen(req, timeout=15) as resp:
|
||||
return resp.read().decode('utf-8')
|
||||
except urllib.error.HTTPError as err:
|
||||
error(f'Could not fetch JSON from {url}: {err}')
|
||||
else:
|
||||
error('Not a valid url')
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(1)
|
||||
def _read_file(self, path: Path) -> str:
|
||||
if not path.exists():
|
||||
error(f'Could not find file {path}')
|
||||
sys.exit(1)
|
||||
|
||||
def _read_file(self, path: Path) -> str:
|
||||
if not path.exists():
|
||||
error(f'Could not find file {path}')
|
||||
sys.exit(1)
|
||||
return path.read_text()
|
||||
|
||||
return path.read_text()
|
||||
def _cleanup_config(self, config: Namespace | dict[str, Any]) -> dict[str, Any]:
|
||||
clean_args = {}
|
||||
for key, val in config.items():
|
||||
if isinstance(val, dict):
|
||||
val = self._cleanup_config(val)
|
||||
|
||||
# FIXED: Added safe handling to support both ArgumentParser Namespace objects AND dictionaries,
|
||||
# as calling .items() on a Namespace object without using `vars()` throws an AttributeError.
|
||||
def _cleanup_config(self, config: Namespace | dict[str, Any]) -> dict[str, Any]:
|
||||
clean_args = {}
|
||||
# Convert Namespace to dict if necessary before iterating
|
||||
config_dict = vars(config) if isinstance(config, Namespace) else config
|
||||
|
||||
for key, val in config_dict.items():
|
||||
if isinstance(val, dict):
|
||||
val = self._cleanup_config(val)
|
||||
if val is not None:
|
||||
clean_args[key] = val
|
||||
|
||||
if val is not None:
|
||||
clean_args[key] = val
|
||||
|
||||
return clean_args
|
||||
return clean_args
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ from typing import Any
|
|||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from archinstall.lib.args import ArchConfig
|
||||
from archinstall.lib.args import ArchConfig, ArchConfigType
|
||||
from archinstall.lib.crypt import encrypt
|
||||
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||
from archinstall.lib.menu.util import get_password, prompt_dir
|
||||
from archinstall.lib.models.bootloader import Bootloader
|
||||
from archinstall.lib.models.network import NetworkConfiguration
|
||||
from archinstall.lib.output import debug, logger, warn
|
||||
from archinstall.lib.output import as_key_value_pair, debug, logger, warn
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
|
|
@ -45,15 +44,15 @@ class ConfigurationOutput:
|
|||
def user_config_to_json(self) -> str:
|
||||
config = self._config.safe_config()
|
||||
|
||||
adapter = TypeAdapter(dict[str, Any])
|
||||
adapter = TypeAdapter(dict[ArchConfigType, Any])
|
||||
python_dict = adapter.dump_python(config)
|
||||
return json.dumps(python_dict, indent=4, sort_keys=True)
|
||||
|
||||
def user_credentials_to_json(self) -> str:
|
||||
config = self._config.unsafe_config()
|
||||
cfg = self._config.unsafe_config()
|
||||
|
||||
adapter = TypeAdapter(dict[str, Any])
|
||||
python_dict = adapter.dump_python(config)
|
||||
adapter = TypeAdapter(dict[ArchConfigType, Any])
|
||||
python_dict = adapter.dump_python(cfg)
|
||||
return json.dumps(python_dict, indent=4, sort_keys=True)
|
||||
|
||||
def write_debug(self) -> None:
|
||||
|
|
@ -64,65 +63,24 @@ class ConfigurationOutput:
|
|||
"""
|
||||
Render a concise two-column summary of the current configuration.
|
||||
|
||||
The left column holds section labels, the right column holds values.
|
||||
Column width adapts to the longest translated label so translations
|
||||
do not break the alignment. Rows whose underlying config is not set
|
||||
are skipped.
|
||||
|
||||
Returns an empty string if nothing meaningful to show.
|
||||
"""
|
||||
rows: list[tuple[str, str]] = []
|
||||
cfg: dict[str, str | list[str] | bool] = {}
|
||||
|
||||
disk_config = self._config.disk_config
|
||||
if disk_config and disk_config.device_modifications:
|
||||
disk_parts: list[str] = []
|
||||
for mod in disk_config.device_modifications:
|
||||
path = str(mod.device_path)
|
||||
root_part = mod.get_root_partition()
|
||||
flags: list[str] = []
|
||||
if root_part and root_part.fs_type:
|
||||
flags.append(root_part.fs_type.value)
|
||||
if disk_config.disk_encryption:
|
||||
flags.append(tr('LUKS'))
|
||||
disk_parts.append(f'{path} ({" + ".join(flags)})' if flags else path)
|
||||
rows.append((tr('Disks'), ', '.join(disk_parts)))
|
||||
for key, value in self._config.plain_cfg().items():
|
||||
cfg[key.text()] = value
|
||||
|
||||
bl_config = self._config.bootloader_config
|
||||
if bl_config and bl_config.bootloader != Bootloader.NO_BOOTLOADER:
|
||||
rows.append((tr('Bootloader'), bl_config.bootloader.value))
|
||||
for config_type, obj in self._config.sub_cfg().items():
|
||||
if not hasattr(obj, 'summary'):
|
||||
continue
|
||||
|
||||
kernels = self._config.kernels
|
||||
if kernels:
|
||||
rows.append((tr('Kernel'), ', '.join(kernels)))
|
||||
summary = obj.summary()
|
||||
if summary:
|
||||
cfg[config_type.text()] = summary
|
||||
|
||||
profile_config = self._config.profile_config
|
||||
if profile_config and profile_config.profile:
|
||||
names = profile_config.profile.current_selection_names()
|
||||
rows.append((tr('Profile'), ', '.join(names) if names else profile_config.profile.name))
|
||||
if profile_config.greeter:
|
||||
rows.append((tr('Greeter'), profile_config.greeter.value))
|
||||
simple_summary = as_key_value_pair(cfg, ignore_empty=True)
|
||||
|
||||
packages = self._config.packages
|
||||
if packages:
|
||||
rows.append((tr('Packages'), str(len(packages))))
|
||||
|
||||
net_config = self._config.network_config
|
||||
if isinstance(net_config, NetworkConfiguration):
|
||||
rows.append((tr('Network'), net_config.type.display_msg()))
|
||||
|
||||
locale_config = self._config.locale_config
|
||||
if locale_config:
|
||||
rows.append((tr('Locale'), locale_config.sys_lang))
|
||||
|
||||
tz = self._config.timezone
|
||||
if tz:
|
||||
rows.append((tr('Timezone'), tz))
|
||||
|
||||
if not rows:
|
||||
return ''
|
||||
|
||||
label_width = max(len(label) for label, _ in rows) + 2
|
||||
return '\n'.join(f'{label:<{label_width}}{value}' for label, value in rows)
|
||||
return simple_summary
|
||||
|
||||
async def confirm_config(self, show_install_warnings: bool = False) -> bool:
|
||||
header = f'{tr("The specified configuration will be applied")}. '
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class Selection[ValueT]:
|
|||
preview_location: Literal['right', 'bottom'] | None = None,
|
||||
multi: bool = False,
|
||||
enable_filter: bool = False,
|
||||
wrap_preview: bool = False,
|
||||
):
|
||||
self._header = header
|
||||
self._title = title
|
||||
|
|
@ -29,6 +30,7 @@ class Selection[ValueT]:
|
|||
self._preview_location = preview_location
|
||||
self._multi = multi
|
||||
self._enable_filter = enable_filter
|
||||
self._wrap_preview = wrap_preview
|
||||
|
||||
async def show(self) -> Result[ValueT]:
|
||||
if self._multi:
|
||||
|
|
@ -39,6 +41,7 @@ class Selection[ValueT]:
|
|||
allow_reset=self._allow_reset,
|
||||
preview_location=self._preview_location,
|
||||
enable_filter=self._enable_filter,
|
||||
wrap_preview=self._wrap_preview,
|
||||
).run()
|
||||
else:
|
||||
result = await OptionListScreen[ValueT](
|
||||
|
|
@ -49,6 +52,7 @@ class Selection[ValueT]:
|
|||
allow_reset=self._allow_reset,
|
||||
preview_location=self._preview_location,
|
||||
enable_filter=self._enable_filter,
|
||||
wrap_preview=self._wrap_preview,
|
||||
).run()
|
||||
|
||||
if result.type_ == ResultType.Reset:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import StrEnum, auto
|
||||
from typing import Any, NotRequired, Self, TypedDict
|
||||
from typing import Any, NotRequired, Self, TypedDict, override
|
||||
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
|
|
@ -168,7 +169,7 @@ class FontsConfiguration:
|
|||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZramConfiguration:
|
||||
class ZramConfiguration(SubConfig):
|
||||
enabled: bool
|
||||
algorithm: ZramAlgorithm = ZramAlgorithm.ZSTD
|
||||
|
||||
|
|
@ -181,9 +182,28 @@ class ZramConfiguration:
|
|||
algo = arg.get('algorithm', arg.get('algo', ZramAlgorithm.ZSTD.value))
|
||||
return cls(enabled=enabled, algorithm=ZramAlgorithm(algo))
|
||||
|
||||
@override
|
||||
def json(self) -> dict[str, bool | str]:
|
||||
return {
|
||||
'enabled': self.enabled,
|
||||
'algorithm': self.algorithm.value,
|
||||
}
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str] | None:
|
||||
out: list[str] = []
|
||||
|
||||
if self.enabled:
|
||||
out.append(tr('Zram enabled'))
|
||||
|
||||
out.append(tr('Zram algorithm {}').format(self.algorithm))
|
||||
return out
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplicationConfiguration:
|
||||
class ApplicationConfiguration(SubConfig):
|
||||
bluetooth_config: BluetoothConfiguration | None = None
|
||||
audio_config: AudioConfiguration | None = None
|
||||
power_management_config: PowerManagementConfiguration | None = None
|
||||
|
|
@ -223,6 +243,7 @@ class ApplicationConfiguration:
|
|||
|
||||
return app_config
|
||||
|
||||
@override
|
||||
def json(self) -> ApplicationSerialization:
|
||||
config: ApplicationSerialization = {}
|
||||
|
||||
|
|
@ -245,3 +266,28 @@ class ApplicationConfiguration:
|
|||
config['fonts_config'] = self.fonts_config.json()
|
||||
|
||||
return config
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str]:
|
||||
out: list[str] = []
|
||||
|
||||
if self.bluetooth_config and self.bluetooth_config.enabled:
|
||||
out.append(tr('Bluetooth enabled'))
|
||||
|
||||
if self.audio_config:
|
||||
out.append(tr('Audio server "{}"').format(self.audio_config.audio))
|
||||
|
||||
if self.power_management_config:
|
||||
out.append(tr('Power management "{}"').format(self.power_management_config.power_management))
|
||||
|
||||
if self.print_service_config and self.print_service_config.enabled:
|
||||
out.append(tr('Print service enabled'))
|
||||
|
||||
if self.firewall_config:
|
||||
out.append(tr('Firewall "{}"').format(self.firewall_config.firewall))
|
||||
|
||||
if self.fonts_config and self.fonts_config.fonts:
|
||||
fonts = ', '.join(f.value for f in self.fonts_config.fonts)
|
||||
out.append(tr('Extra fonts "{}"').format(fonts))
|
||||
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, NotRequired, Self, TypedDict
|
||||
from typing import Any, NotRequired, Self, TypedDict, override
|
||||
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.models.users import Password, User
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ class U2FLoginConfiguration:
|
|||
|
||||
|
||||
@dataclass
|
||||
class AuthenticationConfiguration:
|
||||
class AuthenticationConfiguration(SubConfig):
|
||||
root_enc_password: Password | None = None
|
||||
users: list[User] = field(default_factory=list)
|
||||
u2f_config: U2FLoginConfiguration | None = None
|
||||
|
|
@ -75,6 +76,7 @@ class AuthenticationConfiguration:
|
|||
|
||||
return auth_config
|
||||
|
||||
@override
|
||||
def json(self) -> AuthenticationSerialization:
|
||||
config: AuthenticationSerialization = {}
|
||||
|
||||
|
|
@ -83,6 +85,21 @@ class AuthenticationConfiguration:
|
|||
|
||||
return config
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str]:
|
||||
out: list[str] = []
|
||||
|
||||
if self.root_enc_password:
|
||||
out.append(tr('Root password set'))
|
||||
|
||||
if self.users:
|
||||
out.append(tr('Configured {} user(s)').format(len(self.users)))
|
||||
|
||||
if self.u2f_config:
|
||||
out.append(tr('U2F set up'))
|
||||
|
||||
return out
|
||||
|
||||
def has_superuser(self) -> bool:
|
||||
return any(u.sudo for u in self.users)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import sys
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Self
|
||||
from typing import Any, Self, override
|
||||
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.output import warn
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
|
@ -60,14 +61,26 @@ class Bootloader(Enum):
|
|||
|
||||
|
||||
@dataclass
|
||||
class BootloaderConfiguration:
|
||||
class BootloaderConfiguration(SubConfig):
|
||||
bootloader: Bootloader
|
||||
uki: bool = False
|
||||
removable: bool = True
|
||||
|
||||
@override
|
||||
def json(self) -> dict[str, Any]:
|
||||
return {'bootloader': self.bootloader.json(), 'uki': self.uki, 'removable': self.removable}
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str]:
|
||||
out = [tr('Bootloader "{}"').format(self.bootloader.value)]
|
||||
|
||||
if self.uki:
|
||||
out.append(tr('UKI enabled'))
|
||||
if self.removable:
|
||||
out.append(tr('Removable'))
|
||||
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def parse_arg(cls, config: dict[str, Any], skip_boot: bool) -> Self:
|
||||
bootloader = Bootloader.from_arg(config.get('bootloader', ''), skip_boot)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SubConfig(ABC):
|
||||
@abstractmethod
|
||||
def json(self) -> Any:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def summary(self) -> str | list[str] | None:
|
||||
pass
|
||||
|
|
@ -12,6 +12,7 @@ from parted import Disk, Geometry, Partition
|
|||
from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator
|
||||
|
||||
from archinstall.lib.hardware import SysInfo
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.models.users import Password
|
||||
from archinstall.lib.output import debug
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
|
@ -34,6 +35,15 @@ class DiskLayoutType(Enum):
|
|||
case DiskLayoutType.Pre_mount:
|
||||
return tr('Pre-mounted configuration')
|
||||
|
||||
def short_msg(self) -> str:
|
||||
match self:
|
||||
case DiskLayoutType.Default:
|
||||
return tr('Default')
|
||||
case DiskLayoutType.Manual:
|
||||
return tr('Manual')
|
||||
case DiskLayoutType.Pre_mount:
|
||||
return tr('Pre-mount')
|
||||
|
||||
|
||||
class _DiskLayoutConfigurationSerialization(TypedDict):
|
||||
config_type: str
|
||||
|
|
@ -45,7 +55,7 @@ class _DiskLayoutConfigurationSerialization(TypedDict):
|
|||
|
||||
|
||||
@dataclass
|
||||
class DiskLayoutConfiguration:
|
||||
class DiskLayoutConfiguration(SubConfig):
|
||||
config_type: DiskLayoutType
|
||||
device_modifications: list[DeviceModification] = field(default_factory=list)
|
||||
lvm_config: LvmConfiguration | None = None
|
||||
|
|
@ -55,6 +65,7 @@ class DiskLayoutConfiguration:
|
|||
# used for pre-mounted config
|
||||
mountpoint: Path | None = None
|
||||
|
||||
@override
|
||||
def json(self) -> _DiskLayoutConfigurationSerialization:
|
||||
if self.config_type == DiskLayoutType.Pre_mount:
|
||||
return {
|
||||
|
|
@ -78,6 +89,28 @@ class DiskLayoutConfiguration:
|
|||
|
||||
return config
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str]:
|
||||
out = [tr('{} layout').format(self.config_type.short_msg())]
|
||||
|
||||
devices = set(mod.device_path for mod in self.device_modifications)
|
||||
|
||||
if devices:
|
||||
dev_str = ', '.join(str(d) for d in devices)
|
||||
out.append(tr('Devices {}').format(dev_str))
|
||||
|
||||
if self.lvm_config is not None:
|
||||
out.append(tr('LVM set up'))
|
||||
|
||||
if self.disk_encryption is not None:
|
||||
out.append(tr('{} encryption').format(self.disk_encryption.encryption_type.type_to_text()))
|
||||
|
||||
if self.btrfs_options is not None:
|
||||
if self.btrfs_options.snapshot_config:
|
||||
out.append(tr('Btrfs snapshot "{}"').format(self.btrfs_options.snapshot_config.snapshot_type))
|
||||
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def parse_arg(
|
||||
cls,
|
||||
|
|
@ -1321,7 +1354,7 @@ class _SnapshotConfigSerialization(TypedDict):
|
|||
type: str
|
||||
|
||||
|
||||
class SnapshotType(Enum):
|
||||
class SnapshotType(StrEnum):
|
||||
Snapper = 'Snapper'
|
||||
Timeshift = 'Timeshift'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Any, Self
|
||||
from typing import Any, Self, override
|
||||
|
||||
from archinstall.lib.locale.utils import get_kb_layout
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocaleConfiguration:
|
||||
class LocaleConfiguration(SubConfig):
|
||||
kb_layout: str
|
||||
sys_lang: str
|
||||
sys_enc: str
|
||||
|
|
@ -23,6 +24,7 @@ class LocaleConfiguration:
|
|||
layout = 'us'
|
||||
return cls(layout, 'en_US.UTF-8', 'UTF-8')
|
||||
|
||||
@override
|
||||
def json(self) -> dict[str, str]:
|
||||
return {
|
||||
'kb_layout': self.kb_layout,
|
||||
|
|
@ -31,6 +33,15 @@ class LocaleConfiguration:
|
|||
'console_font': self.console_font,
|
||||
}
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str]:
|
||||
return [
|
||||
tr('Keyboard layout "{}"').format(self.kb_layout),
|
||||
tr('Locale language "{}"').format(self.sys_lang),
|
||||
tr('Locale encoding "{}"').format(self.sys_enc),
|
||||
tr('Console font "{}"').format(self.console_font),
|
||||
]
|
||||
|
||||
def preview(self) -> str:
|
||||
output = '{}: {}\n'.format(tr('Keyboard layout'), self.kb_layout)
|
||||
output += '{}: {}\n'.format(tr('Locale language'), self.sys_lang)
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ from typing import TYPE_CHECKING, Any, Self, TypedDict, override
|
|||
|
||||
from pydantic import BaseModel, ValidationInfo, field_validator, model_validator
|
||||
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.models.packages import Repository
|
||||
from archinstall.lib.networking import DownloadTimer, ping
|
||||
from archinstall.lib.output import debug
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
|
||||
|
|
@ -236,7 +238,7 @@ class _MirrorConfigurationSerialization(TypedDict):
|
|||
|
||||
|
||||
@dataclass
|
||||
class MirrorConfiguration:
|
||||
class MirrorConfiguration(SubConfig):
|
||||
mirror_regions: list[MirrorRegion] = field(default_factory=list)
|
||||
custom_servers: list[CustomServer] = field(default_factory=list)
|
||||
optional_repositories: list[Repository] = field(default_factory=list)
|
||||
|
|
@ -250,6 +252,7 @@ class MirrorConfiguration:
|
|||
def custom_server_urls(self) -> str:
|
||||
return '\n'.join(s.url for s in self.custom_servers)
|
||||
|
||||
@override
|
||||
def json(self) -> _MirrorConfigurationSerialization:
|
||||
regions = {}
|
||||
for m in self.mirror_regions:
|
||||
|
|
@ -262,6 +265,24 @@ class MirrorConfiguration:
|
|||
'custom_repositories': [c.json() for c in self.custom_repositories],
|
||||
}
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str]:
|
||||
out: list[str] = []
|
||||
|
||||
if self.mirror_regions:
|
||||
out.append(tr('Mirror regions "{}"').format(', '.join(m.name for m in self.mirror_regions)))
|
||||
|
||||
if self.optional_repositories:
|
||||
out.append(tr('Optional repositories "{}"').format(', '.join(r.value for r in self.optional_repositories)))
|
||||
|
||||
if self.custom_servers:
|
||||
out.append(tr('Custom servers set up'))
|
||||
|
||||
if self.custom_repositories:
|
||||
out.append(tr('Custom repositories set up'))
|
||||
|
||||
return out
|
||||
|
||||
def custom_servers_config(self) -> str:
|
||||
config = ''
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from dataclasses import dataclass, field
|
|||
from enum import Enum
|
||||
from typing import NotRequired, Self, TypedDict, override
|
||||
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.output import debug
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
|
@ -103,10 +104,11 @@ class _NetworkConfigurationSerialization(TypedDict):
|
|||
|
||||
|
||||
@dataclass
|
||||
class NetworkConfiguration:
|
||||
class NetworkConfiguration(SubConfig):
|
||||
type: NicType
|
||||
nics: list[Nic] = field(default_factory=list)
|
||||
|
||||
@override
|
||||
def json(self) -> _NetworkConfigurationSerialization:
|
||||
config: _NetworkConfigurationSerialization = {'type': self.type.value}
|
||||
if self.nics:
|
||||
|
|
@ -114,6 +116,10 @@ class NetworkConfiguration:
|
|||
|
||||
return config
|
||||
|
||||
@override
|
||||
def summary(self) -> str:
|
||||
return self.type.display_msg()
|
||||
|
||||
@classmethod
|
||||
def parse_arg(cls, config: _NetworkConfigurationSerialization) -> Self | None:
|
||||
nic_type = config.get('type', None)
|
||||
|
|
|
|||
|
|
@ -132,7 +132,6 @@ class AvailablePackage(BaseModel):
|
|||
def longest_key(self) -> int:
|
||||
return max(len(key) for key in self.model_dump().keys())
|
||||
|
||||
# return all package info line by line
|
||||
def info(self) -> str:
|
||||
output = ''
|
||||
for key, value in self.model_dump().items():
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Self, TypedDict
|
||||
from typing import Self, TypedDict, override
|
||||
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ class PacmanConfigSerialization(TypedDict):
|
|||
|
||||
|
||||
@dataclass
|
||||
class PacmanConfiguration:
|
||||
class PacmanConfiguration(SubConfig):
|
||||
parallel_downloads: int = 5
|
||||
color: bool = True
|
||||
|
||||
|
|
@ -18,12 +19,19 @@ class PacmanConfiguration:
|
|||
def default(cls) -> Self:
|
||||
return cls()
|
||||
|
||||
@override
|
||||
def json(self) -> PacmanConfigSerialization:
|
||||
return {
|
||||
'parallel_downloads': self.parallel_downloads,
|
||||
'color': self.color,
|
||||
}
|
||||
|
||||
@override
|
||||
def summary(self) -> str | None:
|
||||
if self.color:
|
||||
return tr('Color enabled')
|
||||
return None
|
||||
|
||||
def preview(self) -> str:
|
||||
color_str = str(self.color)
|
||||
output = '{}: {}\n'.format(tr('Parallel Downloads'), self.parallel_downloads)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Self, TypedDict
|
||||
from typing import TYPE_CHECKING, Self, TypedDict, override
|
||||
|
||||
from archinstall.default_profiles.profile import GreeterType, Profile
|
||||
from archinstall.lib.hardware import GfxDriver
|
||||
from archinstall.lib.models.config import SubConfig
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.profile.profiles_handler import ProfileSerialization
|
||||
|
|
@ -15,11 +17,12 @@ class _ProfileConfigurationSerialization(TypedDict):
|
|||
|
||||
|
||||
@dataclass
|
||||
class ProfileConfiguration:
|
||||
class ProfileConfiguration(SubConfig):
|
||||
profile: Profile | None = None
|
||||
gfx_driver: GfxDriver | None = None
|
||||
greeter: GreeterType | None = None
|
||||
|
||||
@override
|
||||
def json(self) -> _ProfileConfigurationSerialization:
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
|
||||
|
|
@ -29,6 +32,23 @@ class ProfileConfiguration:
|
|||
'greeter': self.greeter.value if self.greeter else None,
|
||||
}
|
||||
|
||||
@override
|
||||
def summary(self) -> list[str] | None:
|
||||
out: list[str] = []
|
||||
|
||||
if self.profile:
|
||||
out.append(self.profile.name)
|
||||
|
||||
if self.gfx_driver:
|
||||
out.append(tr('{} grphics driver').format(self.gfx_driver.value))
|
||||
|
||||
if self.greeter:
|
||||
out.append(tr('{} greeter').format(self.greeter.value))
|
||||
|
||||
return out
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def parse_arg(cls, arg: _ProfileConfigurationSerialization) -> Self:
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from collections.abc import Callable
|
||||
from dataclasses import asdict, is_dataclass
|
||||
from datetime import UTC, datetime
|
||||
|
|
@ -9,6 +11,7 @@ from pathlib import Path
|
|||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from archinstall.lib.utils.encoding import unicode_ljust, unicode_rjust
|
||||
from archinstall.tui.rich import BaseRichTable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import DataclassInstance
|
||||
|
|
@ -18,7 +21,7 @@ class FormattedOutput:
|
|||
@staticmethod
|
||||
def _get_values(
|
||||
o: DataclassInstance,
|
||||
class_formatter: str | Callable | None = None, # type: ignore[type-arg]
|
||||
class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]
|
||||
filter_list: list[str] = [],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
|
|
@ -127,6 +130,35 @@ class FormattedOutput:
|
|||
return output
|
||||
|
||||
|
||||
def as_key_value_pair(
|
||||
entries: dict[str, str | list[str] | bool],
|
||||
ignore_empty: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Formats key-values as a Rich Table:
|
||||
key1 : value1
|
||||
key2 : value2
|
||||
...
|
||||
"""
|
||||
table = BaseRichTable()
|
||||
table.add_column('key', style='bold', no_wrap=True)
|
||||
table.add_column('value', style='white', max_width=70)
|
||||
|
||||
for label, value in entries.items():
|
||||
if ignore_empty and not value:
|
||||
continue
|
||||
|
||||
if isinstance(value, bool):
|
||||
value = 'Yes' if value else 'No'
|
||||
|
||||
if isinstance(value, list):
|
||||
value = '\n '.join(str(val) for val in value)
|
||||
|
||||
table.add_row(label.title(), f': {value}')
|
||||
|
||||
return table.stringify()
|
||||
|
||||
|
||||
class Journald:
|
||||
@staticmethod
|
||||
def log(message: str, level: int = logging.DEBUG) -> None:
|
||||
|
|
@ -333,3 +365,51 @@ def log(
|
|||
|
||||
if level != logging.DEBUG:
|
||||
print(text)
|
||||
|
||||
|
||||
def share_install_log(
|
||||
paste_url: str = 'https://paste.rs',
|
||||
max_size: int = 10 * 1024 * 1024,
|
||||
confirm: Callable[[str], bool] = lambda _: True,
|
||||
) -> int:
|
||||
log_path = logger.path
|
||||
|
||||
if not log_path.exists():
|
||||
info(f'Log file not found: {log_path}')
|
||||
return 1
|
||||
|
||||
size = log_path.stat().st_size
|
||||
if size == 0:
|
||||
info(f'Log file is empty: {log_path}')
|
||||
return 1
|
||||
|
||||
if size > max_size:
|
||||
info(f'Log file exceeds {max_size} bytes, uploading last {max_size} bytes')
|
||||
content = log_path.read_bytes()[-max_size:]
|
||||
else:
|
||||
content = log_path.read_bytes()
|
||||
|
||||
header = f'About to upload {log_path} ({len(content)} bytes) to {paste_url}\n\n'
|
||||
header += 'The log may contain hostname, mirror URLs, package list and partition layout.\n'
|
||||
header += 'The uploaded paste is public.\n\n'
|
||||
header += 'Continue?'
|
||||
|
||||
if not confirm(header):
|
||||
info('Cancelled.')
|
||||
return 1
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(paste_url, data=content)
|
||||
with urllib.request.urlopen(req) as response:
|
||||
url = response.read().decode().strip()
|
||||
except urllib.error.URLError as e:
|
||||
info(f'Upload failed: {e}')
|
||||
return 1
|
||||
|
||||
if not url.startswith('http'):
|
||||
info(f'Unexpected response from {paste_url}: {url[:200]!r}')
|
||||
return 1
|
||||
|
||||
# raw print so the URL is pipe-friendly (no ANSI colors, no log prefix)
|
||||
print(url)
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ def installed_package(package: str) -> LocalPackage | None:
|
|||
try:
|
||||
package_info = []
|
||||
for line in Pacman.run(f'-Q --info {package}'):
|
||||
package_info.append(line.decode().strip())
|
||||
package_info.append(line.decode().rstrip())
|
||||
|
||||
return _parse_package_output(package_info, LocalPackage)
|
||||
except SysCallError:
|
||||
|
|
@ -53,7 +53,7 @@ def available_package(package: str) -> AvailablePackage | None:
|
|||
try:
|
||||
package_info: list[str] = []
|
||||
for line in Pacman.run(f'-S --info {package}'):
|
||||
package_info.append(line.decode().strip())
|
||||
package_info.append(line.decode().rstrip())
|
||||
|
||||
return _parse_package_output(package_info, AvailablePackage)
|
||||
except SysCallError:
|
||||
|
|
@ -79,7 +79,7 @@ def list_available_packages(
|
|||
debug(f'Failed to sync Arch Linux package database: {e}')
|
||||
|
||||
for line in Pacman.run('-S --info'):
|
||||
dec_line = line.decode().strip()
|
||||
dec_line = line.decode().rstrip()
|
||||
current_package.append(dec_line)
|
||||
|
||||
if dec_line.startswith('Validated'):
|
||||
|
|
@ -187,6 +187,7 @@ async def select_additional_packages(
|
|||
multi=True,
|
||||
preview_location='right',
|
||||
enable_filter=True,
|
||||
wrap_preview=True,
|
||||
).show()
|
||||
|
||||
match pck_result.type_:
|
||||
|
|
|
|||
|
|
@ -2293,3 +2293,14 @@ msgstr ""
|
|||
#, python-brace-format
|
||||
msgid "Default: {}ms, Recommended range: 1000-60000"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{} needs access to your seat"
|
||||
msgstr ""
|
||||
|
||||
msgid "collection of hardware devices i.e. keyboard, mouse"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Choose an option how to give {} access to your hardware"
|
||||
msgstr ""
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -256,6 +256,9 @@ msgstr "Język"
|
|||
msgid "Locale encoding"
|
||||
msgstr "Kodowanie"
|
||||
|
||||
msgid "Console font"
|
||||
msgstr "Czcionka konsoli"
|
||||
|
||||
msgid "Drive(s)"
|
||||
msgstr "Dysk(i)"
|
||||
|
||||
|
|
@ -820,6 +823,19 @@ msgstr "Nieprawidłowa wartość! Spróbuj jeszcze raz z prawidłową wartości
|
|||
msgid "Parallel Downloads"
|
||||
msgstr "Pobieranie kilku plików jednocześnie"
|
||||
|
||||
msgid "Pacman"
|
||||
msgstr "Pacman"
|
||||
|
||||
msgid "Color"
|
||||
msgstr "Kolor"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Enter the number of parallel downloads (1-{})"
|
||||
msgstr "Wprowadź maksymalną liczbę plików pobieranych jednocześnie (1-{})"
|
||||
|
||||
msgid "Enable colored output for pacman"
|
||||
msgstr "Włącz kolory w pacmanie"
|
||||
|
||||
msgid "ESC to skip"
|
||||
msgstr "Naciśnij ESC, aby pominąć"
|
||||
|
||||
|
|
@ -1203,6 +1219,21 @@ msgstr "Produkt"
|
|||
msgid "Invalid configuration: {error}"
|
||||
msgstr "Niepoprawna konfiguracja: {error}"
|
||||
|
||||
msgid "Ready to install"
|
||||
msgstr "Gotowe do zainstalowania"
|
||||
|
||||
msgid "Disks"
|
||||
msgstr "Dyski"
|
||||
|
||||
msgid "Packages"
|
||||
msgstr "Pakiety"
|
||||
|
||||
msgid "Network"
|
||||
msgstr "Sieć"
|
||||
|
||||
msgid "Locale"
|
||||
msgstr "Ustawienia regionalne (locale)"
|
||||
|
||||
msgid "Type"
|
||||
msgstr "Typ"
|
||||
|
||||
|
|
@ -1660,6 +1691,123 @@ msgstr "Wejdź w tryb wyszukiwania"
|
|||
msgid "Exit search mode"
|
||||
msgstr "Wyjdź z trybu wyszukiwania"
|
||||
|
||||
msgid "General"
|
||||
msgstr "Ogólne"
|
||||
|
||||
msgid "Navigation"
|
||||
msgstr "Nawigacja"
|
||||
|
||||
msgid "Selection"
|
||||
msgstr "Wybór"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Szukaj"
|
||||
|
||||
msgid "Down"
|
||||
msgstr "W dół"
|
||||
|
||||
msgid "Up"
|
||||
msgstr "W górę"
|
||||
|
||||
msgid "Confirm"
|
||||
msgstr "Potwierdź"
|
||||
|
||||
msgid "Focus right"
|
||||
msgstr "W prawo"
|
||||
|
||||
msgid "Focus left"
|
||||
msgstr "W lewo"
|
||||
|
||||
msgid "Toggle"
|
||||
msgstr "Zaznacz/Odznacz"
|
||||
|
||||
msgid "Show/Hide help"
|
||||
msgstr "Pokaż/Ukryj pomoc"
|
||||
|
||||
msgid "Quit"
|
||||
msgstr "Wyjdź"
|
||||
|
||||
msgid "First"
|
||||
msgstr "Pierwszy"
|
||||
|
||||
msgid "Last"
|
||||
msgstr "Ostatni"
|
||||
|
||||
msgid "Select"
|
||||
msgstr "Wybierz"
|
||||
|
||||
msgid "Page Up"
|
||||
msgstr "Strona w górę"
|
||||
|
||||
msgid "Page Down"
|
||||
msgstr "Strona w dół"
|
||||
|
||||
msgid "Page up"
|
||||
msgstr "Strona w górę"
|
||||
|
||||
msgid "Page down"
|
||||
msgstr "Strona w dół"
|
||||
|
||||
msgid "Page Left"
|
||||
msgstr "Strona w lewo"
|
||||
|
||||
msgid "Page Right"
|
||||
msgstr "Strona w prawo"
|
||||
|
||||
msgid "Cursor up"
|
||||
msgstr "Kursor w górę"
|
||||
|
||||
msgid "Cursor down"
|
||||
msgstr "Kursor w dół"
|
||||
|
||||
msgid "Cursor right"
|
||||
msgstr "Kursor w prawo"
|
||||
|
||||
msgid "Cursor left"
|
||||
msgstr "Kursor w lewo"
|
||||
|
||||
msgid "Top"
|
||||
msgstr "Góra"
|
||||
|
||||
msgid "Bottom"
|
||||
msgstr "Dół"
|
||||
|
||||
msgid "Home"
|
||||
msgstr "Początek"
|
||||
|
||||
msgid "End"
|
||||
msgstr "Koniec"
|
||||
|
||||
msgid "Toggle option"
|
||||
msgstr "Zaznacz/Odznacz opcję"
|
||||
|
||||
msgid "Scroll Up"
|
||||
msgstr "Przewiń w górę"
|
||||
|
||||
msgid "Scroll Down"
|
||||
msgstr "Przewiń w dół"
|
||||
|
||||
msgid "Scroll Left"
|
||||
msgstr "Przewiń w lewo"
|
||||
|
||||
msgid "Scroll Right"
|
||||
msgstr "Przewiń w prawo"
|
||||
|
||||
msgid "Scroll Home"
|
||||
msgstr "Przewiń do początku"
|
||||
|
||||
msgid "Scroll End"
|
||||
msgstr "Przewiń do końca"
|
||||
|
||||
msgid "Focus Next"
|
||||
msgstr "Następny"
|
||||
|
||||
msgid "Focus Previous"
|
||||
msgstr "Poprzedni"
|
||||
|
||||
msgid "Copy selected text"
|
||||
msgstr "Skopiuj zaznaczony tekst"
|
||||
|
||||
msgid "labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
|
||||
msgstr "labwc potrzebuje dostępu do twojego seat'a (sprzętu, np. klawiatury, myszki)"
|
||||
|
||||
|
|
@ -1878,6 +2026,21 @@ msgstr "Użyj NetworkManager'a (backend iwd)"
|
|||
msgid "Firewall"
|
||||
msgstr "Zapora sieciowa"
|
||||
|
||||
msgid "Additional fonts"
|
||||
msgstr "Dodatkowe czcionki"
|
||||
|
||||
msgid "Select font packages to install"
|
||||
msgstr "Wybierz pakiety czcionek do zainstalowania"
|
||||
|
||||
msgid "Unicode font coverage for most languages"
|
||||
msgstr "Wsparcie Unicode dla większości języków"
|
||||
|
||||
msgid "color emoji for browsers and apps"
|
||||
msgstr "kolorowe emoji dla przeglądarek i aplikacji"
|
||||
|
||||
msgid "Chinese, Japanese, Korean characters"
|
||||
msgstr "Chińskie, Japońskie, Koreańskie znaki"
|
||||
|
||||
msgid "Select audio configuration"
|
||||
msgstr "Wybierz konfigurację audio"
|
||||
|
||||
|
|
@ -1965,7 +2128,13 @@ msgid "Select an interface"
|
|||
msgstr "Wybierz interfejs"
|
||||
|
||||
msgid "Choose network configuration"
|
||||
msgstr "Wybierz konfigurację sieciową"
|
||||
msgstr "Wybierz konfigurację sieci"
|
||||
|
||||
msgid "Recommended: Network Manager for desktop, Manual for server"
|
||||
msgstr "Rekomendowane: Network Manager dla desktopów, ręcznie dla serwerów"
|
||||
|
||||
msgid "Warning: no network configuration selected. Network will need to be set up manually on the installed system."
|
||||
msgstr "Uwaga: nie wybrano konfiguracji sieci. Sieć musi zostać skonfigurowana ręcznie w zainstalowanym systemie."
|
||||
|
||||
msgid "No packages found"
|
||||
msgstr "Nie znaleziono pakietów"
|
||||
|
|
@ -2004,6 +2173,72 @@ msgstr "Wybrany profil pulpitu wymaga logowania normalnego użytkownika przez gr
|
|||
msgid "Environment type: {} {}"
|
||||
msgstr "Typ środowiska: {} {}"
|
||||
|
||||
msgid "Input cannot be empty"
|
||||
msgstr "Podana wartość nie może być pusta"
|
||||
|
||||
msgid "Recommended"
|
||||
msgstr "Rekomendowane"
|
||||
|
||||
msgid "Package"
|
||||
msgstr "Pakiet"
|
||||
|
||||
msgid "Curated selection of KDE Plasma packages"
|
||||
msgstr "Wyselekcjonowany zestaw pakietów KDE Plasma"
|
||||
|
||||
msgid "Dependencies"
|
||||
msgstr "Zależności"
|
||||
|
||||
msgid "Package group"
|
||||
msgstr "Grupa pakietów"
|
||||
|
||||
msgid "Extensive KDE Plasma installation"
|
||||
msgstr "Obszerna instalacja KDE Plasma"
|
||||
|
||||
msgid "Packages in group"
|
||||
msgstr "Pakiety w grupie"
|
||||
|
||||
msgid "Minimal KDE Plasma installation"
|
||||
msgstr "Minimalistyczna instalacja KDE Plasma"
|
||||
|
||||
msgid "Description"
|
||||
msgstr "Opis"
|
||||
|
||||
msgid "Select a flavor of KDE Plasma to install"
|
||||
msgstr "Wybierz rodzaj KDE Plasma do zainstalowania"
|
||||
|
||||
msgid "Arial/Times/Courier replacement, Cyrillic support for Steam/games"
|
||||
msgstr "Zamiennik Arial/Times/Courier, obsługa Cyrylicy dla Steam/gier"
|
||||
|
||||
msgid "wide Unicode coverage, good fallback font"
|
||||
msgstr "szerokie wsparcie Unicode, dobra czczionka zastępcza"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Setting up U2F login: {u2f_config.u2f_login_method.value}"
|
||||
msgstr "Ustawianie logowania U2F {u2f_config.u2f_login_method.value}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000"
|
||||
msgstr "Domyślnie: {DEFAULT_ITER_TIME}ms, Rekomendowany przedział: 1000-60000"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Setting up U2F login: {}"
|
||||
msgstr "Konfigurowanie logowania U2F: {}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Default: {}ms, Recommended range: 1000-60000"
|
||||
msgstr "Domyślnie: {}ms, Rekomendowany przedział: 1000-60000"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{} needs access to your seat"
|
||||
msgstr "{} potrzebuje dostępu do twojego seat'a"
|
||||
|
||||
msgid "collection of hardware devices i.e. keyboard, mouse"
|
||||
msgstr "zbiór urządzeń, t.j. klawiatura, myszka"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Choose an option how to give {} access to your hardware"
|
||||
msgstr "Wybierz opcję, aby nadać {} dostęp do twojego sprzętu"
|
||||
|
||||
#~ msgid "When picking a directory to save configuration files to, by default we will ignore the following folders: "
|
||||
#~ msgstr "Podczas wybierania katalogu do zapisywania plików konfiguracyjnych, domyślnie ignorowane są następujące foldery: "
|
||||
|
||||
|
|
|
|||
|
|
@ -11,14 +11,16 @@ from pathlib import Path
|
|||
from archinstall.lib.args import ArchConfigHandler
|
||||
from archinstall.lib.disk.utils import disk_layouts
|
||||
from archinstall.lib.hardware import SysInfo
|
||||
from archinstall.lib.menu.helpers import Confirmation
|
||||
from archinstall.lib.network.wifi_handler import WifiHandler
|
||||
from archinstall.lib.networking import ping
|
||||
from archinstall.lib.output import debug, error, info, warn
|
||||
from archinstall.lib.output import debug, error, info, share_install_log, warn
|
||||
from archinstall.lib.packages.util import check_version_upgrade
|
||||
from archinstall.lib.pacman.pacman import Pacman
|
||||
from archinstall.lib.translationhandler import tr, translation_handler
|
||||
from archinstall.lib.utils.util import running_from_iso
|
||||
from archinstall.tui.components import tui
|
||||
from archinstall.tui.menu_item import MenuItemGroup
|
||||
|
||||
|
||||
def _log_sys_info() -> None:
|
||||
|
|
@ -73,12 +75,28 @@ def _list_scripts() -> str:
|
|||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def _tui_confirm(header: str) -> bool:
|
||||
async def _ask() -> bool:
|
||||
result = await Confirmation(
|
||||
group=MenuItemGroup.yes_no(),
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
preset=False,
|
||||
).show()
|
||||
return result.get_value()
|
||||
|
||||
return tui.run(_ask)
|
||||
|
||||
|
||||
def run() -> int:
|
||||
"""
|
||||
This can either be run as the compiled and installed application: python setup.py install
|
||||
OR straight as a module: python -m archinstall
|
||||
In any case we will be attempting to load the provided script to be run from the scripts/ folder
|
||||
"""
|
||||
if 'share-log' in sys.argv:
|
||||
return share_install_log(confirm=_tui_confirm)
|
||||
|
||||
arch_config_handler = ArchConfigHandler()
|
||||
|
||||
if '--help' in sys.argv or '-h' in sys.argv:
|
||||
|
|
@ -141,8 +159,8 @@ def _error_message(exc: Exception) -> None:
|
|||
Archinstall experienced the above error. If you think this is a bug, please report it to
|
||||
https://github.com/archlinux/archinstall and include the log file "/var/log/archinstall/install.log".
|
||||
|
||||
Hint: To extract the log from a live ISO
|
||||
curl -F 'file=@/var/log/archinstall/install.log' https://0x0.st
|
||||
Hint: To upload the log and get a shareable URL, run
|
||||
archinstall share-log
|
||||
"""
|
||||
)
|
||||
warn(text)
|
||||
|
|
|
|||
|
|
@ -200,6 +200,11 @@ class OptionListScreen(BaseScreen[ValueT]):
|
|||
color: white;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
.wrap-preview {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -211,6 +216,7 @@ class OptionListScreen(BaseScreen[ValueT]):
|
|||
allow_reset: bool = False,
|
||||
preview_location: Literal['right', 'bottom'] | None = None,
|
||||
enable_filter: bool = False,
|
||||
wrap_preview: bool = False,
|
||||
):
|
||||
super().__init__(allow_skip, allow_reset)
|
||||
self._group = group
|
||||
|
|
@ -218,6 +224,7 @@ class OptionListScreen(BaseScreen[ValueT]):
|
|||
self._title = title
|
||||
self._preview_location = preview_location
|
||||
self._filter = enable_filter
|
||||
self._wrap_preview = wrap_preview
|
||||
self._show_frame = False
|
||||
|
||||
self._options = self._get_options()
|
||||
|
|
@ -280,7 +287,10 @@ class OptionListScreen(BaseScreen[ValueT]):
|
|||
with Container():
|
||||
yield option_list
|
||||
yield Rule(orientation=rule_orientation)
|
||||
yield ScrollableContainer(Label('', id='preview_content', markup=False))
|
||||
preview_label = Label('', id='preview_content', markup=False)
|
||||
if self._wrap_preview:
|
||||
preview_label.add_class('wrap-preview')
|
||||
yield ScrollableContainer(preview_label)
|
||||
|
||||
if self._filter:
|
||||
yield Input(placeholder='/filter', id='filter-input')
|
||||
|
|
@ -340,11 +350,6 @@ class OptionListScreen(BaseScreen[ValueT]):
|
|||
]
|
||||
)
|
||||
|
||||
# debug(f'Index: {index}')
|
||||
# debug(f'Region: {option_list.region}')
|
||||
# debug(f'Scroll offset: {option_list.scroll_offset}')
|
||||
# debug(f'Target_Y: {target_y}')
|
||||
|
||||
self.app.cursor_position = Offset(option_list.region.x, target_y)
|
||||
self.app.refresh()
|
||||
|
||||
|
|
@ -433,6 +438,11 @@ class SelectListScreen(BaseScreen[ValueT]):
|
|||
color: white;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
.wrap-preview {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -443,6 +453,7 @@ class SelectListScreen(BaseScreen[ValueT]):
|
|||
allow_reset: bool = False,
|
||||
preview_location: Literal['right', 'bottom'] | None = None,
|
||||
enable_filter: bool = False,
|
||||
wrap_preview: bool = False,
|
||||
):
|
||||
super().__init__(allow_skip, allow_reset)
|
||||
self._group = group
|
||||
|
|
@ -450,6 +461,7 @@ class SelectListScreen(BaseScreen[ValueT]):
|
|||
self._preview_location = preview_location
|
||||
self._show_frame = False
|
||||
self._filter = enable_filter
|
||||
self._wrap_preview = wrap_preview
|
||||
|
||||
self._selected_items: list[MenuItem] = self._group.selected_items
|
||||
self._options: list[Selection[MenuItem]] = self._get_selections()
|
||||
|
|
@ -510,7 +522,10 @@ class SelectListScreen(BaseScreen[ValueT]):
|
|||
with Container():
|
||||
yield selection_list
|
||||
yield Rule(orientation=rule_orientation)
|
||||
yield ScrollableContainer(Label('', id='preview_content', markup=False))
|
||||
preview_label = Label('', id='preview_content', markup=False)
|
||||
if self._wrap_preview:
|
||||
preview_label.add_class('wrap-preview')
|
||||
yield ScrollableContainer(preview_label)
|
||||
|
||||
if self._filter:
|
||||
yield Input(placeholder='/filter', id='filter-input')
|
||||
|
|
@ -1143,8 +1158,6 @@ class TableSelectionScreen(BaseScreen[ValueT]):
|
|||
]
|
||||
)
|
||||
|
||||
debug(f'Setting cursor to target_y: {target_y}')
|
||||
|
||||
self.app.cursor_position = Offset(data_table.region.x, target_y)
|
||||
self.app.refresh()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
from io import StringIO
|
||||
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table as RichTable
|
||||
|
||||
|
||||
class BaseRichTable(RichTable):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
box=box.SIMPLE,
|
||||
show_header=False,
|
||||
pad_edge=False,
|
||||
show_edge=False,
|
||||
)
|
||||
|
||||
def stringify(self) -> str:
|
||||
string_io = StringIO()
|
||||
buf = Console(file=string_io, highlight=False)
|
||||
buf.print(self)
|
||||
|
||||
_ = string_io.seek(0)
|
||||
output = string_io.read()
|
||||
|
||||
return output
|
||||
|
|
@ -15,7 +15,7 @@ When submitting a help ticket, please include the :code:`/var/log/archinstall/in
|
|||
It can be found both on the live ISO but also in the installed filesystem if the base packages were strapped in.
|
||||
|
||||
.. tip::
|
||||
| An easy way to submit logs is ``curl -F 'file=@/var/log/archinstall/install.log' https://0x0.st``.
|
||||
| An easy way to submit logs is ``archinstall share-log``, which uploads ``install.log`` to paste.rs and prints a shareable URL.
|
||||
| Use caution when submitting other log files, but ``archinstall`` pledges to keep ``install.log`` safe for posting publicly!
|
||||
|
||||
There are additional log files under ``/var/log/archinstall/`` that can be useful:
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ classifiers = [
|
|||
]
|
||||
dependencies = [
|
||||
"pyparted==3.13.0",
|
||||
"pydantic==2.12.5",
|
||||
"pydantic==2.13.4",
|
||||
"cryptography==48.0.0",
|
||||
"textual==8.2.5",
|
||||
"markdown-it-py==4.0.0",
|
||||
|
|
@ -34,7 +34,7 @@ Source = "https://github.com/archlinux/archinstall"
|
|||
[project.optional-dependencies]
|
||||
log = ["systemd_python==235"]
|
||||
dev = [
|
||||
"mypy==1.20.2",
|
||||
"mypy==2.1.0",
|
||||
"flake8==7.3.0",
|
||||
"pre-commit==4.6.0",
|
||||
"ruff==0.15.12",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
# pylint: disable=redefined-outer-name
|
||||
import urllib.error
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from archinstall.lib.output import share_install_log
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def log_file(tmp_path: Path) -> Path:
|
||||
log_dir = tmp_path / 'archinstall'
|
||||
log_dir.mkdir()
|
||||
return log_dir / 'install.log'
|
||||
|
||||
|
||||
def _fake_logger(log_file: Path) -> MagicMock:
|
||||
mock = MagicMock()
|
||||
mock.path = log_file
|
||||
return mock
|
||||
|
||||
|
||||
def test_file_not_found(tmp_path: Path) -> None:
|
||||
missing = tmp_path / 'no-such' / 'install.log'
|
||||
with patch('archinstall.lib.output.logger', _fake_logger(missing)):
|
||||
assert share_install_log() == 1
|
||||
|
||||
|
||||
def test_empty_file(log_file: Path) -> None:
|
||||
log_file.write_bytes(b'')
|
||||
with patch('archinstall.lib.output.logger', _fake_logger(log_file)):
|
||||
assert share_install_log() == 1
|
||||
|
||||
|
||||
def test_user_cancels(log_file: Path) -> None:
|
||||
log_file.write_text('some log content')
|
||||
with patch('archinstall.lib.output.logger', _fake_logger(log_file)):
|
||||
assert share_install_log(confirm=lambda _: False) == 1
|
||||
|
||||
|
||||
def test_successful_upload(log_file: Path) -> None:
|
||||
log_file.write_text('some log content')
|
||||
fake_response = BytesIO(b'https://paste.rs/abc.def')
|
||||
|
||||
with (
|
||||
patch('archinstall.lib.output.logger', _fake_logger(log_file)),
|
||||
patch('urllib.request.urlopen', return_value=fake_response) as mock_open,
|
||||
):
|
||||
result = share_install_log()
|
||||
|
||||
assert result == 0
|
||||
req = mock_open.call_args[0][0]
|
||||
assert req.data == b'some log content'
|
||||
|
||||
|
||||
def test_truncation(log_file: Path) -> None:
|
||||
max_size = 100
|
||||
content = b'A' * 50 + b'B' * 80
|
||||
log_file.write_bytes(content)
|
||||
fake_response = BytesIO(b'https://paste.rs/abc.def')
|
||||
|
||||
with (
|
||||
patch('archinstall.lib.output.logger', _fake_logger(log_file)),
|
||||
patch('urllib.request.urlopen', return_value=fake_response) as mock_open,
|
||||
):
|
||||
result = share_install_log(max_size=max_size)
|
||||
|
||||
assert result == 0
|
||||
req = mock_open.call_args[0][0]
|
||||
assert len(req.data) == max_size
|
||||
assert req.data == content[-max_size:]
|
||||
|
||||
|
||||
def test_network_error(log_file: Path) -> None:
|
||||
log_file.write_text('some log content')
|
||||
|
||||
with (
|
||||
patch('archinstall.lib.output.logger', _fake_logger(log_file)),
|
||||
patch('urllib.request.urlopen', side_effect=urllib.error.URLError('no network')),
|
||||
):
|
||||
assert share_install_log() == 1
|
||||
|
||||
|
||||
def test_unexpected_response(log_file: Path) -> None:
|
||||
log_file.write_text('some log content')
|
||||
fake_response = BytesIO(b'ERROR: something went wrong')
|
||||
|
||||
with (
|
||||
patch('archinstall.lib.output.logger', _fake_logger(log_file)),
|
||||
patch('urllib.request.urlopen', return_value=fake_response),
|
||||
):
|
||||
assert share_install_log() == 1
|
||||
Loading…
Reference in New Issue