Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 134 additions & 31 deletions archinstall/lib/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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
Expand All @@ -17,6 +18,7 @@
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
Expand Down Expand Up @@ -57,6 +59,81 @@ 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
Expand All @@ -80,58 +157,84 @@ class ArchConfig:
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]
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
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
config[ArchConfigType.ENCRYPTION_PASSWORD] = disk_encryption.encryption_password.plaintext

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

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

return base_config

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

def sub_cfg(self) -> dict[ArchConfigType, SubConfig]:
cfg: dict[ArchConfigType, SubConfig] = {
ArchConfigType.PACMAN_CONFIG: self.pacman_config,
}

if self.mirror_config:
cfg[ArchConfigType.MIRROR_CONFIG] = self.mirror_config

if self.bootloader_config:
cfg[ArchConfigType.BOOTLOADER_CONFIG] = self.bootloader_config

if self.disk_config:
config['disk_config'] = self.disk_config.json()
cfg[ArchConfigType.DISK_CONFIG] = self.disk_config

if self.profile_config:
config['profile_config'] = self.profile_config.json()
if self.swap:
cfg[ArchConfigType.SWAP] = self.swap

if self.mirror_config:
config['mirror_config'] = self.mirror_config.json()
if self.auth_config:
cfg[ArchConfigType.AUTH_CONFIG] = self.auth_config

if self.locale_config:
cfg[ArchConfigType.LOCALE_CONFIG] = self.locale_config

if self.profile_config:
cfg[ArchConfigType.PROFILE_CONFIG] = self.profile_config

if self.network_config:
config['network_config'] = self.network_config.json()
cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config

return config
if self.app_config:
cfg[ArchConfigType.APP_CONFIG] = self.app_config

return cfg

@classmethod
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
Expand Down
84 changes: 21 additions & 63 deletions archinstall/lib/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@

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
Expand Down Expand Up @@ -45,15 +44,15 @@ def user_credentials_file(self) -> Path:
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:
Expand All @@ -64,65 +63,24 @@ def as_summary(self) -> str:
"""
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]] = []

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

bl_config = self._config.bootloader_config
if bl_config and bl_config.bootloader != Bootloader.NO_BOOTLOADER:
rows.append((tr('Bootloader'), bl_config.bootloader.value))

kernels = self._config.kernels
if kernels:
rows.append((tr('Kernel'), ', '.join(kernels)))

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

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 ''
cfg: dict[str, str | list[str] | bool] = {}

for key, value in self._config.plain_cfg().items():
cfg[key.text()] = value

for config_type, obj in self._config.sub_cfg().items():
if not hasattr(obj, 'summary'):
continue

summary = obj.summary()
if summary:
cfg[config_type.text()] = summary

simple_summary = as_key_value_pair(cfg, ignore_empty=True)

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")}. '
Expand Down
Loading