Integrate new arguments data structure (#3167)

* Integrate new args dataclass

* Integrate args

* Update

* Update

* Update

* Update
This commit is contained in:
Daniel Girtler 2025-02-20 07:35:59 +11:00 committed by GitHub
parent a9ae064359
commit b57f7f91cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 1356 additions and 1391 deletions

View File

@ -1,4 +1,5 @@
"""Arch Linux installer - guided, templates etc."""
import curses
import importlib
import os
@ -9,29 +10,13 @@ from argparse import ArgumentParser, Namespace
from pathlib import Path
from typing import TYPE_CHECKING, Any
from . import default_profiles
from .lib import disk, exceptions, interactions, locale, luks, mirrors, models, networking, packages, profile
from .lib.boot import Boot
from .lib.configuration import ConfigurationOutput
from .lib.general import (
JSON,
UNSAFE_JSON,
SysCommand,
SysCommandWorker,
clear_vt100_escape_codes,
generate_password,
json_stream_to_structure,
locate_binary,
run_custom_user_commands,
secret,
)
from .lib.global_menu import GlobalMenu
from .lib.hardware import GfxDriver, SysInfo
from .lib.installer import Installer, accessibility_tools_in_use
from archinstall.lib.args import arch_config_handler
from archinstall.lib.disk.utils import disk_layouts
from .lib.hardware import SysInfo
from .lib.output import FormattedOutput, debug, error, info, log, warn
from .lib.pacman import Pacman
from .lib.plugins import load_plugin, plugins
from .lib.storage import storage
from .lib.translationhandler import DeferredTranslation, Language, translation_handler
from .tui import Tui
@ -41,9 +26,6 @@ if TYPE_CHECKING:
_: Callable[[str], DeferredTranslation]
__version__ = "3.0.2"
storage['__version__'] = __version__
# add the custom _ as a builtin, it can now be used anywhere in the
# project to mark strings as translatable with _('translate me')
DeferredTranslation.install()
@ -56,236 +38,18 @@ debug(f"Virtualization detected: {SysInfo.virtualization()}; is VM: {SysInfo.is_
debug(f"Graphics devices detected: {SysInfo._graphics_devices().keys()}")
# For support reasons, we'll log the disk layout pre installation to match against post-installation layout
debug(f"Disk states before installing:\n{disk.disk_layouts()}")
parser = ArgumentParser()
def define_arguments() -> None:
"""
Define which explicit arguments do we allow.
Refer to https://docs.python.org/3/library/argparse.html for documentation and
https://docs.python.org/3/howto/argparse.html for a tutorial
Remember that the property/entry name python assigns to the parameters is the first string defined as argument and
dashes inside it '-' are changed to '_'
"""
parser.add_argument("-v", "--version", action="version", version="%(prog)s " + __version__)
parser.add_argument("--config", nargs="?", help="JSON configuration file or URL")
parser.add_argument("--creds", nargs="?", help="JSON credentials configuration file")
parser.add_argument("--silent", action="store_true",
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",
help="Generates a configuration file and then exits instead of performing an installation")
parser.add_argument("--script", default="guided", nargs="?", help="Script to run for installation", type=str)
parser.add_argument("--mount-point", "--mount_point", default=Path("/mnt/archinstall"), nargs="?", type=Path,
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("--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)
parser.add_argument("--skip-version-check", action="store_true",
help="Skip the version check when running archinstall")
debug(f"Disk states before installing:\n{disk_layouts()}")
if 'sphinx' not in sys.modules and 'pylint' not in sys.modules:
if '--help' in sys.argv or '-h' in sys.argv:
define_arguments()
parser.print_help()
arch_config_handler.print_help()
exit(0)
if os.getuid() != 0:
print(_("Archinstall requires root privileges to run. See --help for more."))
exit(1)
def parse_unspecified_argument_list(unknowns: list, multiple: bool = False, err: bool = False) -> dict: # type: ignore[type-arg]
"""We accept arguments not defined to the parser. (arguments "ad hoc").
Internally argparse return to us a list of words so we have to parse its contents, manually.
We accept following individual syntax for each argument
--argument value
--argument=value
--argument = value
--argument (boolean as default)
the optional parameters to the function alter a bit its behaviour:
* multiple allows multivalued arguments, each value separated by whitespace. They're returned as a list
* error. If set any non correctly specified argument-value pair to raise an exception. Else, simply notifies the
existence of a problem and continues processing.
To a certain extent, multiple and error are incompatible. In fact, the only error this routine can catch, as of now,
is the event argument value value ...
which isn't am error if multiple is specified
"""
tmp_list = [arg for arg in unknowns if arg != "="] # wastes a few bytes, but avoids any collateral effect of the destructive nature of the pop method()
config = {}
key = None
last_key = None
while tmp_list:
element = tmp_list.pop(0) # retrieve an element of the list
if element.startswith('--'): # is an argument ?
if '=' in element: # uses the arg=value syntax ?
key, value = [x.strip() for x in element[2:].split('=', 1)]
config[key] = value
last_key = key # for multiple handling
key = None # we have the kwy value pair we need
else:
key = element[2:]
config[key] = True # every argument starts its lifecycle as boolean
elif key:
config[key] = element
last_key = key # multiple
key = None
elif multiple and last_key:
if isinstance(config[last_key], str):
config[last_key] = [config[last_key], element]
else:
config[last_key].append(element)
elif err:
raise ValueError(f"Entry {element} is not related to any argument")
else:
print(f" We ignore the entry {element} as it isn't related to any argument")
return config
def cleanup_empty_args(args: Namespace | dict) -> dict: # type: ignore[type-arg]
"""
Takes arguments (dictionary or argparse Namespace) and removes any
None values. This ensures clean mergers during dict.update(args)
"""
if type(args) is Namespace:
args = vars(args)
clean_args = {}
for key, val in args.items():
if isinstance(val, dict):
val = cleanup_empty_args(val)
if val is not None:
clean_args[key] = val
return clean_args
def get_arguments() -> dict[str, Any]:
""" The handling of parameters from the command line
Is done on following steps:
0) we create a dict to store the arguments and their values
1) preprocess.
We take those arguments which use JSON files, and read them into the argument dict. So each first level entry
becomes an argument on its own right
2) Load.
We convert the predefined argument list directly into the dict via the vars() function. Non specified arguments
are loaded with value None or false if they are booleans (action="store_true"). The name is chosen according to
argparse conventions. See above (the first text is used as argument name, but underscore substitutes dash). We
then load all the undefined arguments. In this case the names are taken as written.
Important. This way explicit command line arguments take precedence over configuration files.
3) Amend
Change whatever is needed on the configuration dictionary (it could be done in post_process_arguments but this
ougth to be left to changes anywhere else in the code, not in the arguments dictionary
"""
config: dict[str, Any] = {}
args, unknowns = parser.parse_known_args()
# preprocess the JSON files.
# TODO Expand the url access to the other JSON file arguments ?
if args.config is not None:
if not json_stream_to_structure('--config', args.config, config):
exit(1)
if args.creds is not None:
if not json_stream_to_structure('--creds', args.creds, config):
exit(1)
# load the parameters. first the known, then the unknowns
clean_args = cleanup_empty_args(args)
config.update(clean_args)
config.update(parse_unspecified_argument_list(unknowns))
# amend the parameters (check internal consistency)
# Installation can't be silent if config is not passed
if clean_args.get('config') is None:
config["silent"] = False
else:
config["silent"] = clean_args.get('silent')
# avoiding a compatibility issue
if 'dry-run' in config:
del config['dry-run']
return config
def load_config() -> None:
"""
refine and set some arguments. Formerly at the scripts
"""
from .lib.models import NetworkConfiguration
arguments['locale_config'] = locale.LocaleConfiguration.parse_arg(arguments)
if (archinstall_lang := arguments.get('archinstall-language', None)) is not None:
arguments['archinstall-language'] = translation_handler.get_language_by_name(archinstall_lang)
if disk_config := arguments.get('disk_config', {}):
arguments['disk_config'] = disk.DiskLayoutConfiguration.parse_arg(disk_config)
if profile_config := arguments.get('profile_config', None):
arguments['profile_config'] = profile.ProfileConfiguration.parse_arg(profile_config)
if mirror_config := arguments.get('mirror_config', None):
arguments['mirror_config'] = mirrors.MirrorConfiguration.parse_args(mirror_config)
if arguments.get('servers', None) is not None:
storage['_selected_servers'] = arguments.get('servers', None)
if (net_config := arguments.get('network_config', None)) is not None:
config = NetworkConfiguration.parse_arg(net_config)
arguments['network_config'] = config
if arguments.get('!users', None) is not None or arguments.get('!superusers', None) is not None:
users = arguments.get('!users', None)
superusers = arguments.get('!superusers', None)
arguments['!users'] = models.User.parse_arguments(users, superusers)
if arguments.get('bootloader', None) is not None:
arguments['bootloader'] = models.Bootloader.from_arg(arguments['bootloader'])
if arguments.get('uki') and not arguments['bootloader'].has_uki_support():
arguments['uki'] = False
if arguments.get('audio_config', None) is not None:
arguments['audio_config'] = models.AudioConfiguration.parse_arg(arguments['audio_config'])
if arguments.get('disk_encryption', None) is not None and disk_config is not None:
arguments['disk_encryption'] = disk.DiskEncryption.parse_arg(
arguments['disk_config'],
arguments['disk_encryption'],
arguments.get('encryption_password', '')
)
def post_process_arguments(args: dict[str, Any]) -> None:
storage['arguments'] = args
if args.get('debug'):
warn(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!")
if args.get('plugin'):
path = args['plugin']
load_plugin(path)
try:
load_config()
except ValueError as err:
warn(str(err))
exit(1)
define_arguments()
arguments: dict[str, Any] = get_arguments()
post_process_arguments(arguments)
# @archinstall.plugin decorator hook to programmatically add
# plugins in runtime. Useful in profiles_bck and other things.
def plugin(f, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
@ -321,13 +85,10 @@ def main() -> None:
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 not arguments.get('skip_version_check'):
if not arch_config_handler.args.skip_version_check:
_check_new_version()
script = arguments.get('script', None)
if script is None:
print('No script to run provided')
script = arch_config_handler.args.script
mod_name = f'archinstall.scripts.{script}'
# by loading the module we'll automatically run the script

View File

@ -1,6 +1,5 @@
from typing import TYPE_CHECKING, override
import archinstall
from archinstall.default_profiles.profile import Profile, ProfileType
if TYPE_CHECKING:
@ -26,14 +25,12 @@ class PipewireProfile(Profile):
]
def _enable_pipewire_for_all(self, install_session: 'Installer') -> None:
users: User | list[User] | None = archinstall.arguments.get('!users', None)
from archinstall.lib.args import arch_config_handler
users: list[User] | None = arch_config_handler.config.users
if users is None:
return
if not isinstance(users, list):
users = [users]
for user in users:
# Create the full path for enabling the pipewire systemd items
service_dir = install_session.target / "home" / user.username / ".config" / "systemd" / "user" / "default.target.wants"

View File

@ -4,8 +4,6 @@ import sys
from enum import Enum, auto
from typing import TYPE_CHECKING
from ..lib.storage import storage
if TYPE_CHECKING:
from collections.abc import Callable
@ -110,7 +108,8 @@ class Profile:
Used to control if the Profile() should be visible or not in different contexts.
Returns True if --advanced is given on a Profile(advanced=True) instance.
"""
return self.advanced is False or storage['arguments'].get('advanced', False) is True
from archinstall.lib.args import arch_config_handler
return self.advanced is False or arch_config_handler.args.advanced is True
def install(self, install_session: 'Installer') -> None:
"""

View File

@ -1,8 +1,7 @@
from typing import TYPE_CHECKING, override
import archinstall
from archinstall.default_profiles.profile import Profile, ProfileType
from archinstall.lib.models import User
from archinstall.lib.args import arch_config_handler
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
@ -27,9 +26,5 @@ class DockerProfile(Profile):
@override
def post_install(self, install_session: 'Installer') -> None:
users: User | list[User] = archinstall.arguments.get('!users', [])
if not isinstance(users, list):
users = [users]
for user in users:
for user in arch_config_handler.config.users:
install_session.arch_chroot(f'usermod -a -G docker {user.username}')

View File

@ -4,21 +4,22 @@ import urllib.error
import urllib.parse
from argparse import ArgumentParser, Namespace
from dataclasses import dataclass, field
from importlib.metadata import version
from pathlib import Path
from typing import Any
from urllib.request import Request, urlopen
from pydantic.dataclasses import dataclass as p_dataclass
from .disk import DiskEncryption, DiskLayoutConfiguration
from .locale import LocaleConfiguration
from .mirrors import MirrorConfiguration
from .models import AudioConfiguration, Bootloader, NetworkConfiguration, User
from .output import error, warn
from .plugins import load_plugin
from .profile import ProfileConfiguration
from .storage import storage
from .translationhandler import Language, translation_handler
from archinstall.lib.mirrors import MirrorConfiguration
from archinstall.lib.models import AudioConfiguration, Bootloader, NetworkConfiguration, User
from archinstall.lib.models.device_model import DiskEncryption, DiskLayoutConfiguration
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.output import error, warn
from archinstall.lib.plugins import load_plugin
from archinstall.lib.storage import storage
from archinstall.lib.translationhandler import Language, translation_handler
@p_dataclass
@ -30,7 +31,7 @@ class Arguments:
silent: bool = False
dry_run: bool = False
script: str = 'guided'
mount_point: Path | None = Path('/mnt')
mountpoint: Path = Path('/mnt')
skip_ntp: bool = False
debug: bool = False
offline: bool = False
@ -38,11 +39,12 @@ class Arguments:
plugin: str | None = None
skip_version_check: bool = False
advanced: bool = False
verbose: bool = False
@dataclass
class ArchConfig:
version: str = field(default_factory=lambda: storage['__version__'])
version: str = field(default_factory=lambda: version('archinstall'))
locale_config: LocaleConfiguration | None = None
archinstall_language: Language = field(default_factory=lambda: translation_handler.get_language_by_abbr('en'))
disk_config: DiskLayoutConfiguration | None = None
@ -60,10 +62,62 @@ class ArchConfig:
swap: bool = True
timezone: str = 'UTC'
additional_repositories: list[str] = field(default_factory=list)
services: list[str] = field(default_factory=list)
custom_commands: list[str] = field(default_factory=list)
# Special fields that should be handle with care due to security implications
_users: list[User] = field(default_factory=list)
_disk_encryption: DiskEncryption | None = None
users: list[User] = field(default_factory=list)
disk_encryption: DiskEncryption | None = None
root_password: str | None = None
def unsafe_json(self) -> dict[str, Any]:
config = {
'!users': [user.json() for user in self.users],
'!root-password': self.root_password,
}
if self.disk_encryption:
config['encryption_password'] = self.disk_encryption.encryption_password
return config
def safe_json(self) -> dict[str, Any]:
config = {
'version': self.version,
'archinstall-language': self.archinstall_language.json(),
'hostname': self.hostname,
'kernels': self.kernels,
'ntp': self.ntp,
'packages': self.packages,
'parallel_downloads': self.parallel_downloads,
'swap': self.swap,
'timezone': self.timezone,
'additional-repositories': self.additional_repositories,
'services': self.services,
'custom_commands': self.custom_commands,
'bootloader': self.bootloader.json(),
'audio_config': self.audio_config.json() if self.audio_config else None,
}
if self.locale_config:
config['locale_config'] = self.locale_config.json()
if self.disk_config:
config['disk_config'] = self.disk_config.json()
if self.disk_encryption:
config['disk_encryption'] = self.disk_encryption.json()
if self.profile_config:
config['profile_config'] = self.profile_config.json()
if self.mirror_config:
config['mirror_config'] = self.mirror_config.json()
if self.network_config:
config['network_config'] = self.network_config.json()
return config
@classmethod
def from_config(cls, args_config: dict[str, Any]) -> 'ArchConfig':
@ -89,7 +143,7 @@ class ArchConfig:
users = args_config.get('!users', None)
superusers = args_config.get('!superusers', None)
if users is not None or superusers is not None:
arch_config._users = User.parse_arguments(users, superusers)
arch_config.users = User.parse_arguments(users, superusers)
if bootloader_config := args_config.get('bootloader', None):
arch_config.bootloader = Bootloader.from_arg(bootloader_config)
@ -101,7 +155,7 @@ class ArchConfig:
arch_config.audio_config = AudioConfiguration.parse_arg(audio_config)
if args_config.get('disk_encryption', None) is not None and arch_config.disk_config is not None:
arch_config._disk_encryption = DiskEncryption.parse_arg(
arch_config.disk_encryption = DiskEncryption.parse_arg(
arch_config.disk_config,
args_config['disk_encryption'],
args_config.get('encryption_password', '')
@ -130,6 +184,15 @@ class ArchConfig:
if additional_repositories := args_config.get('additional-repositories', []):
arch_config.additional_repositories = additional_repositories
if services := args_config.get('services', []):
arch_config.services = services
if root_password := args_config.get('!root-password', None):
arch_config.root_password = root_password
if custom_commands := args_config.get('custom_commands', []):
arch_config.custom_commands = custom_commands
return arch_config
@ -139,11 +202,11 @@ class ArchConfigHandler:
self._args: Arguments = self._parse_args()
config = self._parse_config()
self._arch_config = ArchConfig.from_config(config)
self._config = ArchConfig.from_config(config)
@property
def arch_config(self) -> ArchConfig:
return self._arch_config
def config(self) -> ArchConfig:
return self._config
@property
def args(self) -> Arguments:
@ -159,7 +222,7 @@ class ArchConfigHandler:
"--version",
action="version",
default=False,
version="%(prog)s " + storage['__version__']
version="%(prog)s " + version('archinstall')
)
parser.add_argument(
"--config",
@ -260,6 +323,12 @@ class ArchConfigHandler:
default=False,
help="Enabled advanced options"
)
parser.add_argument(
"--verbose",
action="store_true",
default=False,
help="Enabled verbose options"
)
return parser
@ -336,3 +405,6 @@ class ArchConfigHandler:
clean_args[key] = val
return clean_args
arch_config_handler: ArchConfigHandler = ArchConfigHandler()

View File

@ -2,10 +2,11 @@ import json
import readline
import stat
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, Orientation, PreviewStyle, ResultType, SelectMenu, Tui
from .args import ArchConfig
from .general import JSON, UNSAFE_JSON
from .output import debug, warn
from .storage import storage
@ -20,58 +21,36 @@ if TYPE_CHECKING:
class ConfigurationOutput:
def __init__(self, config: dict[str, Any]):
def __init__(self, config: ArchConfig):
"""
Configuration output handler to parse the existing configuration data structure and prepare for output on the
Configuration output handler to parse the existing
configuration data structure and prepare for output on the
console and for saving it to configuration files
:param config: A dictionary containing configurations (basically archinstall.arguments)
:type config: dict
:param config: Archinstall configuration object
:type config: ArchConfig
"""
self._config = config
self._user_credentials: dict[str, Any] = {}
self._user_config: dict[str, Any] = {}
self._default_save_path = storage.get('LOG_PATH', Path('.'))
self._user_config_file = 'user_configuration.json'
self._user_creds_file = "user_credentials.json"
self._sensitive = ['!users', '!root-password']
self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run']
self._process_config()
self._user_config_file = Path('user_configuration.json')
self._user_creds_file = Path('user_credentials.json')
@property
def user_credentials_file(self) -> str:
return self._user_creds_file
@property
def user_configuration_file(self) -> str:
def user_configuration_file(self) -> Path:
return self._user_config_file
def _process_config(self) -> None:
for key, value in self._config.items():
if key in self._sensitive:
self._user_credentials[key] = value
elif key in self._ignore:
pass
else:
self._user_config[key] = value
# special handling for encryption password
if key == 'disk_encryption' and value:
self._user_credentials['encryption_password'] = value.encryption_password
@property
def user_credentials_file(self) -> Path:
return self._user_creds_file
def user_config_to_json(self) -> str:
return json.dumps({
'config_version': storage['__version__'], # Tells us what version was used to generate the config
**self._user_config, # __version__ will be overwritten by old version definition found in config
'version': storage['__version__']
}, indent=4, sort_keys=True, cls=JSON)
out = self._config.safe_json()
return json.dumps(out, indent=4, sort_keys=True, cls=JSON)
def user_credentials_to_json(self) -> str | None:
if self._user_credentials:
return json.dumps(self._user_credentials, indent=4, sort_keys=True, cls=UNSAFE_JSON)
return None
def user_credentials_to_json(self) -> str:
out = self._config.unsafe_json()
return json.dumps(out, indent=4, sort_keys=True, cls=UNSAFE_JSON)
def write_debug(self) -> None:
debug(" -- Chosen configuration --")
@ -120,10 +99,9 @@ class ConfigurationOutput:
def save_user_creds(self, dest_path: Path) -> None:
if self._is_valid_path(dest_path):
if user_creds := self.user_credentials_to_json():
target = dest_path / self._user_creds_file
target.write_text(user_creds)
target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
target = dest_path / self._user_creds_file
target.write_text(self.user_credentials_to_json())
target.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
def save(self, dest_path: Path | None = None) -> None:
save_path = dest_path or self._default_save_path
@ -133,7 +111,7 @@ class ConfigurationOutput:
self.save_user_creds(save_path)
def save_config(config: dict[str, Any]) -> None:
def save_config(config: ArchConfig) -> None:
def preview(item: MenuItem) -> str | None:
match item.value:
case "user_config":
@ -144,9 +122,9 @@ def save_config(config: dict[str, Any]) -> None:
return f"{config_output.user_credentials_file}\n{maybe_serial}"
return str(_("No configuration"))
case "all":
output = [config_output.user_configuration_file]
if config_output.user_credentials_to_json():
output.append(config_output.user_credentials_file)
output = [str(config_output.user_configuration_file)]
config_output.user_credentials_to_json()
output.append(str(config_output.user_credentials_file))
return '\n'.join(output)
return None

View File

@ -1,43 +0,0 @@
from .device_handler import device_handler, disk_layouts
from .device_model import (
BDevice,
DeviceGeometry,
DeviceModification,
DiskEncryption,
DiskLayoutConfiguration,
DiskLayoutType,
EncryptionType,
Fido2Device,
FilesystemType,
LsblkInfo,
LvmConfiguration,
LvmLayoutType,
LvmVolume,
LvmVolumeGroup,
LvmVolumeStatus,
ModificationStatus,
PartitionFlag,
PartitionModification,
PartitionTable,
PartitionType,
SectorSize,
Size,
SubvolumeModification,
Unit,
_DeviceInfo,
get_all_lsblk_info,
get_lsblk_by_mountpoint,
get_lsblk_info,
)
from .disk_menu import DiskLayoutConfigurationMenu
from .encryption_menu import (
DiskEncryptionMenu,
select_encrypted_password,
select_encryption_type,
select_hsm,
select_partitions_to_encrypt,
)
from .fido import Fido2
from .filesystem import FilesystemHandler
from .partitioning_menu import PartitioningList, manual_partitioning
from .subvolume_menu import SubvolumeMenu

View File

@ -13,9 +13,7 @@ from parted import Device, Disk, DiskException, FileSystem, Geometry, IOExceptio
from ..exceptions import DiskError, UnknownFilesystemFormat
from ..general import SysCallError, SysCommand, SysCommandWorker
from ..luks import Luks2
from ..output import debug, error, info, log, warn
from ..utils.util import is_subpath
from .device_model import (
from ..models.device_model import (
BDevice,
BtrfsMountOption,
DeviceModification,
@ -39,10 +37,13 @@ from .device_model import (
_BtrfsSubvolumeInfo,
_DeviceInfo,
_PartitionInfo,
)
from ..output import debug, error, info, log
from ..utils.util import is_subpath
from .utils import (
find_lsblk_info,
get_all_lsblk_info,
get_lsblk_info,
get_lsblk_output,
)
@ -866,13 +867,3 @@ class DeviceHandler:
device_handler = DeviceHandler()
def disk_layouts() -> str:
try:
lsblk_output = get_lsblk_output()
except SysCallError as err:
warn(f"Could not return disk layouts: {err}")
return ''
return lsblk_output.model_dump_json(indent=4)

View File

@ -1,13 +1,13 @@
from typing import TYPE_CHECKING, Any, override
from dataclasses import dataclass
from typing import TYPE_CHECKING, override
from archinstall.lib.models.device_model import DiskLayoutConfiguration, DiskLayoutType, LvmConfiguration
from archinstall.tui import MenuItem, MenuItemGroup
from ..interactions import select_disk_config
from ..interactions.disk_conf import select_lvm_config
from ..menu import AbstractSubMenu
from ..output import FormattedOutput
from . import DiskLayoutConfiguration, DiskLayoutType
from .device_model import LvmConfiguration
if TYPE_CHECKING:
from collections.abc import Callable
@ -17,34 +17,44 @@ if TYPE_CHECKING:
_: Callable[[str], DeferredTranslation]
@dataclass
class DiskMenuConfig:
disk_config: DiskLayoutConfiguration | None
lvm_config: LvmConfiguration | None
class DiskLayoutConfigurationMenu(AbstractSubMenu):
def __init__(
self,
disk_layout_config: DiskLayoutConfiguration | None,
advanced: bool = False
):
self._disk_layout_config = disk_layout_config
self._advanced = advanced
self._data_store: dict[str, Any] = {}
def __init__(self, disk_layout_config: DiskLayoutConfiguration | None):
if not disk_layout_config:
self._disk_menu_config = DiskMenuConfig(disk_config=None, lvm_config=None)
else:
self._disk_menu_config = DiskMenuConfig(
disk_config=disk_layout_config,
lvm_config=disk_layout_config.lvm_config
)
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
super().__init__(
self._item_group,
self._disk_menu_config,
allow_reset=True
)
def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=str(_('Partitioning')),
action=self._select_disk_layout_config,
value=self._disk_layout_config,
value=self._disk_menu_config.disk_config,
preview_action=self._prev_disk_layouts,
key='disk_config'
),
MenuItem(
text='LVM (BETA)',
action=self._select_lvm_config,
value=self._disk_layout_config.lvm_config if self._disk_layout_config else None,
value=self._disk_menu_config.lvm_config,
preview_action=self._prev_lvm_config,
dependencies=[self._check_dep_lvm],
key='lvm_config'
@ -55,12 +65,11 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
def run(self) -> DiskLayoutConfiguration | None:
super().run()
disk_layout_config: DiskLayoutConfiguration | None = self._data_store.get('disk_config', None)
if self._disk_menu_config.disk_config:
self._disk_menu_config.disk_config.lvm_config = self._disk_menu_config.lvm_config
return self._disk_menu_config.disk_config
if disk_layout_config:
disk_layout_config.lvm_config = self._data_store.get('lvm_config', None)
return disk_layout_config
return None
def _check_dep_lvm(self) -> bool:
disk_layout_conf: DiskLayoutConfiguration | None = self._menu_item_group.find_by_key('disk_config').value
@ -74,7 +83,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu):
self,
preset: DiskLayoutConfiguration | None
) -> DiskLayoutConfiguration | None:
disk_config = select_disk_config(preset, advanced_option=self._advanced)
disk_config = select_disk_config(preset)
if disk_config != preset:
self._menu_item_group.find_by_key('lvm_config').value = None

View File

@ -1,14 +1,21 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, override
from typing import TYPE_CHECKING, override
from archinstall.lib.menu.menu_helper import MenuHelper
from archinstall.lib.models.device_model import (
DeviceModification,
DiskEncryption,
DiskLayoutConfiguration,
EncryptionType,
LvmConfiguration,
LvmVolume,
PartitionModification,
)
from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, ResultType, SelectMenu
from ..disk import DeviceModification, DiskEncryption, DiskLayoutConfiguration, EncryptionType, PartitionModification
from ..menu import AbstractSubMenu
from ..output import FormattedOutput
from ..utils.util import get_password
from . import LvmConfiguration, LvmVolume
from .fido import Fido2, Fido2Device
if TYPE_CHECKING:
@ -26,31 +33,34 @@ class DiskEncryptionMenu(AbstractSubMenu):
preset: DiskEncryption | None = None
):
if preset:
self._preset = preset
self._enc_config = preset
else:
self._preset = DiskEncryption()
self._enc_config = DiskEncryption()
self._data_store: dict[str, Any] = {}
self._disk_config = disk_config
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
super().__init__(
self._item_group,
self._enc_config,
allow_reset=True
)
def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=str(_('Encryption type')),
action=lambda x: select_encryption_type(self._disk_config, x),
value=self._preset.encryption_type,
value=self._enc_config.encryption_type,
preview_action=self._preview,
key='encryption_type'
),
MenuItem(
text=str(_('Encryption password')),
action=lambda x: select_encrypted_password(),
value=self._preset.encryption_password,
value=self._enc_config.encryption_password,
dependencies=[self._check_dep_enc_type],
preview_action=self._preview,
key='encryption_password'
@ -58,7 +68,7 @@ class DiskEncryptionMenu(AbstractSubMenu):
MenuItem(
text=str(_('Partitions')),
action=lambda x: select_partitions_to_encrypt(self._disk_config.device_modifications, x),
value=self._preset.partitions,
value=self._enc_config.partitions,
dependencies=[self._check_dep_partitions],
preview_action=self._preview,
key='partitions'
@ -66,18 +76,18 @@ class DiskEncryptionMenu(AbstractSubMenu):
MenuItem(
text=str(_('LVM volumes')),
action=self._select_lvm_vols,
value=self._preset.lvm_volumes,
value=self._enc_config.lvm_volumes,
dependencies=[self._check_dep_lvm_vols],
preview_action=self._preview,
key='lvm_vols'
key='lvm_volumes'
),
MenuItem(
text=str(_('HSM')),
action=select_hsm,
value=self._preset.hsm_device,
value=self._enc_config.hsm_device,
dependencies=[self._check_dep_enc_type],
preview_action=self._preview,
key='HSM'
key='hsm_device'
),
]
@ -111,7 +121,7 @@ class DiskEncryptionMenu(AbstractSubMenu):
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
enc_password: str | None = self._item_group.find_by_key('encryption_password').value
enc_partitions = self._item_group.find_by_key('partitions').value
enc_lvm_vols = self._item_group.find_by_key('lvm_vols').value
enc_lvm_vols = self._item_group.find_by_key('lvm_volumes').value
assert enc_type is not None
assert enc_partitions is not None
@ -129,7 +139,7 @@ class DiskEncryptionMenu(AbstractSubMenu):
encryption_type=enc_type,
partitions=enc_partitions,
lvm_volumes=enc_lvm_vols,
hsm_device=self._data_store.get('HSM', None)
hsm_device=self._enc_config.hsm_device
)
return None
@ -186,7 +196,7 @@ class DiskEncryptionMenu(AbstractSubMenu):
return None
def _prev_lvm_vols(self) -> str | None:
volumes: list[PartitionModification] | None = self._item_group.find_by_key('lvm_vols').value
volumes: list[PartitionModification] | None = self._item_group.find_by_key('lvm_volumes').value
if volumes:
output = str(_('LVM volumes to be encrypted')) + '\n'
@ -196,7 +206,7 @@ class DiskEncryptionMenu(AbstractSubMenu):
return None
def _prev_hsm(self) -> str | None:
fido_device: Fido2Device | None = self._item_group.find_by_key('HSM').value
fido_device: Fido2Device | None = self._item_group.find_by_key('hsm_device').value
if not fido_device:
return None

View File

@ -4,10 +4,11 @@ import getpass
from pathlib import Path
from typing import ClassVar
from archinstall.lib.models.device_model import Fido2Device
from ..exceptions import SysCallError
from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes
from ..output import error, info
from .device_model import Fido2Device
class Fido2:

View File

@ -9,9 +9,7 @@ from archinstall.tui import Tui
from ..hardware import SysInfo
from ..interactions.general_conf import ask_abort
from ..luks import Luks2
from ..output import debug, info
from .device_handler import device_handler
from .device_model import (
from ..models.device_model import (
DiskEncryption,
DiskLayoutConfiguration,
DiskLayoutType,
@ -26,6 +24,8 @@ from .device_model import (
Size,
Unit,
)
from ..output import debug, info
from .device_handler import device_handler
if TYPE_CHECKING:
from collections.abc import Callable

View File

@ -4,13 +4,7 @@ import re
from pathlib import Path
from typing import TYPE_CHECKING, override
from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, Orientation, ResultType, SelectMenu
from ..hardware import SysInfo
from ..menu import ListManager
from ..output import FormattedOutput
from ..utils.util import prompt_dir
from .device_model import (
from archinstall.lib.models.device_model import (
BDevice,
BtrfsMountOption,
FilesystemType,
@ -22,6 +16,12 @@ from .device_model import (
Size,
Unit,
)
from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, Orientation, ResultType, SelectMenu
from ..hardware import SysInfo
from ..menu import ListManager
from ..output import FormattedOutput
from ..utils.util import prompt_dir
from .subvolume_menu import SubvolumeMenu
if TYPE_CHECKING:

View File

@ -1,11 +1,11 @@
from pathlib import Path
from typing import TYPE_CHECKING, override
from archinstall.lib.models.device_model import SubvolumeModification
from archinstall.tui import Alignment, EditMenu, ResultType
from ..menu import ListManager
from ..utils.util import prompt_dir
from .device_model import SubvolumeModification
if TYPE_CHECKING:
from collections.abc import Callable

View File

@ -0,0 +1,111 @@
from pathlib import Path
from pydantic import BaseModel
from archinstall.lib.exceptions import DiskError, SysCallError
from archinstall.lib.general import SysCommand
from archinstall.lib.models.device_model import LsblkInfo
from archinstall.lib.output import debug, warn
class LsblkOutput(BaseModel):
blockdevices: list[LsblkInfo]
def _fetch_lsblk_info(
dev_path: Path | str | None = None,
reverse: bool = False,
full_dev_path: bool = False
) -> LsblkOutput:
cmd = ['lsblk', '--json', '--bytes', '--output', ','.join(LsblkInfo.fields())]
if reverse:
cmd.append('--inverse')
if full_dev_path:
cmd.append('--paths')
if dev_path:
cmd.append(str(dev_path))
try:
worker = SysCommand(cmd)
except SysCallError as err:
# Get the output minus the message/info from lsblk if it returns a non-zero exit code.
if err.worker:
err_str = err.worker.decode()
debug(f'Error calling lsblk: {err_str}')
if dev_path:
raise DiskError(f'Failed to read disk "{dev_path}" with lsblk')
raise err
output = worker.output(remove_cr=False)
return LsblkOutput.model_validate_json(output)
def get_lsblk_info(
dev_path: Path | str,
reverse: bool = False,
full_dev_path: bool = False
) -> LsblkInfo:
infos = _fetch_lsblk_info(dev_path, reverse=reverse, full_dev_path=full_dev_path)
if infos.blockdevices:
return infos.blockdevices[0]
raise DiskError(f'lsblk failed to retrieve information for "{dev_path}"')
def get_all_lsblk_info() -> list[LsblkInfo]:
return _fetch_lsblk_info().blockdevices
def get_lsblk_output() -> LsblkOutput:
return _fetch_lsblk_info()
def find_lsblk_info(
dev_path: Path | str,
info: list[LsblkInfo]
) -> LsblkInfo | None:
if isinstance(dev_path, str):
dev_path = Path(dev_path)
for lsblk_info in info:
if lsblk_info.path == dev_path:
return lsblk_info
return None
def get_lsblk_by_mountpoint(mountpoint: Path, as_prefix: bool = False) -> list[LsblkInfo]:
def _check(infos: list[LsblkInfo]) -> list[LsblkInfo]:
devices = []
for entry in infos:
if as_prefix:
matches = [m for m in entry.mountpoints if str(m).startswith(str(mountpoint))]
if matches:
devices += [entry]
elif mountpoint in entry.mountpoints:
devices += [entry]
if len(entry.children) > 0:
if len(match := _check(entry.children)) > 0:
devices += match
return devices
all_info = get_all_lsblk_info()
return _check(all_info)
def disk_layouts() -> str:
try:
lsblk_output = get_lsblk_output()
except SysCallError as err:
warn(f"Could not return disk layouts: {err}")
return ''
return lsblk_output.model_dump_json(indent=4)

View File

@ -319,8 +319,7 @@ class SysCommandWorker:
# If history_logfile does not exist, ignore the error
pass
if storage.get('arguments', {}).get('debug'):
debug(f"Executing: {self.cmd}")
debug(f"Executing: {" ".join(self.cmd)}")
try:
os.execve(self.cmd[0], list(self.cmd), {**os.environ, **self.environment_vars})

View File

@ -1,10 +1,13 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu
from archinstall.lib.models.device_model import DiskEncryption, DiskLayoutConfiguration, DiskLayoutType, EncryptionType, FilesystemType, PartitionModification
from archinstall.tui import MenuItem, MenuItemGroup
from . import disk
from .args import ArchConfig
from .configuration import save_config
from .general import secret
from .hardware import SysInfo
@ -23,12 +26,13 @@ from .interactions import (
select_additional_repositories,
select_kernel,
)
from .locale.locale_menu import LocaleConfiguration, LocaleMenu
from .locale.locale_menu import LocaleMenu
from .menu import AbstractMenu
from .mirrors import MirrorConfiguration, MirrorMenu
from .models import NetworkConfiguration, NicType
from .models.audio_configuration import AudioConfiguration
from .models.bootloader import Bootloader
from .models.locale import LocaleConfiguration
from .models.users import User
from .output import FormattedOutput
from .profile.profile_menu import ProfileConfiguration
@ -44,20 +48,17 @@ if TYPE_CHECKING:
class GlobalMenu(AbstractMenu):
def __init__(self, data_store: dict[str, Any]):
self._data_store = data_store
if 'archinstall-language' not in data_store:
data_store['archinstall-language'] = translation_handler.get_language_by_abbr('en')
def __init__(self, arch_config: ArchConfig) -> None:
self._arch_config = arch_config
menu_optioons = self._get_menu_options()
self._item_group = MenuItemGroup(
menu_optioons,
sort_items=False,
checkmarks=True
)
super().__init__(self._item_group, data_store)
super().__init__(self._item_group, config=arch_config)
def _get_menu_options(self) -> list[MenuItem]:
return [
@ -65,7 +66,7 @@ class GlobalMenu(AbstractMenu):
text=str(_('Archinstall language')),
action=self._select_archinstall_language,
display_action=lambda x: x.display_name if x else '',
key='archinstall-language'
key='archinstall_language'
),
MenuItem(
text=str(_('Locales')),
@ -90,8 +91,8 @@ class GlobalMenu(AbstractMenu):
text=str(_('Disk encryption')),
action=self._disk_encryption,
preview_action=self._prev_disk_encryption,
key='disk_encryption',
dependencies=['disk_config']
dependencies=['disk_config'],
key='disk_encryption'
),
MenuItem(
text=str(_('Swap')),
@ -126,13 +127,13 @@ class GlobalMenu(AbstractMenu):
text=str(_('Root password')),
action=self._set_root_password,
preview_action=self._prev_root_pwd,
key='!root-password',
key='root_password',
),
MenuItem(
text=str(_('User account')),
action=self._create_user_account,
preview_action=self._prev_users,
key='!users'
key='users'
),
MenuItem(
text=str(_('Profile')),
@ -166,7 +167,7 @@ class GlobalMenu(AbstractMenu):
action=add_number_of_parallel_downloads,
value=0,
preview_action=self._prev_parallel_dw,
key='parallel downloads'
key='parallel_downloads'
),
MenuItem(
text=str(_('Additional packages')),
@ -180,7 +181,7 @@ class GlobalMenu(AbstractMenu):
action=select_additional_repositories,
value=[],
preview_action=self._prev_additional_repos,
key='additional-repositories'
key='additional_repositories'
),
MenuItem(
text=str(_('Timezone')),
@ -202,27 +203,28 @@ class GlobalMenu(AbstractMenu):
MenuItem(
text=str(_('Save configuration')),
action=lambda x: self._safe_config(),
key='save_config'
key='__config__'
),
MenuItem(
text=str(_('Install')),
preview_action=self._prev_install_invalid_config,
key='install'
key='__config__'
),
MenuItem(
text=str(_('Abort')),
action=lambda x: exit(1),
key='abort'
key='__config__'
)
]
def _safe_config(self) -> None:
data: dict[str, Any] = {}
for item in self._item_group.items:
if item.key is not None:
data[item.key] = item.value
# data: dict[str, Any] = {}
# for item in self._item_group.items:
# if item.key is not None:
# data[item.key] = item.value
save_config(data)
self.sync_all_to_config()
save_config(self._arch_config)
def _missing_configs(self) -> list[str]:
def check(s) -> bool:
@ -280,17 +282,17 @@ class GlobalMenu(AbstractMenu):
if o.key is not None:
self._item_group.find_by_key(o.key).text = o.text
def _disk_encryption(self, preset: disk.DiskEncryption | None) -> disk.DiskEncryption | None:
disk_config: disk.DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
def _disk_encryption(self, preset: DiskEncryption | None) -> DiskEncryption | None:
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
if not disk_config:
# this should not happen as the encryption menu has the disk_config as dependency
raise ValueError('No disk layout specified')
if not disk.DiskEncryption.validate_enc(disk_config):
if not DiskEncryption.validate_enc(disk_config):
return None
disk_encryption = disk.DiskEncryptionMenu(disk_config, preset=preset).run()
disk_encryption = DiskEncryptionMenu(disk_config, preset=preset).run()
return disk_encryption
def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration:
@ -339,12 +341,12 @@ class GlobalMenu(AbstractMenu):
return None
def _prev_disk_config(self, item: MenuItem) -> str | None:
disk_layout_conf: disk.DiskLayoutConfiguration | None = item.value
disk_layout_conf: DiskLayoutConfiguration | None = item.value
if disk_layout_conf:
output = str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg()) + '\n'
if disk_layout_conf.config_type == disk.DiskLayoutType.Pre_mount:
if disk_layout_conf.config_type == DiskLayoutType.Pre_mount:
output += str(_('Mountpoint')) + ': ' + str(disk_layout_conf.mountpoint)
if disk_layout_conf.lvm_config:
@ -401,14 +403,14 @@ class GlobalMenu(AbstractMenu):
return None
def _prev_disk_encryption(self, item: MenuItem) -> str | None:
disk_config: disk.DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
enc_config: disk.DiskEncryption | None = item.value
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
enc_config: DiskEncryption | None = item.value
if disk_config and not disk.DiskEncryption.validate_enc(disk_config):
if disk_config and not DiskEncryption.validate_enc(disk_config):
return str(_('LVM disk encryption with more than 2 partitions is currently not supported'))
if enc_config:
enc_type = disk.EncryptionType.type_to_text(enc_config.encryption_type)
enc_type = EncryptionType.type_to_text(enc_config.encryption_type)
output = str(_('Encryption type')) + f': {enc_type}\n'
output += str(_('Password')) + f': {secret(enc_config.encryption_password)}\n'
@ -436,7 +438,7 @@ class GlobalMenu(AbstractMenu):
shim if necessary.
"""
bootloader = self._item_group.find_by_key('bootloader').value
boot_partition: disk.PartitionModification | None = None
boot_partition: PartitionModification | None = None
if disk_config := self._item_group.find_by_key('disk_config').value:
for layout in disk_config.device_modifications:
@ -449,7 +451,7 @@ class GlobalMenu(AbstractMenu):
return "Boot partition not found"
if bootloader == Bootloader.Limine:
if boot_partition.fs_type != disk.FilesystemType.Fat32:
if boot_partition.fs_type != FilesystemType.Fat32:
return "Limine does not support booting from filesystems other than FAT32"
return None
@ -499,9 +501,9 @@ class GlobalMenu(AbstractMenu):
def _select_disk_config(
self,
preset: disk.DiskLayoutConfiguration | None = None
) -> disk.DiskLayoutConfiguration | None:
disk_config = disk.DiskLayoutConfigurationMenu(preset).run()
preset: DiskLayoutConfiguration | None = None
) -> DiskLayoutConfiguration | None:
disk_config = DiskLayoutConfigurationMenu(preset).run()
if disk_config != preset:
self._menu_item_group.find_by_key('disk_encryption').value = None

View File

@ -10,16 +10,33 @@ from pathlib import Path
from types import TracebackType
from typing import TYPE_CHECKING, Any
from archinstall.lib.disk.device_handler import device_handler
from archinstall.lib.disk.fido import Fido2
from archinstall.lib.disk.utils import get_lsblk_by_mountpoint, get_lsblk_info
from archinstall.lib.models.device_model import (
DiskEncryption,
DiskLayoutConfiguration,
EncryptionType,
FilesystemType,
LvmVolume,
PartitionModification,
SectorSize,
Size,
SubvolumeModification,
Unit,
)
from archinstall.tui.curses_menu import Tui
from . import disk, pacman
from . import pacman
from .args import arch_config_handler
from .exceptions import DiskError, HardwareIncompatibilityError, RequirementError, ServiceException, SysCallError
from .general import SysCommand
from .hardware import SysInfo
from .locale import LocaleConfiguration, verify_keyboard_layout, verify_x11_keyboard_layout
from .locale import verify_keyboard_layout, verify_x11_keyboard_layout
from .luks import Luks2
from .mirrors import MirrorConfiguration
from .models.bootloader import Bootloader
from .models.locale import LocaleConfiguration
from .models.network_configuration import Nic
from .models.users import User
from .output import debug, error, info, log, warn
@ -45,8 +62,8 @@ class Installer:
def __init__(
self,
target: Path,
disk_config: disk.DiskLayoutConfiguration,
disk_encryption: disk.DiskEncryption | None = None,
disk_config: DiskLayoutConfiguration,
disk_encryption: DiskEncryption | None = None,
base_packages: list[str] = [],
kernels: list[str] | None = None
):
@ -58,7 +75,7 @@ class Installer:
self.kernels = kernels or ['linux']
self._disk_config = disk_config
self._disk_encryption = disk_encryption or disk.DiskEncryption(disk.EncryptionType.NoEncryption)
self._disk_encryption = disk_encryption or DiskEncryption(EncryptionType.NoEncryption)
self.target: Path = target
self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S')
@ -94,7 +111,7 @@ class Installer:
self._zram_enabled = False
self._disable_fstrim = False
self.pacman = Pacman(self.target, storage['arguments'].get('silent', False))
self.pacman = Pacman(self.target, arch_config_handler.args.silent)
def __enter__(self) -> 'Installer':
return self
@ -144,7 +161,7 @@ class Installer:
We need to wait for it before we continue since we opted in to use a custom mirror/region.
"""
if not storage['arguments'].get('skip_ntp', False):
if not arch_config_handler.args.skip_ntp:
info(_('Waiting for time sync (timedatectl show) to complete.'))
started_wait = time.time()
@ -189,10 +206,10 @@ class Installer:
NOTE: this function should be run AFTER running the mount_ordered_layout function
"""
boot_mount = self.target / 'boot'
lsblk_info = disk.get_lsblk_by_mountpoint(boot_mount)
lsblk_info = get_lsblk_by_mountpoint(boot_mount)
if len(lsblk_info) > 0:
if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB, disk.SectorSize.default()):
if lsblk_info[0].size < Size(200, Unit.MiB, SectorSize.default()):
raise DiskError(
f'The boot partition mounted at {boot_mount} is not large enough to install a boot loader. '
f'Please resize it to at least 200MiB and re-run the installation.'
@ -208,15 +225,15 @@ class Installer:
luks_handlers: dict[Any, Luks2] = {}
match self._disk_encryption.encryption_type:
case disk.EncryptionType.NoEncryption:
case EncryptionType.NoEncryption:
self._mount_lvm_layout()
case disk.EncryptionType.Luks:
case EncryptionType.Luks:
luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions)
case disk.EncryptionType.LvmOnLuks:
case EncryptionType.LvmOnLuks:
luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions)
self._import_lvm()
self._mount_lvm_layout(luks_handlers)
case disk.EncryptionType.LuksOnLvm:
case EncryptionType.LuksOnLvm:
self._import_lvm()
luks_handlers = self._prepare_luks_lvm(self._disk_encryption.lvm_volumes)
self._mount_lvm_layout(luks_handlers)
@ -275,10 +292,10 @@ class Installer:
def _prepare_luks_partitions(
self,
partitions: list[disk.PartitionModification]
) -> dict[disk.PartitionModification, Luks2]:
partitions: list[PartitionModification]
) -> dict[PartitionModification, Luks2]:
return {
part_mod: disk.device_handler.unlock_luks2_dev(
part_mod: device_handler.unlock_luks2_dev(
part_mod.dev_path,
part_mod.mapper_name,
self._disk_encryption.encryption_password
@ -295,17 +312,17 @@ class Installer:
return
for vg in lvm_config.vol_groups:
disk.device_handler.lvm_import_vg(vg)
device_handler.lvm_import_vg(vg)
for vol in vg.volumes:
disk.device_handler.lvm_vol_change(vol, True)
device_handler.lvm_vol_change(vol, True)
def _prepare_luks_lvm(
self,
lvm_volumes: list[disk.LvmVolume]
) -> dict[disk.LvmVolume, Luks2]:
lvm_volumes: list[LvmVolume]
) -> dict[LvmVolume, Luks2]:
return {
vol: disk.device_handler.unlock_luks2_dev(
vol: device_handler.unlock_luks2_dev(
vol.dev_path,
vol.mapper_name,
self._disk_encryption.encryption_password
@ -314,69 +331,69 @@ class Installer:
if vol.mapper_name and vol.dev_path
}
def _mount_partition(self, part_mod: disk.PartitionModification) -> None:
def _mount_partition(self, part_mod: PartitionModification) -> None:
if not part_mod.dev_path:
return
# it would be none if it's btrfs as the subvolumes will have the mountpoints defined
if part_mod.mountpoint:
target = self.target / part_mod.relative_mountpoint
disk.device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options)
elif part_mod.fs_type == disk.FilesystemType.Btrfs:
device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options)
elif part_mod.fs_type == FilesystemType.Btrfs:
self._mount_btrfs_subvol(
part_mod.dev_path,
part_mod.btrfs_subvols,
part_mod.mount_options
)
elif part_mod.is_swap():
disk.device_handler.swapon(part_mod.dev_path)
device_handler.swapon(part_mod.dev_path)
def _mount_lvm_vol(self, volume: disk.LvmVolume) -> None:
if volume.fs_type != disk.FilesystemType.Btrfs:
def _mount_lvm_vol(self, volume: LvmVolume) -> None:
if volume.fs_type != FilesystemType.Btrfs:
if volume.mountpoint and volume.dev_path:
target = self.target / volume.relative_mountpoint
disk.device_handler.mount(volume.dev_path, target, options=volume.mount_options)
device_handler.mount(volume.dev_path, target, options=volume.mount_options)
if volume.fs_type == disk.FilesystemType.Btrfs and volume.dev_path:
if volume.fs_type == FilesystemType.Btrfs and volume.dev_path:
self._mount_btrfs_subvol(volume.dev_path, volume.btrfs_subvols, volume.mount_options)
def _mount_luks_partition(self, part_mod: disk.PartitionModification, luks_handler: Luks2) -> None:
def _mount_luks_partition(self, part_mod: PartitionModification, luks_handler: Luks2) -> None:
if not luks_handler.mapper_dev:
return None
if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.btrfs_subvols:
if part_mod.fs_type == FilesystemType.Btrfs and part_mod.btrfs_subvols:
self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols, part_mod.mount_options)
elif part_mod.mountpoint:
target = self.target / part_mod.relative_mountpoint
disk.device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options)
device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options)
def _mount_luks_volume(self, volume: disk.LvmVolume, luks_handler: Luks2) -> None:
if volume.fs_type != disk.FilesystemType.Btrfs:
def _mount_luks_volume(self, volume: LvmVolume, luks_handler: Luks2) -> None:
if volume.fs_type != FilesystemType.Btrfs:
if volume.mountpoint and luks_handler.mapper_dev:
target = self.target / volume.relative_mountpoint
disk.device_handler.mount(luks_handler.mapper_dev, target, options=volume.mount_options)
device_handler.mount(luks_handler.mapper_dev, target, options=volume.mount_options)
if volume.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev:
if volume.fs_type == FilesystemType.Btrfs and luks_handler.mapper_dev:
self._mount_btrfs_subvol(luks_handler.mapper_dev, volume.btrfs_subvols, volume.mount_options)
def _mount_btrfs_subvol(
self,
dev_path: Path,
subvolumes: list[disk.SubvolumeModification],
subvolumes: list[SubvolumeModification],
mount_options: list[str] = []
) -> None:
for subvol in sorted(subvolumes, key=lambda x: x.relative_mountpoint):
mountpoint = self.target / subvol.relative_mountpoint
options = mount_options + [f'subvol={subvol.name}']
disk.device_handler.mount(dev_path, mountpoint, options=options)
device_handler.mount(dev_path, mountpoint, options=options)
def generate_key_files(self) -> None:
match self._disk_encryption.encryption_type:
case disk.EncryptionType.Luks:
case EncryptionType.Luks:
self._generate_key_files_partitions()
case disk.EncryptionType.LuksOnLvm:
case EncryptionType.LuksOnLvm:
self._generate_key_file_lvm_volumes()
case disk.EncryptionType.LvmOnLuks:
case EncryptionType.LvmOnLuks:
# currently LvmOnLuks only supports a single
# partitioning layout (boot + partition)
# so we won't need any keyfile generation atm
@ -398,7 +415,7 @@ class Installer:
if part_mod.is_root() and not gen_enc_file:
if self._disk_encryption.hsm_device:
disk.Fido2.fido2_enroll(
Fido2.fido2_enroll(
self._disk_encryption.hsm_device,
part_mod.safe_dev_path,
self._disk_encryption.encryption_password
@ -420,7 +437,7 @@ class Installer:
if vol.is_root() and not gen_enc_file:
if self._disk_encryption.hsm_device:
disk.Fido2.fido2_enroll(
Fido2.fido2_enroll(
self._disk_encryption.hsm_device,
vol.safe_dev_path,
self._disk_encryption.encryption_password
@ -742,7 +759,7 @@ class Installer:
def _prepare_fs_type(
self,
fs_type: disk.FilesystemType,
fs_type: FilesystemType,
mountpoint: Path | None
) -> None:
if (pkg := fs_type.installation_pkg) is not None:
@ -778,7 +795,7 @@ class Installer:
multilib: bool = False,
mkinitcpio: bool = True,
hostname: str | None = None,
locale_config: LocaleConfiguration = LocaleConfiguration.default()
locale_config: LocaleConfiguration | None = LocaleConfiguration.default()
):
if self._disk_config.lvm_config:
lvm = 'lvm2'
@ -790,7 +807,7 @@ class Installer:
if vol.fs_type is not None:
self._prepare_fs_type(vol.fs_type, vol.mountpoint)
types = (disk.EncryptionType.LvmOnLuks, disk.EncryptionType.LuksOnLvm)
types = (EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm)
if self._disk_encryption.encryption_type in types:
self._prepare_encrypt(lvm)
else:
@ -852,8 +869,9 @@ class Installer:
if hostname:
self.set_hostname(hostname)
self.set_locale(locale_config)
self.set_keyboard_language(locale_config.kb_layout)
if locale_config:
self.set_locale(locale_config)
self.set_keyboard_language(locale_config.kb_layout)
# TODO: Use python functions for this
SysCommand(f'arch-chroot {self.target} chmod 700 /root')
@ -889,19 +907,19 @@ class Installer:
else:
raise ValueError("Archinstall currently only supports setting up swap on zram")
def _get_efi_partition(self) -> disk.PartitionModification | None:
def _get_efi_partition(self) -> PartitionModification | None:
for layout in self._disk_config.device_modifications:
if partition := layout.get_efi_partition():
return partition
return None
def _get_boot_partition(self) -> disk.PartitionModification | None:
def _get_boot_partition(self) -> PartitionModification | None:
for layout in self._disk_config.device_modifications:
if boot := layout.get_boot_partition():
return boot
return None
def _get_root(self) -> disk.PartitionModification | disk.LvmVolume | None:
def _get_root(self) -> PartitionModification | LvmVolume | None:
if self._disk_config.lvm_config:
return self._disk_config.lvm_config.get_root_volume()
else:
@ -911,7 +929,7 @@ class Installer:
return None
def _get_luks_uuid_from_mapper_dev(self, mapper_dev_path: Path) -> str:
lsblk_info = disk.get_lsblk_info(mapper_dev_path, reverse=True, full_dev_path=True)
lsblk_info = get_lsblk_info(mapper_dev_path, reverse=True, full_dev_path=True)
if not lsblk_info.children or not lsblk_info.children[0].uuid:
raise ValueError('Unable to determine UUID of luks superblock')
@ -920,7 +938,7 @@ class Installer:
def _get_kernel_params_partition(
self,
root_partition: disk.PartitionModification,
root_partition: PartitionModification,
id_root: bool = True,
partuuid: bool = True
) -> list[str]:
@ -958,16 +976,16 @@ class Installer:
def _get_kernel_params_lvm(
self,
lvm: disk.LvmVolume
lvm: LvmVolume
) -> list[str]:
kernel_parameters = []
match self._disk_encryption.encryption_type:
case disk.EncryptionType.LvmOnLuks:
case EncryptionType.LvmOnLuks:
if not lvm.vg_name:
raise ValueError(f'Unable to determine VG name for {lvm.name}')
pv_seg_info = disk.device_handler.lvm_pvseg_info(lvm.vg_name, lvm.name)
pv_seg_info = device_handler.lvm_pvseg_info(lvm.vg_name, lvm.name)
if not pv_seg_info:
raise ValueError(f'Unable to determine PV segment info for {lvm.vg_name}/{lvm.name}')
@ -980,7 +998,7 @@ class Installer:
else:
debug(f'LvmOnLuks, encrypted root partition, identifying by UUID: {uuid}')
kernel_parameters.append(f'cryptdevice=UUID={uuid}:cryptlvm root={lvm.safe_dev_path}')
case disk.EncryptionType.LuksOnLvm:
case EncryptionType.LuksOnLvm:
uuid = self._get_luks_uuid_from_mapper_dev(lvm.mapper_path)
if self._disk_encryption.hsm_device:
@ -989,7 +1007,7 @@ class Installer:
else:
debug(f'LuksOnLvm, encrypted root partition, identifying by UUID: {uuid}')
kernel_parameters.append(f'cryptdevice=UUID={uuid}:root root=/dev/mapper/root')
case disk.EncryptionType.NoEncryption:
case EncryptionType.NoEncryption:
debug(f'Identifying root lvm by mapper device: {lvm.dev_path}')
kernel_parameters.append(f'root={lvm.safe_dev_path}')
@ -997,13 +1015,13 @@ class Installer:
def _get_kernel_params(
self,
root: disk.PartitionModification | disk.LvmVolume,
root: PartitionModification | LvmVolume,
id_root: bool = True,
partuuid: bool = True
) -> list[str]:
kernel_parameters = []
if isinstance(root, disk.LvmVolume):
if isinstance(root, LvmVolume):
kernel_parameters = self._get_kernel_params_lvm(root)
else:
kernel_parameters = self._get_kernel_params_partition(root, id_root, partuuid)
@ -1030,9 +1048,9 @@ class Installer:
def _add_systemd_bootloader(
self,
boot_partition: disk.PartitionModification,
root: disk.PartitionModification | disk.LvmVolume,
efi_partition: disk.PartitionModification | None,
boot_partition: PartitionModification,
root: PartitionModification | LvmVolume,
efi_partition: PartitionModification | None,
uki_enabled: bool = False
) -> None:
debug('Installing systemd bootloader')
@ -1129,9 +1147,9 @@ class Installer:
def _add_grub_bootloader(
self,
boot_partition: disk.PartitionModification,
root: disk.PartitionModification | disk.LvmVolume,
efi_partition: disk.PartitionModification | None
boot_partition: PartitionModification,
root: PartitionModification | LvmVolume,
efi_partition: PartitionModification | None
) -> None:
debug('Installing grub bootloader')
@ -1189,7 +1207,7 @@ class Installer:
else:
info(f"GRUB boot partition: {boot_partition.dev_path}")
parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path)
parent_dev_path = device_handler.get_parent_device_path(boot_partition.safe_dev_path)
add_options = [
'--target=i386-pc',
@ -1214,9 +1232,9 @@ class Installer:
def _add_limine_bootloader(
self,
boot_partition: disk.PartitionModification,
efi_partition: disk.PartitionModification | None,
root: disk.PartitionModification | disk.LvmVolume
boot_partition: PartitionModification,
efi_partition: PartitionModification | None,
root: PartitionModification | LvmVolume
) -> None:
debug('Installing limine bootloader')
@ -1249,16 +1267,16 @@ class Installer:
f' && /usr/bin/cp /usr/share/limine/BOOTX64.EFI {efi_partition.mountpoint}/EFI/BOOT/'
)
else:
parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path)
parent_dev_path = device_handler.get_parent_device_path(boot_partition.safe_dev_path)
if unique_path := disk.device_handler.get_unique_path_for_device(parent_dev_path):
if unique_path := device_handler.get_unique_path_for_device(parent_dev_path):
parent_dev_path = unique_path
try:
# The `limine-bios.sys` file contains stage 3 code.
shutil.copy(limine_path / 'limine-bios.sys', self.target / 'boot')
# `limine bios-install` deploys the stage 1 and 2 to the disk.
# `limine bios-install` deploys the stage 1 and 2 to the
SysCommand(f'arch-chroot {self.target} limine bios-install {parent_dev_path}', peek_output=True)
except Exception as err:
raise DiskError(f'Failed to install Limine on {parent_dev_path}: {err}')
@ -1308,8 +1326,8 @@ Exec = /bin/sh -c "{hook_command}"
def _add_efistub_bootloader(
self,
boot_partition: disk.PartitionModification,
root: disk.PartitionModification | disk.LvmVolume,
boot_partition: PartitionModification,
root: PartitionModification | LvmVolume,
uki_enabled: bool = False
) -> None:
debug('Installing efistub bootloader')
@ -1336,7 +1354,7 @@ Exec = /bin/sh -c "{hook_command}"
loader = '/EFI/Linux/arch-{kernel}.efi'
cmdline = []
parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path)
parent_dev_path = device_handler.get_parent_device_path(boot_partition.safe_dev_path)
cmd_template = (
'efibootmgr',
@ -1358,8 +1376,8 @@ Exec = /bin/sh -c "{hook_command}"
def _config_uki(
self,
root: disk.PartitionModification | disk.LvmVolume,
efi_partition: disk.PartitionModification | None
root: PartitionModification | LvmVolume,
efi_partition: PartitionModification | None
) -> None:
if not efi_partition or not efi_partition.mountpoint:
raise ValueError(f'Could not detect ESP at mountpoint {self.target}')

View File

@ -1,16 +1,37 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from archinstall.lib.args import arch_config_handler
from archinstall.lib.disk.device_handler import device_handler
from archinstall.lib.disk.partitioning_menu import manual_partitioning
from archinstall.lib.menu.menu_helper import MenuHelper
from archinstall.lib.models.device_model import (
BDevice,
BtrfsMountOption,
DeviceModification,
DiskLayoutConfiguration,
DiskLayoutType,
FilesystemType,
LvmConfiguration,
LvmLayoutType,
LvmVolume,
LvmVolumeGroup,
LvmVolumeStatus,
ModificationStatus,
PartitionFlag,
PartitionModification,
PartitionType,
SectorSize,
Size,
SubvolumeModification,
Unit,
_DeviceInfo,
)
from archinstall.lib.output import debug
from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, Orientation, PreviewStyle, ResultType, SelectMenu
from .. import disk
from ..disk.device_model import BtrfsMountOption
from ..hardware import SysInfo
from ..output import FormattedOutput, debug
from ..storage import storage
from ..output import FormattedOutput
from ..utils.util import prompt_dir
if TYPE_CHECKING:
@ -21,10 +42,10 @@ if TYPE_CHECKING:
_: Callable[[str], DeferredTranslation]
def select_devices(preset: list[disk.BDevice] | None = []) -> list[disk.BDevice]:
def select_devices(preset: list[BDevice] | None = []) -> list[BDevice]:
def _preview_device_selection(item: MenuItem) -> str | None:
device: disk._DeviceInfo = item.get_value()
dev = disk.device_handler.get_device(device.path)
device: _DeviceInfo = item.get_value()
dev = device_handler.get_device(device.path)
if dev and dev.partition_infos:
return FormattedOutput.as_table(dev.partition_infos)
@ -33,7 +54,7 @@ def select_devices(preset: list[disk.BDevice] | None = []) -> list[disk.BDevice]
if preset is None:
preset = []
devices = disk.device_handler.devices
devices = device_handler.devices
options = [d.device_info for d in devices]
presets = [p.device_info for p in preset]
@ -59,7 +80,7 @@ def select_devices(preset: list[disk.BDevice] | None = []) -> list[disk.BDevice]
case ResultType.Skip:
return preset
case ResultType.Selection:
selected_device_info: list[disk._DeviceInfo] = result.get_values()
selected_device_info: list[_DeviceInfo] = result.get_values()
selected_devices = []
for device in devices:
@ -70,49 +91,43 @@ def select_devices(preset: list[disk.BDevice] | None = []) -> list[disk.BDevice]
def get_default_partition_layout(
devices: list[disk.BDevice],
filesystem_type: disk.FilesystemType | None = None,
advanced_option: bool = False
) -> list[disk.DeviceModification]:
devices: list[BDevice],
filesystem_type: FilesystemType | None = None
) -> list[DeviceModification]:
if len(devices) == 1:
device_modification = suggest_single_disk_layout(
devices[0],
filesystem_type=filesystem_type,
advanced_options=advanced_option
filesystem_type=filesystem_type
)
return [device_modification]
else:
return suggest_multi_disk_layout(
devices,
filesystem_type=filesystem_type,
advanced_options=advanced_option
filesystem_type=filesystem_type
)
def _manual_partitioning(
preset: list[disk.DeviceModification],
devices: list[disk.BDevice]
) -> list[disk.DeviceModification]:
preset: list[DeviceModification],
devices: list[BDevice]
) -> list[DeviceModification]:
modifications = []
for device in devices:
mod = next(filter(lambda x: x.device == device, preset), None)
if not mod:
mod = disk.DeviceModification(device, wipe=False)
mod = DeviceModification(device, wipe=False)
if partitions := disk.manual_partitioning(device, preset=mod.partitions):
if partitions := manual_partitioning(device, preset=mod.partitions):
mod.partitions = partitions
modifications.append(mod)
return modifications
def select_disk_config(
preset: disk.DiskLayoutConfiguration | None = None,
advanced_option: bool = False
) -> disk.DiskLayoutConfiguration | None:
default_layout = disk.DiskLayoutType.Default.display_msg()
manual_mode = disk.DiskLayoutType.Manual.display_msg()
pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg()
def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLayoutConfiguration | None:
default_layout = DiskLayoutType.Default.display_msg()
manual_mode = DiskLayoutType.Manual.display_msg()
pre_mount_mode = DiskLayoutType.Pre_mount.display_msg()
items = [
MenuItem(default_layout, value=default_layout),
@ -149,12 +164,10 @@ def select_disk_config(
if path is None:
return None
mods = disk.device_handler.detect_pre_mounted_mods(path)
mods = device_handler.detect_pre_mounted_mods(path)
storage['arguments']['mount_point'] = path
return disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Pre_mount,
return DiskLayoutConfiguration(
config_type=DiskLayoutType.Pre_mount,
device_modifications=mods,
mountpoint=path
)
@ -166,10 +179,10 @@ def select_disk_config(
return None
if result.get_value() == default_layout:
modifications = get_default_partition_layout(devices, advanced_option=advanced_option)
modifications = get_default_partition_layout(devices)
if modifications:
return disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Default,
return DiskLayoutConfiguration(
config_type=DiskLayoutType.Default,
device_modifications=modifications
)
elif result.get_value() == manual_mode:
@ -177,8 +190,8 @@ def select_disk_config(
modifications = _manual_partitioning(preset_mods, devices)
if modifications:
return disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Manual,
return DiskLayoutConfiguration(
config_type=DiskLayoutType.Manual,
device_modifications=modifications
)
@ -186,11 +199,11 @@ def select_disk_config(
def select_lvm_config(
disk_config: disk.DiskLayoutConfiguration,
preset: disk.LvmConfiguration | None = None,
) -> disk.LvmConfiguration | None:
disk_config: DiskLayoutConfiguration,
preset: LvmConfiguration | None = None,
) -> LvmConfiguration | None:
preset_value = preset.config_type.display_msg() if preset else None
default_mode = disk.LvmLayoutType.Default.display_msg()
default_mode = LvmLayoutType.Default.display_msg()
items = [MenuItem(default_mode, value=default_mode)]
group = MenuItemGroup(items)
@ -216,37 +229,37 @@ def select_lvm_config(
return None
def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.PartitionModification:
flags = [disk.PartitionFlag.BOOT]
size = disk.Size(1, disk.Unit.GiB, sector_size)
def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModification:
flags = [PartitionFlag.BOOT]
size = Size(1, Unit.GiB, sector_size)
if using_gpt:
start = disk.Size(1, disk.Unit.MiB, sector_size)
flags.append(disk.PartitionFlag.ESP)
start = Size(1, Unit.MiB, sector_size)
flags.append(PartitionFlag.ESP)
else:
start = disk.Size(3, disk.Unit.MiB, sector_size)
start = Size(3, Unit.MiB, sector_size)
# boot partition
return disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
return PartitionModification(
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=start,
length=size,
mountpoint=Path('/boot'),
fs_type=disk.FilesystemType.Fat32,
fs_type=FilesystemType.Fat32,
flags=flags
)
def select_main_filesystem_format(advanced_options: bool = False) -> disk.FilesystemType:
def select_main_filesystem_format() -> FilesystemType:
items = [
MenuItem('btrfs', value=disk.FilesystemType.Btrfs),
MenuItem('ext4', value=disk.FilesystemType.Ext4),
MenuItem('xfs', value=disk.FilesystemType.Xfs),
MenuItem('f2fs', value=disk.FilesystemType.F2fs)
MenuItem('btrfs', value=FilesystemType.Btrfs),
MenuItem('ext4', value=FilesystemType.Ext4),
MenuItem('xfs', value=FilesystemType.Xfs),
MenuItem('f2fs', value=FilesystemType.F2fs)
]
if advanced_options:
items.append(MenuItem('ntfs', value=disk.FilesystemType.Ntfs))
if arch_config_handler.args.advanced:
items.append(MenuItem('ntfs', value=FilesystemType.Ntfs))
group = MenuItemGroup(items, sort_items=False)
result = SelectMenu(
@ -292,36 +305,35 @@ def select_mount_options() -> list[str]:
raise ValueError('Unhandled result type')
def process_root_partition_size(total_size: disk.Size, sector_size: disk.SectorSize) -> disk.Size:
def process_root_partition_size(total_size: Size, sector_size: SectorSize) -> Size:
# root partition size processing
total_device_size = total_size.convert(disk.Unit.GiB)
total_device_size = total_size.convert(Unit.GiB)
if total_device_size.value > 500:
# maximum size
return disk.Size(value=50, unit=disk.Unit.GiB, sector_size=sector_size)
return Size(value=50, unit=Unit.GiB, sector_size=sector_size)
elif total_device_size.value < 320:
# minimum size
return disk.Size(value=32, unit=disk.Unit.GiB, sector_size=sector_size)
return Size(value=32, unit=Unit.GiB, sector_size=sector_size)
else:
# 10% of total size
length = total_device_size.value // 10
return disk.Size(value=length, unit=disk.Unit.GiB, sector_size=sector_size)
return Size(value=length, unit=Unit.GiB, sector_size=sector_size)
def suggest_single_disk_layout(
device: disk.BDevice,
filesystem_type: disk.FilesystemType | None = None,
advanced_options: bool = False,
device: BDevice,
filesystem_type: FilesystemType | None = None,
separate_home: bool | None = None
) -> disk.DeviceModification:
) -> DeviceModification:
if not filesystem_type:
filesystem_type = select_main_filesystem_format(advanced_options)
filesystem_type = select_main_filesystem_format()
sector_size = device.device_info.sector_size
total_size = device.device_info.total_size
available_space = total_size
min_size_to_allow_home_part = disk.Size(64, disk.Unit.GiB, sector_size)
min_size_to_allow_home_part = Size(64, Unit.GiB, sector_size)
if filesystem_type == disk.FilesystemType.Btrfs:
if filesystem_type == FilesystemType.Btrfs:
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + '\n'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(MenuItem.yes().value)
@ -340,7 +352,7 @@ def suggest_single_disk_layout(
using_subvolumes = False
mount_options = []
device_modification = disk.DeviceModification(device, wipe=True)
device_modification = DeviceModification(device, wipe=True)
using_gpt = SysInfo.has_uefi()
@ -395,9 +407,9 @@ def suggest_single_disk_layout(
else:
root_length = available_space - root_start
root_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
root_partition = PartitionModification(
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=root_start,
length=root_length,
mountpoint=Path('/') if not using_subvolumes else None,
@ -412,11 +424,11 @@ def suggest_single_disk_layout(
# https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash
# https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh
subvolumes = [
disk.SubvolumeModification(Path('@'), Path('/')),
disk.SubvolumeModification(Path('@home'), Path('/home')),
disk.SubvolumeModification(Path('@log'), Path('/var/log')),
disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')),
disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots'))
SubvolumeModification(Path('@'), Path('/')),
SubvolumeModification(Path('@home'), Path('/home')),
SubvolumeModification(Path('@log'), Path('/var/log')),
SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')),
SubvolumeModification(Path('@.snapshots'), Path('/.snapshots'))
]
root_partition.btrfs_subvols = subvolumes
elif using_home_partition:
@ -428,11 +440,11 @@ def suggest_single_disk_layout(
flags = []
if using_gpt:
flags.append(disk.PartitionFlag.LINUX_HOME)
flags.append(PartitionFlag.LINUX_HOME)
home_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
home_partition = PartitionModification(
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=home_start,
length=home_length,
mountpoint=Path('/home'),
@ -446,23 +458,22 @@ def suggest_single_disk_layout(
def suggest_multi_disk_layout(
devices: list[disk.BDevice],
filesystem_type: disk.FilesystemType | None = None,
advanced_options: bool = False
) -> list[disk.DeviceModification]:
devices: list[BDevice],
filesystem_type: FilesystemType | None = None
) -> list[DeviceModification]:
if not devices:
return []
# Not really a rock solid foundation of information to stand on, but it's a start:
# https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/
# https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/
min_home_partition_size = disk.Size(40, disk.Unit.GiB, disk.SectorSize.default())
min_home_partition_size = Size(40, Unit.GiB, SectorSize.default())
# rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size?
desired_root_partition_size = disk.Size(32, disk.Unit.GiB, disk.SectorSize.default())
desired_root_partition_size = Size(32, Unit.GiB, SectorSize.default())
mount_options = []
if not filesystem_type:
filesystem_type = select_main_filesystem_format(advanced_options)
filesystem_type = select_main_filesystem_format()
# find proper disk for /home
possible_devices = [d for d in devices if d.device_info.total_size >= min_home_partition_size]
@ -475,13 +486,13 @@ def suggest_multi_disk_layout(
delta = device.device_info.total_size - desired_root_partition_size
devices_delta[device] = delta
sorted_delta: list[tuple[disk.BDevice, disk.Size]] = sorted(devices_delta.items(), key=lambda x: x[1])
root_device: disk.BDevice | None = sorted_delta[0][0]
sorted_delta: list[tuple[BDevice, Size]] = sorted(devices_delta.items(), key=lambda x: x[1])
root_device: BDevice | None = sorted_delta[0][0]
if home_device is None or root_device is None:
text = str(_('The selected drives do not have the minimum capacity required for an automatic suggestion\n'))
text += str(_('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB)))
text += str(_('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB)))
text += str(_('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(Unit.GiB)))
text += str(_('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(Unit.GiB)))
items = [MenuItem(str(_('Continue')))]
group = MenuItemGroup(items)
@ -489,7 +500,7 @@ def suggest_multi_disk_layout(
return []
if filesystem_type == disk.FilesystemType.Btrfs:
if filesystem_type == FilesystemType.Btrfs:
mount_options = select_mount_options()
device_paths = ', '.join([str(d.device_info.path) for d in devices])
@ -498,8 +509,8 @@ def suggest_multi_disk_layout(
debug(f'/root: {root_device.device_info.path}')
debug(f'/home: {home_device.device_info.path}')
root_device_modification = disk.DeviceModification(root_device, wipe=True)
home_device_modification = disk.DeviceModification(home_device, wipe=True)
root_device_modification = DeviceModification(root_device, wipe=True)
home_device_modification = DeviceModification(home_device, wipe=True)
root_device_sector_size = root_device_modification.device.device_info.sector_size
home_device_sector_size = home_device_modification.device.device_info.sector_size
@ -519,9 +530,9 @@ def suggest_multi_disk_layout(
root_length = root_length.align()
# add root partition to the root device
root_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
root_partition = PartitionModification(
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=root_start,
length=root_length,
mountpoint=Path('/'),
@ -530,20 +541,20 @@ def suggest_multi_disk_layout(
)
root_device_modification.add_partition(root_partition)
home_start = disk.Size(1, disk.Unit.MiB, home_device_sector_size)
home_start = Size(1, Unit.MiB, home_device_sector_size)
home_length = home_device.device_info.total_size - home_start
flags = []
if using_gpt:
home_length = home_length.gpt_end()
flags.append(disk.PartitionFlag.LINUX_HOME)
flags.append(PartitionFlag.LINUX_HOME)
home_length = home_length.align()
# add home partition to home device
home_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
home_partition = PartitionModification(
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=home_start,
length=home_length,
mountpoint=Path('/home'),
@ -557,11 +568,11 @@ def suggest_multi_disk_layout(
def suggest_lvm_layout(
disk_config: disk.DiskLayoutConfiguration,
filesystem_type: disk.FilesystemType | None = None,
disk_config: DiskLayoutConfiguration,
filesystem_type: FilesystemType | None = None,
vg_grp_name: str = 'ArchinstallVg',
) -> disk.LvmConfiguration:
if disk_config.config_type != disk.DiskLayoutType.Default:
) -> LvmConfiguration:
if disk_config.config_type != DiskLayoutType.Default:
raise ValueError('LVM suggested volumes are only available for default partitioning')
using_subvolumes = False
@ -572,7 +583,7 @@ def suggest_lvm_layout(
if not filesystem_type:
filesystem_type = select_main_filesystem_format()
if filesystem_type == disk.FilesystemType.Btrfs:
if filesystem_type == FilesystemType.Btrfs:
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + '\n'
group = MenuItemGroup.yes_no()
group.set_focus_by_value(MenuItem.yes().value)
@ -592,17 +603,17 @@ def suggest_lvm_layout(
if using_subvolumes:
btrfs_subvols = [
disk.SubvolumeModification(Path('@'), Path('/')),
disk.SubvolumeModification(Path('@home'), Path('/home')),
disk.SubvolumeModification(Path('@log'), Path('/var/log')),
disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')),
disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')),
SubvolumeModification(Path('@'), Path('/')),
SubvolumeModification(Path('@home'), Path('/home')),
SubvolumeModification(Path('@log'), Path('/var/log')),
SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')),
SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')),
]
home_volume = False
boot_part: disk.PartitionModification | None = None
other_part: list[disk.PartitionModification] = []
boot_part: PartitionModification | None = None
other_part: list[PartitionModification] = []
for mod in disk_config.device_modifications:
for part in mod.partitions:
@ -616,15 +627,15 @@ def suggest_lvm_layout(
total_vol_available = sum(
[p.length for p in other_part],
disk.Size(0, disk.Unit.B, disk.SectorSize.default()),
Size(0, Unit.B, SectorSize.default()),
)
root_vol_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default())
root_vol_size = Size(20, Unit.GiB, SectorSize.default())
home_vol_size = total_vol_available - root_vol_size
lvm_vol_group = disk.LvmVolumeGroup(vg_grp_name, pvs=other_part)
lvm_vol_group = LvmVolumeGroup(vg_grp_name, pvs=other_part)
root_vol = disk.LvmVolume(
status=disk.LvmVolumeStatus.Create,
root_vol = LvmVolume(
status=LvmVolumeStatus.Create,
name='root',
fs_type=filesystem_type,
length=root_vol_size,
@ -636,8 +647,8 @@ def suggest_lvm_layout(
lvm_vol_group.volumes.append(root_vol)
if home_volume:
home_vol = disk.LvmVolume(
status=disk.LvmVolumeStatus.Create,
home_vol = LvmVolume(
status=LvmVolumeStatus.Create,
name='home',
fs_type=filesystem_type,
length=home_vol_size,
@ -646,4 +657,4 @@ def suggest_lvm_layout(
lvm_vol_group.volumes.append(home_vol)
return disk.LvmConfiguration(disk.LvmLayoutType.Default, [lvm_vol_group])
return LvmConfiguration(LvmLayoutType.Default, [lvm_vol_group])

View File

@ -5,11 +5,11 @@ from typing import TYPE_CHECKING
from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, Orientation, ResultType, SelectMenu, Tui
from ..args import arch_config_handler
from ..locale import list_timezones
from ..models.audio_configuration import Audio, AudioConfiguration
from ..output import warn
from ..packages.packages import validate_package_list
from ..storage import storage
from ..translationhandler import Language
if TYPE_CHECKING:
@ -175,7 +175,7 @@ def ask_additional_packages_to_install(preset: list[str] = []) -> list[str]:
if len(packages) == 0:
return None
if storage['arguments']['offline'] or storage['arguments']['no_pkg_lookups']:
if arch_config_handler.args.offline or arch_config_handler.args.no_pkg_lookups:
return None
# Verify packages that were given

View File

@ -1,4 +1,3 @@
from .locale_menu import LocaleConfiguration
from .utils import (
list_keyboard_languages,
list_locales,

View File

@ -1,10 +1,10 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, override
from typing import TYPE_CHECKING, override
from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, ResultType, SelectMenu
from ..menu import AbstractSubMenu
from .utils import get_kb_layout, list_keyboard_languages, list_locales, set_kb_layout
from ..models.locale import LocaleConfiguration
from .utils import list_keyboard_languages, list_locales, set_kb_layout
if TYPE_CHECKING:
from collections.abc import Callable
@ -14,66 +14,20 @@ if TYPE_CHECKING:
_: Callable[[str], DeferredTranslation]
@dataclass
class LocaleConfiguration:
kb_layout: str
sys_lang: str
sys_enc: str
@staticmethod
def default() -> 'LocaleConfiguration':
layout = get_kb_layout()
if layout == "":
return LocaleConfiguration('us', 'en_US', 'UTF-8')
return LocaleConfiguration(layout, 'en_US', 'UTF-8')
def json(self) -> dict[str, str]:
return {
'kb_layout': self.kb_layout,
'sys_lang': self.sys_lang,
'sys_enc': self.sys_enc
}
def preview(self) -> str:
output = '{}: {}\n'.format(str(_('Keyboard layout')), self.kb_layout)
output += '{}: {}\n'.format(str(_('Locale language')), self.sys_lang)
output += '{}: {}'.format(str(_('Locale encoding')), self.sys_enc)
return output
@classmethod
def _load_config(cls, config: 'LocaleConfiguration', args: dict[str, Any]) -> 'LocaleConfiguration':
if 'sys_lang' in args:
config.sys_lang = args['sys_lang']
if 'sys_enc' in args:
config.sys_enc = args['sys_enc']
if 'kb_layout' in args:
config.kb_layout = args['kb_layout']
return config
@classmethod
def parse_arg(cls, args: dict[str, Any]) -> 'LocaleConfiguration':
default = cls.default()
if 'locale_config' in args:
default = cls._load_config(default, args['locale_config'])
else:
default = cls._load_config(default, args)
return default
class LocaleMenu(AbstractSubMenu):
def __init__(
self,
locale_conf: LocaleConfiguration
):
self._locale_conf = locale_conf
self._data_store: dict[str, str] = {}
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
super().__init__(
self._item_group,
config=self._locale_conf,
allow_reset=True
)
def _define_menu_options(self) -> list[MenuItem]:
return [
@ -82,44 +36,36 @@ class LocaleMenu(AbstractSubMenu):
action=self._select_kb_layout,
value=self._locale_conf.kb_layout,
preview_action=self._prev_locale,
key='keyboard-layout'
key='kb_layout'
),
MenuItem(
text=str(_('Locale language')),
action=select_locale_lang,
value=self._locale_conf.sys_lang,
preview_action=self._prev_locale,
key='sys-language'
key='sys_lang'
),
MenuItem(
text=str(_('Locale encoding')),
action=select_locale_enc,
value=self._locale_conf.sys_enc,
preview_action=self._prev_locale,
key='sys-encoding'
key='sys_enc'
)
]
def _prev_locale(self, item: MenuItem) -> str | None:
temp_locale = LocaleConfiguration(
self._menu_item_group.find_by_key('keyboard-layout').get_value(),
self._menu_item_group.find_by_key('sys-language').get_value(),
self._menu_item_group.find_by_key('sys-encoding').get_value(),
self._menu_item_group.find_by_key('kb_layout').get_value(),
self._menu_item_group.find_by_key('sys_lang').get_value(),
self._menu_item_group.find_by_key('sys_enc').get_value(),
)
return temp_locale.preview()
@override
def run(self) -> LocaleConfiguration:
super().run()
if not self._data_store:
return LocaleConfiguration.default()
return LocaleConfiguration(
self._data_store['keyboard-layout'],
self._data_store['sys-language'],
self._data_store['sys-encoding']
)
return self._locale_conf
def _select_kb_layout(self, preset: str | None) -> str | None:
kb_lang = select_kb_layout(preset)

View File

@ -5,7 +5,8 @@ import time
from dataclasses import dataclass
from pathlib import Path
from . import disk
from archinstall.lib.disk.utils import get_lsblk_info
from .exceptions import DiskError, SysCallError
from .general import SysCommand, SysCommandWorker, generate_password
from .output import debug, info
@ -152,18 +153,19 @@ class Luks2:
raise DiskError(f'Failed to open luks2 device: {self.luks_dev_path}')
def lock(self) -> None:
disk.device_handler.umount(self.luks_dev_path)
from archinstall.lib.disk.device_handler import device_handler
device_handler.umount(self.luks_dev_path)
# Get crypt-information about the device by doing a reverse lookup starting with the partition path
# For instance: /dev/sda
lsblk_info = disk.get_lsblk_info(self.luks_dev_path)
lsblk_info = get_lsblk_info(self.luks_dev_path)
# For each child (sub-partition/sub-device)
for child in lsblk_info.children:
# Unmount the child location
for mountpoint in child.mountpoints:
debug(f'Unmounting {mountpoint}')
disk.device_handler.umount(mountpoint, recursive=True)
device_handler.umount(mountpoint, recursive=True)
# And close it if possible.
debug(f"Closing crypt device {child.name}")

View File

@ -13,24 +13,27 @@ if TYPE_CHECKING:
_: Callable[[str], DeferredTranslation]
CONFIG_KEY = '__config__'
class AbstractMenu:
def __init__(
self,
item_group: MenuItemGroup,
data_store: dict[str, Any],
config: Any,
auto_cursor: bool = True,
allow_reset: bool = False,
reset_warning: str | None = None
):
self._menu_item_group = item_group
self._data_store = data_store
self._config = config
self.auto_cursor = auto_cursor
self._allow_reset = allow_reset
self._reset_warning = reset_warning
self.is_context_mgr = False
self._sync_all_from_ds()
self._sync_from_config()
def __enter__(self, *args: Any, **kwargs: Any) -> AbstractMenu:
self.is_context_mgr = True
@ -44,43 +47,48 @@ class AbstractMenu:
Tui.print("Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")
raise args[1]
self._sync_all_to_ds()
self.sync_all_to_config()
def _sync_all_from_ds(self) -> None:
def _sync_from_config(self) -> None:
for item in self._menu_item_group.menu_items:
if item.key is not None:
if (store_value := self._data_store.get(item.key, None)) is not None:
item.value = store_value
if item.key is not None and item.key != CONFIG_KEY:
config_value = getattr(self._config, item.key)
if config_value is not None:
item.value = config_value
def _sync_all_to_ds(self) -> None:
def sync_all_to_config(self) -> None:
for item in self._menu_item_group.menu_items:
if item.key:
self._data_store[item.key] = item.value
setattr(self._config, item.key, item.value)
def _sync(self, item: MenuItem) -> None:
if not item.key:
if not item.key or item.key == CONFIG_KEY:
return
store_value = self._data_store.get(item.key, None)
config_value = getattr(self._config, item.key)
if store_value is not None:
item.value = store_value
if config_value is not None:
item.value = config_value
elif item.value is not None:
self._data_store[item.key] = item.value
setattr(self._config, item.key, item.value)
def set_enabled(self, key: str, enabled: bool) -> None:
if (item := self._menu_item_group.find_by_key(key)) is not None:
item.enabled = enabled
return None
# the __config__ is associated with multiple items
found = False
for item in self._menu_item_group.items:
if item.key == key:
item.enabled = enabled
found = True
raise ValueError(f'No selector found: {key}')
if not found:
raise ValueError(f'No selector found: {key}')
def disable_all(self) -> None:
for item in self._menu_item_group.items:
item.enabled = False
def run(self) -> Any | None:
self._sync_all_from_ds()
self._sync_from_config()
while True:
result = SelectMenu(
@ -100,10 +108,9 @@ class AbstractMenu:
if item.action is None:
break
case ResultType.Reset:
self._data_store = {}
return None
self._sync_all_to_ds()
self.sync_all_to_config()
return None
@ -111,7 +118,7 @@ class AbstractSubMenu(AbstractMenu):
def __init__(
self,
item_group: MenuItemGroup,
data_store: dict[str, Any],
config: Any,
auto_cursor: bool = True,
allow_reset: bool = False
):
@ -120,7 +127,7 @@ class AbstractSubMenu(AbstractMenu):
super().__init__(
item_group,
data_store=data_store,
config=config,
auto_cursor=auto_cursor,
allow_reset=allow_reset
)

View File

@ -1,11 +1,9 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any, override
from typing import TYPE_CHECKING, override
from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, ResultType, SelectMenu
from .menu import AbstractSubMenu, ListManager
from .models.mirrors import MirrorRegion, mirror_list_handler
from .models.mirrors import CustomMirror, MirrorConfiguration, MirrorRegion, SignCheck, SignOption, mirror_list_handler
from .output import FormattedOutput
if TYPE_CHECKING:
@ -16,118 +14,6 @@ if TYPE_CHECKING:
_: Callable[[str], DeferredTranslation]
class SignCheck(Enum):
Never = 'Never'
Optional = 'Optional'
Required = 'Required'
class SignOption(Enum):
TrustedOnly = 'TrustedOnly'
TrustAll = 'TrustAll'
@dataclass
class CustomMirror:
name: str
url: str
sign_check: SignCheck
sign_option: SignOption
def table_data(self) -> dict[str, str]:
return {
'Name': self.name,
'Url': self.url,
'Sign check': self.sign_check.value,
'Sign options': self.sign_option.value
}
def json(self) -> dict[str, str]:
return {
'name': self.name,
'url': self.url,
'sign_check': self.sign_check.value,
'sign_option': self.sign_option.value
}
@classmethod
def parse_args(cls, args: list[dict[str, str]]) -> list['CustomMirror']:
configs = []
for arg in args:
configs.append(
CustomMirror(
arg['name'],
arg['url'],
SignCheck(arg['sign_check']),
SignOption(arg['sign_option'])
)
)
return configs
@dataclass
class MirrorConfiguration:
mirror_regions: list[MirrorRegion] = field(default_factory=list)
custom_mirrors: list[CustomMirror] = field(default_factory=list)
@property
def regions(self) -> str:
return ', '.join([m.name for m in self.mirror_regions])
def json(self) -> dict[str, Any]:
regions = {}
for m in self.mirror_regions:
regions.update(m.json())
return {
'mirror_regions': regions,
'custom_mirrors': [c.json() for c in self.custom_mirrors]
}
def mirrorlist_config(self, speed_sort: bool = True) -> str:
config = ''
for mirror_region in self.mirror_regions:
sorted_stati = mirror_list_handler.get_status_by_region(
mirror_region.name,
speed_sort=speed_sort
)
config += f'\n\n## {mirror_region.name}\n'
for status in sorted_stati:
config += f'Server = {status.server_url}\n'
for cm in self.custom_mirrors:
config += f'\n\n## {cm.name}\nServer = {cm.url}\n'
return config
def pacman_config(self) -> str:
config = ''
for mirror in self.custom_mirrors:
config += f'\n\n[{mirror.name}]\n'
config += f'SigLevel = {mirror.sign_check.value} {mirror.sign_option.value}\n'
config += f'Server = {mirror.url}\n'
return config
@classmethod
def parse_args(cls, args: dict[str, Any]) -> 'MirrorConfiguration':
config = MirrorConfiguration()
if 'mirror_regions' in args:
for region, urls in args['mirror_regions'].items():
config.mirror_regions.append(MirrorRegion(region, urls))
if 'custom_mirrors' in args:
config.custom_mirrors = CustomMirror.parse_args(args['custom_mirrors'])
return config
class CustomMirrorList(ListManager):
def __init__(self, custom_mirrors: list[CustomMirror]):
self._actions = [
@ -260,12 +146,14 @@ class MirrorMenu(AbstractSubMenu):
else:
self._mirror_config = MirrorConfiguration()
self._data_store: dict[str, Any] = {}
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
super().__init__(
self._item_group,
config=self._mirror_config,
allow_reset=True
)
def _define_menu_options(self) -> list[MenuItem]:
return [
@ -310,14 +198,7 @@ class MirrorMenu(AbstractSubMenu):
@override
def run(self) -> MirrorConfiguration:
super().run()
if not self._data_store:
return MirrorConfiguration()
return MirrorConfiguration(
mirror_regions=self._data_store.get('mirror_regions', None),
custom_mirrors=self._data_store.get('custom_mirrors', None),
)
return self._mirror_config
def select_mirror_regions(preset: list[MirrorRegion]) -> list[MirrorRegion]:

View File

@ -1,5 +1,35 @@
from .audio_configuration import Audio, AudioConfiguration
from .bootloader import Bootloader
from .device_model import (
BDevice,
DeviceGeometry,
DeviceModification,
DiskEncryption,
DiskLayoutConfiguration,
DiskLayoutType,
EncryptionType,
Fido2Device,
FilesystemType,
LsblkInfo,
LvmConfiguration,
LvmLayoutType,
LvmVolume,
LvmVolumeGroup,
LvmVolumeStatus,
ModificationStatus,
PartitionFlag,
PartitionModification,
PartitionTable,
PartitionType,
SectorSize,
Size,
SubvolumeModification,
Unit,
_DeviceInfo,
)
from .gen import LocalPackage, PackageSearch, PackageSearchResult
from .locale import LocaleConfiguration
from .mirrors import CustomMirror, MirrorConfiguration, MirrorRegion
from .network_configuration import NetworkConfiguration, Nic, NicType
from .profile_model import ProfileConfiguration
from .users import PasswordStrength, User

View File

@ -1,11 +1,13 @@
from dataclasses import dataclass
from enum import StrEnum, auto
from typing import TYPE_CHECKING
from ...default_profiles.applications.pipewire import PipewireProfile
from ..hardware import SysInfo
from ..installer import Installer
from ..output import info
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class Audio(StrEnum):
NO_AUDIO = 'No audio server'
@ -30,10 +32,12 @@ class AudioConfiguration:
def install_audio_config(
self,
installation: Installer
installation: 'Installer'
) -> None:
info(f'Installing audio server: {self.audio.name}')
from ...default_profiles.applications.pipewire import PipewireProfile
match self.audio:
case Audio.PIPEWIRE:
PipewireProfile().install(installation)

View File

@ -11,11 +11,8 @@ import parted
from parted import Disk, Geometry, Partition
from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator
from ..exceptions import DiskError, SysCallError
from ..general import SysCommand
from ..hardware import SysInfo
from ..output import debug
from ..storage import storage
if TYPE_CHECKING:
from collections.abc import Callable
@ -78,7 +75,7 @@ class DiskLayoutConfiguration:
@classmethod
def parse_arg(cls, disk_config: _DiskLayoutConfigurationSerialization) -> DiskLayoutConfiguration | None:
from .device_handler import device_handler
from archinstall.lib.disk.device_handler import device_handler
device_modifications: list[DeviceModification] = []
config_type = disk_config.get('config_type', None)
@ -100,8 +97,6 @@ class DiskLayoutConfiguration:
mods = device_handler.detect_pre_mounted_mods(path)
device_modifications.extend(mods)
storage['arguments']['mount_point'] = path
config.mountpoint = path
return config
@ -1598,96 +1593,3 @@ class LsblkInfo(BaseModel):
for name, field in cls.model_fields.items()
if name != 'children'
]
class LsblkOutput(BaseModel):
blockdevices: list[LsblkInfo]
def _fetch_lsblk_info(
dev_path: Path | str | None = None,
reverse: bool = False,
full_dev_path: bool = False
) -> LsblkOutput:
cmd = ['lsblk', '--json', '--bytes', '--output', ','.join(LsblkInfo.fields())]
if reverse:
cmd.append('--inverse')
if full_dev_path:
cmd.append('--paths')
if dev_path:
cmd.append(str(dev_path))
try:
worker = SysCommand(cmd)
except SysCallError as err:
# Get the output minus the message/info from lsblk if it returns a non-zero exit code.
if err.worker:
err_str = err.worker.decode()
debug(f'Error calling lsblk: {err_str}')
if dev_path:
raise DiskError(f'Failed to read disk "{dev_path}" with lsblk')
raise err
output = worker.output(remove_cr=False)
return LsblkOutput.model_validate_json(output)
def get_lsblk_info(
dev_path: Path | str,
reverse: bool = False,
full_dev_path: bool = False
) -> LsblkInfo:
infos = _fetch_lsblk_info(dev_path, reverse=reverse, full_dev_path=full_dev_path)
if infos.blockdevices:
return infos.blockdevices[0]
raise DiskError(f'lsblk failed to retrieve information for "{dev_path}"')
def get_all_lsblk_info() -> list[LsblkInfo]:
return _fetch_lsblk_info().blockdevices
def get_lsblk_output() -> LsblkOutput:
return _fetch_lsblk_info()
def find_lsblk_info(
dev_path: Path | str,
info: list[LsblkInfo]
) -> LsblkInfo | None:
if isinstance(dev_path, str):
dev_path = Path(dev_path)
for lsblk_info in info:
if lsblk_info.path == dev_path:
return lsblk_info
return None
def get_lsblk_by_mountpoint(mountpoint: Path, as_prefix: bool = False) -> list[LsblkInfo]:
def _check(infos: list[LsblkInfo]) -> list[LsblkInfo]:
devices = []
for entry in infos:
if as_prefix:
matches = [m for m in entry.mountpoints if str(m).startswith(str(mountpoint))]
if matches:
devices += [entry]
elif mountpoint in entry.mountpoints:
devices += [entry]
if len(entry.children) > 0:
if len(match := _check(entry.children)) > 0:
devices += match
return devices
all_info = get_all_lsblk_info()
return _check(all_info)

View File

@ -0,0 +1,60 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from ..locale.utils import get_kb_layout
if TYPE_CHECKING:
from collections.abc import Callable
from archinstall.lib.translationhandler import DeferredTranslation
_: Callable[[str], DeferredTranslation]
@dataclass
class LocaleConfiguration:
kb_layout: str
sys_lang: str
sys_enc: str
@staticmethod
def default() -> 'LocaleConfiguration':
layout = get_kb_layout()
if layout == "":
return LocaleConfiguration('us', 'en_US', 'UTF-8')
return LocaleConfiguration(layout, 'en_US', 'UTF-8')
def json(self) -> dict[str, str]:
return {
'kb_layout': self.kb_layout,
'sys_lang': self.sys_lang,
'sys_enc': self.sys_enc
}
def preview(self) -> str:
output = '{}: {}\n'.format(str(_('Keyboard layout')), self.kb_layout)
output += '{}: {}\n'.format(str(_('Locale language')), self.sys_lang)
output += '{}: {}'.format(str(_('Locale encoding')), self.sys_enc)
return output
@classmethod
def _load_config(cls, config: 'LocaleConfiguration', args: dict[str, Any]) -> 'LocaleConfiguration':
if 'sys_lang' in args:
config.sys_lang = args['sys_lang']
if 'sys_enc' in args:
config.sys_enc = args['sys_enc']
if 'kb_layout' in args:
config.kb_layout = args['kb_layout']
return config
@classmethod
def parse_arg(cls, args: dict[str, Any]) -> 'LocaleConfiguration':
default = cls.default()
if 'locale_config' in args:
default = cls._load_config(default, args['locale_config'])
else:
default = cls._load_config(default, args)
return default

View File

@ -4,14 +4,22 @@ import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any
from pydantic import BaseModel, field_validator, model_validator
from ..networking import DownloadTimer, fetch_data_from_url, ping
from ..output import debug
from ..storage import storage
if TYPE_CHECKING:
from collections.abc import Callable
from archinstall.lib.translationhandler import DeferredTranslation
_: Callable[[str], DeferredTranslation]
class MirrorStatusEntryV3(BaseModel):
@ -171,7 +179,8 @@ class MirrorListHandler:
return available_mirrors
def load_mirrors(self) -> None:
if storage['arguments']['offline']:
from ..args import arch_config_handler
if arch_config_handler.args.offline:
self.load_local_mirrors()
else:
if not self.load_remote_mirrors():
@ -281,3 +290,116 @@ class MirrorListHandler:
mirror_list_handler = MirrorListHandler()
class SignCheck(Enum):
Never = 'Never'
Optional = 'Optional'
Required = 'Required'
class SignOption(Enum):
TrustedOnly = 'TrustedOnly'
TrustAll = 'TrustAll'
@dataclass
class CustomMirror:
name: str
url: str
sign_check: SignCheck
sign_option: SignOption
def table_data(self) -> dict[str, str]:
return {
'Name': self.name,
'Url': self.url,
'Sign check': self.sign_check.value,
'Sign options': self.sign_option.value
}
def json(self) -> dict[str, str]:
return {
'name': self.name,
'url': self.url,
'sign_check': self.sign_check.value,
'sign_option': self.sign_option.value
}
@classmethod
def parse_args(cls, args: list[dict[str, str]]) -> list['CustomMirror']:
configs = []
for arg in args:
configs.append(
CustomMirror(
arg['name'],
arg['url'],
SignCheck(arg['sign_check']),
SignOption(arg['sign_option'])
)
)
return configs
@dataclass
class MirrorConfiguration:
mirror_regions: list[MirrorRegion] = field(default_factory=list)
custom_mirrors: list[CustomMirror] = field(default_factory=list)
@property
def regions(self) -> str:
return ', '.join([m.name for m in self.mirror_regions])
def json(self) -> dict[str, Any]:
regions = {}
for m in self.mirror_regions:
regions.update(m.json())
return {
'mirror_regions': regions,
'custom_mirrors': [c.json() for c in self.custom_mirrors]
}
def mirrorlist_config(self, speed_sort: bool = True) -> str:
config = ''
for mirror_region in self.mirror_regions:
sorted_stati = mirror_list_handler.get_status_by_region(
mirror_region.name,
speed_sort=speed_sort
)
config += f'\n\n## {mirror_region.name}\n'
for status in sorted_stati:
config += f'Server = {status.server_url}\n'
for cm in self.custom_mirrors:
config += f'\n\n## {cm.name}\nServer = {cm.url}\n'
return config
def pacman_config(self) -> str:
config = ''
for mirror in self.custom_mirrors:
config += f'\n\n[{mirror.name}]\n'
config += f'SigLevel = {mirror.sign_check.value} {mirror.sign_option.value}\n'
config += f'Server = {mirror.url}\n'
return config
@classmethod
def parse_args(cls, args: dict[str, Any]) -> 'MirrorConfiguration':
config = MirrorConfiguration()
mirror_regions = args.get('mirror_regions', [])
if mirror_regions:
for region, urls in mirror_regions.items():
config.mirror_regions.append(MirrorRegion(region, urls))
if 'custom_mirrors' in args:
config.custom_mirrors = CustomMirror.parse_args(args['custom_mirrors'])
return config

View File

@ -4,7 +4,7 @@ from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any
from ..profile import ProfileConfiguration
from ..models.profile_model import ProfileConfiguration
if TYPE_CHECKING:
from collections.abc import Callable

View File

@ -15,7 +15,7 @@ class ProfileConfiguration:
greeter: GreeterType | None = None
def json(self) -> dict[str, Any]:
from .profiles_handler import profile_handler
from ..profile.profiles_handler import profile_handler
return {
'profile': profile_handler.to_json(self.profile),
'gfx_driver': self.gfx_driver.value if self.gfx_driver else None,
@ -24,8 +24,7 @@ class ProfileConfiguration:
@classmethod
def parse_arg(cls, arg: dict[str, Any]) -> 'ProfileConfiguration':
from .profiles_handler import profile_handler
from ..profile.profiles_handler import profile_handler
profile = profile_handler.parse_profile_config(arg['profile'])
greeter = arg.get('greeter', None)
gfx_driver = arg.get('gfx_driver', None)

View File

@ -322,7 +322,7 @@ def log(
Journald.log(text, level=level)
if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False):
if level != logging.DEBUG:
from archinstall.tui import Tui
Tui.print(text)

View File

@ -1,3 +1,2 @@
from .profile_menu import ProfileMenu, select_greeter, select_profile
from .profile_model import ProfileConfiguration
from .profiles_handler import profile_handler

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, override
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import GreeterType, Profile
from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup, Orientation, ResultType, SelectMenu
@ -8,7 +8,7 @@ from archinstall.tui import Alignment, FrameProperties, MenuItem, MenuItemGroup,
from ..hardware import GfxDriver
from ..interactions.system_conf import select_driver
from ..menu import AbstractSubMenu
from .profile_model import ProfileConfiguration
from ..models.profile_model import ProfileConfiguration
if TYPE_CHECKING:
from collections.abc import Callable
@ -24,40 +24,42 @@ class ProfileMenu(AbstractSubMenu):
preset: ProfileConfiguration | None = None
):
if preset:
self._preset = preset
self._profile_config = preset
else:
self._preset = ProfileConfiguration()
self._data_store: dict[str, Any] = {}
self._profile_config = ProfileConfiguration()
menu_optioons = self._define_menu_options()
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
super().__init__(self._item_group, data_store=self._data_store, allow_reset=True)
super().__init__(
self._item_group,
self._profile_config,
allow_reset=True
)
def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=str(_('Type')),
action=self._select_profile,
value=self._preset.profile,
value=self._profile_config.profile,
preview_action=self._preview_profile,
key='profile'
),
MenuItem(
text=str(_('Graphics driver')),
action=self._select_gfx_driver,
value=self._preset.gfx_driver if self._preset.profile and self._preset.profile.is_graphic_driver_supported() else None,
value=self._profile_config.gfx_driver if self._profile_config.profile and self._profile_config.profile.is_graphic_driver_supported() else None,
preview_action=self._prev_gfx,
enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False,
enabled=self._profile_config.profile.is_graphic_driver_supported() if self._profile_config.profile else False,
dependencies=['profile'],
key='gfx_driver',
),
MenuItem(
text=str(_('Greeter')),
action=lambda x: select_greeter(preset=x),
value=self._preset.greeter if self._preset.profile and self._preset.profile.is_greeter_supported() else None,
enabled=self._preset.profile.is_graphic_driver_supported() if self._preset.profile else False,
value=self._profile_config.greeter if self._profile_config.profile and self._profile_config.profile.is_greeter_supported() else None,
enabled=self._profile_config.profile.is_graphic_driver_supported() if self._profile_config.profile else False,
preview_action=self._prev_greeter,
dependencies=['profile'],
key='greeter',
@ -67,15 +69,7 @@ class ProfileMenu(AbstractSubMenu):
@override
def run(self) -> ProfileConfiguration | None:
super().run()
if self._data_store.get('profile', None):
return ProfileConfiguration(
self._data_store.get('profile', None),
self._data_store.get('gfx_driver', None),
self._data_store.get('greeter', None),
)
return None
return self._profile_config
def _select_profile(self, preset: Profile | None) -> Profile | None:
profile = select_profile(preset)

View File

@ -12,9 +12,9 @@ from typing import TYPE_CHECKING, Any
from ...default_profiles.profile import GreeterType, Profile
from ..hardware import GfxDriver
from ..models.profile_model import ProfileConfiguration
from ..networking import fetch_data_from_url, list_interfaces
from ..output import debug, error, info
from .profile_model import ProfileConfiguration
if TYPE_CHECKING:
from collections.abc import Callable

View File

@ -1,21 +1,25 @@
from pathlib import Path
import archinstall
from archinstall import SysInfo, debug, info
from archinstall.lib import disk, locale
from archinstall import SysInfo
from archinstall.lib.args import ArchConfig, arch_config_handler
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.general import run_custom_user_commands
from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.installer import Installer
from archinstall.lib.installer import Installer, accessibility_tools_in_use
from archinstall.lib.interactions.general_conf import ask_chroot
from archinstall.lib.models import AudioConfiguration, Bootloader
from archinstall.lib.models.device_model import (
DiskLayoutConfiguration,
DiskLayoutType,
EncryptionType,
)
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.output import debug, error, info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui import Tui
if archinstall.arguments.get('help'):
print("See `man archinstall` for help.")
exit(0)
def ask_user_questions() -> None:
"""
@ -25,10 +29,10 @@ def ask_user_questions() -> None:
"""
with Tui():
global_menu = GlobalMenu(data_store=archinstall.arguments)
global_menu = GlobalMenu(arch_config_handler.config)
if not archinstall.arguments.get('advanced', False):
global_menu.set_enabled('parallel downloads', False)
if not arch_config_handler.args.advanced:
global_menu.set_enabled('parallel_downloads', False)
global_menu.run()
@ -40,111 +44,114 @@ def perform_installation(mountpoint: Path) -> None:
formatted and setup prior to entering this function.
"""
info('Starting installation...')
disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config']
config: ArchConfig = arch_config_handler.config
if not config.disk_config:
error("No disk configuration provided")
return
# Retrieve list of additional repositories and set boolean values appropriately
enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', [])
enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', [])
run_mkinitcpio = not archinstall.arguments.get('uki')
locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config']
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
disk_config: DiskLayoutConfiguration = config.disk_config
enable_testing = 'testing' in config.additional_repositories
enable_multilib = 'multilib' in config.additional_repositories
run_mkinitcpio = not config.uki
locale_config = config.locale_config
disk_encryption = config.disk_encryption
with Installer(
mountpoint,
disk_config,
disk_encryption=disk_encryption,
kernels=archinstall.arguments.get('kernels', ['linux'])
kernels=config.kernels
) as installation:
# Mount all the drives to the desired mountpoint
if disk_config.config_type != disk.DiskLayoutType.Pre_mount:
if disk_config.config_type != DiskLayoutType.Pre_mount:
installation.mount_ordered_layout()
installation.sanity_check()
if disk_config.config_type != disk.DiskLayoutType.Pre_mount:
if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption:
if disk_config.config_type != DiskLayoutType.Pre_mount:
if disk_encryption and disk_encryption.encryption_type != EncryptionType.NoEncryption:
# generate encryption key files for the mounted luks devices
installation.generate_key_files()
if mirror_config := archinstall.arguments.get('mirror_config', None):
if mirror_config := config.mirror_config:
installation.set_mirrors(mirror_config, on_target=False)
installation.minimal_installation(
testing=enable_testing,
multilib=enable_multilib,
mkinitcpio=run_mkinitcpio,
hostname=archinstall.arguments.get('hostname'),
hostname=arch_config_handler.config.hostname,
locale_config=locale_config
)
if mirror_config := archinstall.arguments.get('mirror_config', None):
if mirror_config := config.mirror_config:
installation.set_mirrors(mirror_config, on_target=True)
if archinstall.arguments.get('swap'):
if config.swap:
installation.setup_swap('zram')
if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi():
if config.bootloader == Bootloader.Grub and SysInfo.has_uefi():
installation.add_additional_packages("grub")
installation.add_bootloader(
archinstall.arguments["bootloader"],
archinstall.arguments.get('uki', False)
)
installation.add_bootloader(config.bootloader, config.uki)
# If user selected to copy the current ISO network configuration
# Perform a copy of the config
network_config: NetworkConfiguration | None = archinstall.arguments.get('network_config', None)
network_config: NetworkConfiguration | None = config.network_config
if network_config:
network_config.install_network_config(
installation,
archinstall.arguments.get('profile_config', None)
config.profile_config
)
if users := archinstall.arguments.get('!users', None):
if users := config.users:
installation.create_users(users)
audio_config: AudioConfiguration | None = archinstall.arguments.get('audio_config', None)
audio_config: AudioConfiguration | None = config.audio_config
if audio_config:
audio_config.install_audio_config(installation)
else:
info("No audio server will be installed")
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
installation.add_additional_packages(archinstall.arguments.get('packages', None))
if config.packages and config.packages[0] != '':
installation.add_additional_packages(config.packages)
if profile_config := archinstall.arguments.get('profile_config', None):
if profile_config := config.profile_config:
profile_handler.install_profile_config(installation, profile_config)
if timezone := archinstall.arguments.get('timezone', None):
if timezone := config.timezone:
installation.set_timezone(timezone)
if archinstall.arguments.get('ntp', False):
if config.ntp:
installation.activate_time_synchronization()
if archinstall.accessibility_tools_in_use():
if accessibility_tools_in_use():
installation.enable_espeakup()
if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw):
if (root_pw := config.root_password) and len(root_pw):
installation.user_set_pw('root', root_pw)
if profile_config := archinstall.arguments.get('profile_config', None):
if (profile_config := config.profile_config) and profile_config.profile:
profile_config.profile.post_install(installation)
# If the user provided a list of services to be enabled, pass the list to the enable_service function.
# Note that while it's called enable_service, it can actually take a list of services and iterate it.
if archinstall.arguments.get('services', None):
installation.enable_service(archinstall.arguments.get('services', []))
if servies := config.services:
installation.enable_service(servies)
# If the user provided custom commands to be run post-installation, execute them now.
if archinstall.arguments.get('custom-commands', None):
archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation)
if cc := config.custom_commands:
run_custom_user_commands(cc, installation)
installation.genfstab()
info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
if not archinstall.arguments.get('silent'):
if not arch_config_handler.args.silent:
with Tui():
chroot = ask_chroot()
@ -154,33 +161,35 @@ def perform_installation(mountpoint: Path) -> None:
except Exception:
pass
debug(f"Disk states after installing:\n{disk.disk_layouts()}")
debug(f"Disk states after installing:\n{disk_layouts()}")
def guided() -> None:
if not archinstall.arguments.get('silent'):
if not arch_config_handler.args.silent:
ask_user_questions()
config = ConfigurationOutput(archinstall.arguments)
config = ConfigurationOutput(arch_config_handler.config)
config.write_debug()
config.save()
if archinstall.arguments.get('dry_run'):
if arch_config_handler.args.dry_run:
exit(0)
if not archinstall.arguments.get('silent'):
if not arch_config_handler.args.silent:
with Tui():
if not config.confirm_config():
debug('Installation aborted')
guided()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
if arch_config_handler.config.disk_config:
fs_handler = FilesystemHandler(
arch_config_handler.config.disk_config,
arch_config_handler.config.disk_encryption
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.arguments.get('mount_point', Path('/mnt')))
fs_handler.perform_filesystem_operations()
perform_installation(arch_config_handler.args.mountpoint)
guided()

View File

@ -1,32 +1,38 @@
from pathlib import Path
import archinstall
from archinstall import ConfigurationOutput, Installer, debug, info
from archinstall.default_profiles.minimal import MinimalProfile
from archinstall.lib import disk
from archinstall.lib.interactions import select_devices, suggest_single_disk_layout
from archinstall.lib.args import ArchConfig, arch_config_handler
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.installer import Installer
from archinstall.lib.models import Bootloader, User
from archinstall.lib.profile import ProfileConfiguration, profile_handler
from archinstall.lib.models.device_model import (
DiskLayoutConfiguration,
)
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.output import debug, error, info
from archinstall.lib.profile import profile_handler
from archinstall.tui import Tui
info("Minimal only supports:")
info(" * Being installed to a single disk")
if archinstall.arguments.get('help', None):
info(" - Optional disk encryption via --!encryption-password=<password>")
info(" - Optional filesystem type via --filesystem=<fs type>")
info(" - Optional systemd network via --network")
def perform_installation(mountpoint: Path) -> None:
disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config']
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
config: ArchConfig = arch_config_handler.config
if not config.disk_config:
error("No disk configuration provided")
return
disk_config: DiskLayoutConfiguration = config.disk_config
disk_encryption = config.disk_encryption
with Installer(
mountpoint,
disk_config,
disk_encryption=disk_encryption,
kernels=archinstall.arguments.get('kernels', ['linux'])
kernels=config.kernels
) as installation:
# Strap in the base system, add a boot loader and configure
# some other minor details as specified by this profile and user.
@ -34,9 +40,13 @@ def perform_installation(mountpoint: Path) -> None:
installation.set_hostname('minimal-arch')
installation.add_bootloader(Bootloader.Systemd)
# Optionally enable networking:
if archinstall.arguments.get('network', None):
installation.copy_iso_network_config(enable_services=True)
network_config: NetworkConfiguration | None = config.network_config
if network_config:
network_config.install_network_config(
installation,
config.profile_config
)
installation.add_additional_packages(['nano', 'wget', 'git'])
@ -53,61 +63,39 @@ def perform_installation(mountpoint: Path) -> None:
info(" * devel (password: devel)")
def prompt_disk_layout() -> None:
fs_type = None
if filesystem := archinstall.arguments.get('filesystem', None):
fs_type = disk.FilesystemType(filesystem)
devices = select_devices()
modifications = suggest_single_disk_layout(devices[0], filesystem_type=fs_type)
archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Default,
device_modifications=[modifications]
)
def parse_disk_encryption() -> None:
if enc_password := archinstall.arguments.get('!encryption-password', None):
modification: list[disk.DeviceModification] = archinstall.arguments['disk_config']
partitions: list[disk.PartitionModification] = []
# encrypt all partitions except the /boot
for mod in modification:
partitions += [p for p in mod.partitions if p.mountpoint != Path('/boot')]
archinstall.arguments['disk_encryption'] = disk.DiskEncryption(
encryption_type=disk.EncryptionType.Luks,
encryption_password=enc_password,
partitions=partitions
)
def minimal() -> None:
def _minimal() -> None:
with Tui():
prompt_disk_layout()
parse_disk_encryption()
disk_config = DiskLayoutConfigurationMenu(disk_layout_config=None).run()
config = ConfigurationOutput(archinstall.arguments)
disk_encryption = None
if disk_config:
disk_encryption = DiskEncryptionMenu(disk_config).run()
arch_config_handler.config.disk_config = disk_config
arch_config_handler.config.disk_encryption = disk_encryption
config = ConfigurationOutput(arch_config_handler.config)
config.write_debug()
config.save()
if archinstall.arguments.get('dry_run'):
if arch_config_handler.args.dry_run:
exit(0)
if not archinstall.arguments.get('silent'):
if not arch_config_handler.args.silent:
with Tui():
if not config.confirm_config():
debug('Installation aborted')
minimal()
_minimal()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
if arch_config_handler.config.disk_config:
fs_handler = FilesystemHandler(
arch_config_handler.config.disk_config,
arch_config_handler.config.disk_encryption
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.arguments.get('mount_point', Path('/mnt')))
fs_handler.perform_filesystem_operations()
perform_installation(arch_config_handler.args.mountpoint)
minimal()
_minimal()

View File

@ -1,25 +1,28 @@
from pathlib import Path
import archinstall
from archinstall import debug
from archinstall.lib import disk
from archinstall import debug, error
from archinstall.lib.args import ArchConfig, arch_config_handler
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.installer import Installer
from archinstall.lib.models.device_model import (
DiskLayoutConfiguration,
)
from archinstall.tui import Tui
def ask_user_questions() -> None:
with Tui():
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
global_menu = GlobalMenu(arch_config_handler.config)
global_menu.disable_all()
global_menu.set_enabled('archinstall-language', True)
global_menu.set_enabled('archinstall_language', True)
global_menu.set_enabled('disk_config', True)
global_menu.set_enabled('disk_encryption', True)
global_menu.set_enabled('swap', True)
global_menu.set_enabled('save_config', True)
global_menu.set_enabled('install', True)
global_menu.set_enabled('abort', True)
global_menu.set_enabled('__config__', True)
global_menu.run()
@ -30,18 +33,24 @@ def perform_installation(mountpoint: Path) -> None:
Only requirement is that the block devices are
formatted and setup prior to entering this function.
"""
disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config']
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
config: ArchConfig = arch_config_handler.config
if not config.disk_config:
error("No disk configuration provided")
return
disk_config: DiskLayoutConfiguration = config.disk_config
disk_encryption = config.disk_encryption
with Installer(
mountpoint,
disk_config,
disk_encryption=disk_encryption,
kernels=archinstall.arguments.get('kernels', ['linux'])
kernels=config.kernels
) as installation:
# Mount all the drives to the desired mountpoint
# This *can* be done outside of the installation, but the installer can deal with it.
if archinstall.arguments.get('disk_config'):
if disk_config:
installation.mount_ordered_layout()
# to generate a fstab directory holder. Avoids an error on exit and at the same time checks the procedure
@ -50,33 +59,35 @@ def perform_installation(mountpoint: Path) -> None:
target.parent.mkdir(parents=True)
# For support reasons, we'll log the disk layout post installation (crash or no crash)
debug(f"Disk states after installing:\n{disk.disk_layouts()}")
debug(f"Disk states after installing:\n{disk_layouts()}")
def only_hd() -> None:
if not archinstall.arguments.get('silent'):
def _only_hd() -> None:
if not arch_config_handler.args.silent:
ask_user_questions()
config = ConfigurationOutput(archinstall.arguments)
config = ConfigurationOutput(arch_config_handler.config)
config.write_debug()
config.save()
if archinstall.arguments.get('dry_run'):
if arch_config_handler.args.dry_run:
exit(0)
if not archinstall.arguments.get('silent'):
if not arch_config_handler.args.silent:
with Tui():
if not config.confirm_config():
debug('Installation aborted')
only_hd()
_only_hd()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
if arch_config_handler.config.disk_config:
fs_handler = FilesystemHandler(
arch_config_handler.config.disk_config,
arch_config_handler.config.disk_encryption
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.arguments.get('mount_point', Path('/mnt')))
fs_handler.perform_filesystem_operations()
perform_installation(arch_config_handler.args.mountpoint)
only_hd()
_only_hd()

View File

@ -1,10 +1,11 @@
import time
import archinstall
from archinstall import info, profile
from archinstall.lib.output import info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib.storage import storage
from archinstall.tui import Tui
for p in profile.profile_handler.get_mac_addr_profiles():
for p in profile_handler.get_mac_addr_profiles():
# Tailored means it's a match for this machine
# based on it's MAC address (or some other criteria
# that fits the requirements for this machine specifically).
@ -15,5 +16,5 @@ for p in profile.profile_handler.get_mac_addr_profiles():
Tui.print(f'{i}...')
time.sleep(1)
install_session = archinstall.storage['installation_session']
install_session = storage['installation_session']
p.install(install_session)

View File

@ -1,12 +1,13 @@
from pathlib import Path
from archinstall import disk
from archinstall.lib.disk.device_handler import device_handler
from archinstall.lib.models.device_model import DiskLayoutConfiguration, DiskLayoutType
root_mount_dir = Path('/mnt/archinstall')
mods = disk.device_handler.detect_pre_mounted_mods(root_mount_dir)
mods = device_handler.detect_pre_mounted_mods(root_mount_dir)
disk_config = disk.DiskLayoutConfiguration(
disk.DiskLayoutType.Pre_mount,
disk_config = DiskLayoutConfiguration(
DiskLayoutType.Pre_mount,
device_modifications=mods,
)

View File

@ -1,39 +1,58 @@
from pathlib import Path
from archinstall import Installer, disk, models, profile
from archinstall.default_profiles.minimal import MinimalProfile
from archinstall.lib.disk.device_handler import device_handler
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.installer import Installer
from archinstall.lib.models.device_model import (
DeviceModification,
DiskEncryption,
DiskLayoutConfiguration,
DiskLayoutType,
EncryptionType,
FilesystemType,
ModificationStatus,
PartitionFlag,
PartitionModification,
PartitionType,
Size,
Unit,
)
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.profile import profile_handler
# we're creating a new ext4 filesystem installation
fs_type = disk.FilesystemType('ext4')
fs_type = FilesystemType('ext4')
device_path = Path('/dev/sda')
# get the physical disk device
device = disk.device_handler.get_device(device_path)
device = device_handler.get_device(device_path)
if not device:
raise ValueError('No device found for given path')
# create a new modification for the specific device
device_modification = disk.DeviceModification(device, wipe=True)
device_modification = DeviceModification(device, wipe=True)
# create a new boot partition
boot_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
start=disk.Size(1, disk.Unit.MiB, device.device_info.sector_size),
length=disk.Size(512, disk.Unit.MiB, device.device_info.sector_size),
boot_partition = PartitionModification(
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=Size(1, Unit.MiB, device.device_info.sector_size),
length=Size(512, Unit.MiB, device.device_info.sector_size),
mountpoint=Path('/boot'),
fs_type=disk.FilesystemType.Fat32,
flags=[disk.PartitionFlag.BOOT]
fs_type=FilesystemType.Fat32,
flags=[PartitionFlag.BOOT]
)
device_modification.add_partition(boot_partition)
# create a root partition
root_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
start=disk.Size(513, disk.Unit.MiB, device.device_info.sector_size),
length=disk.Size(20, disk.Unit.GiB, device.device_info.sector_size),
root_partition = PartitionModification(
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=Size(513, Unit.MiB, device.device_info.sector_size),
length=Size(20, Unit.GiB, device.device_info.sector_size),
mountpoint=None,
fs_type=fs_type,
mount_options=[],
@ -44,9 +63,9 @@ start_home = root_partition.length
length_home = device.device_info.total_size - start_home
# create a new home partition
home_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
home_partition = PartitionModification(
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=start_home,
length=length_home,
mountpoint=Path('/home'),
@ -55,21 +74,21 @@ home_partition = disk.PartitionModification(
)
device_modification.add_partition(home_partition)
disk_config = disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Default,
disk_config = DiskLayoutConfiguration(
config_type=DiskLayoutType.Default,
device_modifications=[device_modification]
)
# disk encryption configuration (Optional)
disk_encryption = disk.DiskEncryption(
disk_encryption = DiskEncryption(
encryption_password="enc_password",
encryption_type=disk.EncryptionType.Luks,
encryption_type=EncryptionType.Luks,
partitions=[home_partition],
hsm_device=None
)
# initiate file handler with the disk config and the optional disk encryption config
fs_handler = disk.FilesystemHandler(disk_config, disk_encryption)
fs_handler = FilesystemHandler(disk_config, disk_encryption)
# perform all file operations
# WARNING: this will potentially format the filesystem and delete all data
@ -89,8 +108,8 @@ with Installer(
# Optionally, install a profile of choice.
# In this case, we install a minimal profile that is empty
profile_config = profile.ProfileConfiguration(MinimalProfile())
profile.profile_handler.install_profile_config(installation, profile_config)
profile_config = ProfileConfiguration(MinimalProfile())
profile_handler.install_profile_config(installation, profile_config)
user = models.User('archinstall', 'password', True)
user = User('archinstall', 'password', True)
installation.create_users(user)

View File

@ -1,21 +1,24 @@
from pathlib import Path
import archinstall
from archinstall import SysInfo, debug, info
from archinstall.lib import disk, locale
from archinstall import SysInfo, debug, error, info
from archinstall.lib.args import ArchConfig, arch_config_handler
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.general import run_custom_user_commands
from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.installer import Installer
from archinstall.lib.installer import Installer, accessibility_tools_in_use
from archinstall.lib.interactions.general_conf import ask_chroot
from archinstall.lib.models import AudioConfiguration, Bootloader
from archinstall.lib.models.device_model import (
DiskLayoutConfiguration,
DiskLayoutType,
EncryptionType,
)
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui import Tui
if archinstall.arguments.get('help'):
print("See `man archinstall` for help.")
exit(0)
def ask_user_questions() -> None:
"""
@ -25,10 +28,10 @@ def ask_user_questions() -> None:
"""
with Tui():
global_menu = GlobalMenu(data_store=archinstall.arguments)
global_menu = GlobalMenu(arch_config_handler.config)
if not archinstall.arguments.get('advanced', False):
global_menu.set_enabled('parallel downloads', False)
if not arch_config_handler.args.advanced:
global_menu.set_enabled('parallel_downloads', False)
global_menu.run()
@ -40,111 +43,114 @@ def perform_installation(mountpoint: Path) -> None:
formatted and setup prior to entering this function.
"""
info('Starting installation...')
disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config']
config: ArchConfig = arch_config_handler.config
if not config.disk_config:
error("No disk configuration provided")
return
# Retrieve list of additional repositories and set boolean values appropriately
enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', [])
enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', [])
run_mkinitcpio = not archinstall.arguments.get('uki')
locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config']
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
disk_config: DiskLayoutConfiguration = config.disk_config
enable_testing = 'testing' in config.additional_repositories
enable_multilib = 'multilib' in config.additional_repositories
run_mkinitcpio = not config.uki
locale_config = config.locale_config
disk_encryption = config.disk_encryption
with Installer(
mountpoint,
disk_config,
disk_encryption=disk_encryption,
kernels=archinstall.arguments.get('kernels', ['linux'])
kernels=config.kernels
) as installation:
# Mount all the drives to the desired mountpoint
if disk_config.config_type != disk.DiskLayoutType.Pre_mount:
if disk_config.config_type != DiskLayoutType.Pre_mount:
installation.mount_ordered_layout()
installation.sanity_check()
if disk_config.config_type != disk.DiskLayoutType.Pre_mount:
if disk_encryption and disk_encryption.encryption_type != disk.EncryptionType.NoEncryption:
if disk_config.config_type != DiskLayoutType.Pre_mount:
if disk_encryption and disk_encryption.encryption_type != EncryptionType.NoEncryption:
# generate encryption key files for the mounted luks devices
installation.generate_key_files()
if mirror_config := archinstall.arguments.get('mirror_config', None):
if mirror_config := config.mirror_config:
installation.set_mirrors(mirror_config, on_target=False)
installation.minimal_installation(
testing=enable_testing,
multilib=enable_multilib,
mkinitcpio=run_mkinitcpio,
hostname=archinstall.arguments.get('hostname', 'archlinux'),
hostname=arch_config_handler.config.hostname,
locale_config=locale_config
)
if mirror_config := archinstall.arguments.get('mirror_config', None):
if mirror_config := config.mirror_config:
installation.set_mirrors(mirror_config, on_target=True)
if archinstall.arguments.get('swap'):
if config.swap:
installation.setup_swap('zram')
if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi():
if config.bootloader == Bootloader.Grub and SysInfo.has_uefi():
installation.add_additional_packages("grub")
installation.add_bootloader(
archinstall.arguments["bootloader"],
archinstall.arguments.get('uki', False)
)
installation.add_bootloader(config.bootloader, config.uki)
# If user selected to copy the current ISO network configuration
# Perform a copy of the config
network_config: NetworkConfiguration | None = archinstall.arguments.get('network_config', None)
network_config: NetworkConfiguration | None = config.network_config
if network_config:
network_config.install_network_config(
installation,
archinstall.arguments.get('profile_config', None)
config.profile_config
)
if users := archinstall.arguments.get('!users', None):
if users := config.users:
installation.create_users(users)
audio_config: AudioConfiguration | None = archinstall.arguments.get('audio_config', None)
audio_config: AudioConfiguration | None = config.audio_config
if audio_config:
audio_config.install_audio_config(installation)
else:
info("No audio server will be installed")
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
installation.add_additional_packages(archinstall.arguments.get('packages', None))
if config.packages and config.packages[0] != '':
installation.add_additional_packages(config.packages)
if profile_config := archinstall.arguments.get('profile_config', None):
if profile_config := config.profile_config:
profile_handler.install_profile_config(installation, profile_config)
if timezone := archinstall.arguments.get('timezone', None):
if timezone := config.timezone:
installation.set_timezone(timezone)
if archinstall.arguments.get('ntp', False):
if config.ntp:
installation.activate_time_synchronization()
if archinstall.accessibility_tools_in_use():
if accessibility_tools_in_use():
installation.enable_espeakup()
if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw):
if (root_pw := config.root_password) and len(root_pw):
installation.user_set_pw('root', root_pw)
if profile_config := archinstall.arguments.get('profile_config', None):
if (profile_config := config.profile_config) and profile_config.profile:
profile_config.profile.post_install(installation)
# If the user provided a list of services to be enabled, pass the list to the enable_service function.
# Note that while it's called enable_service, it can actually take a list of services and iterate it.
if archinstall.arguments.get('services', None):
installation.enable_service(archinstall.arguments.get('services', []))
if servies := config.services:
installation.enable_service(servies)
# If the user provided custom commands to be run post-installation, execute them now.
if archinstall.arguments.get('custom-commands', None):
archinstall.run_custom_user_commands(archinstall.arguments['custom-commands'], installation)
if cc := config.custom_commands:
run_custom_user_commands(cc, installation)
installation.genfstab()
info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation")
if not archinstall.arguments.get('silent'):
if not arch_config_handler.args.silent:
with Tui():
chroot = ask_chroot()
@ -154,33 +160,35 @@ def perform_installation(mountpoint: Path) -> None:
except Exception:
pass
debug(f"Disk states after installing:\n{disk.disk_layouts()}")
debug(f"Disk states after installing:\n{disk_layouts()}")
def _guided() -> None:
if not archinstall.arguments.get('silent'):
def guided() -> None:
if not arch_config_handler.args.silent:
ask_user_questions()
config = ConfigurationOutput(archinstall.arguments)
config = ConfigurationOutput(arch_config_handler.config)
config.write_debug()
config.save()
if archinstall.arguments.get('dry_run'):
if arch_config_handler.args.dry_run:
exit(0)
if not archinstall.arguments.get('silent'):
if not arch_config_handler.args.silent:
with Tui():
if not config.confirm_config():
debug('Installation aborted')
_guided()
guided()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
if arch_config_handler.config.disk_config:
fs_handler = FilesystemHandler(
arch_config_handler.config.disk_config,
arch_config_handler.config.disk_encryption
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.arguments.get('mount_point', Path('/mnt')))
fs_handler.perform_filesystem_operations()
perform_installation(arch_config_handler.args.mountpoint)
_guided()
guided()

View File

@ -1,10 +1,11 @@
import time
import archinstall
from archinstall import info, profile
from archinstall.lib.output import info
from archinstall.lib.profile import profile_handler
from archinstall.lib.storage import storage
from archinstall.tui import Tui
for _profile in profile.profile_handler.get_mac_addr_profiles():
for _profile in profile_handler.get_mac_addr_profiles():
# Tailored means it's a match for this machine
# based on it's MAC address (or some other criteria
# that fits the requirements for this machine specifically).
@ -15,5 +16,5 @@ for _profile in profile.profile_handler.get_mac_addr_profiles():
Tui.print(f'{i}...')
time.sleep(1)
install_session = archinstall.storage['installation_session']
install_session = storage['installation_session']
_profile.install(install_session)

View File

@ -1,32 +1,37 @@
from pathlib import Path
import archinstall
from archinstall import ConfigurationOutput, Installer, debug, info
from archinstall.default_profiles.minimal import MinimalProfile
from archinstall.lib import disk
from archinstall.lib.interactions import select_devices, suggest_single_disk_layout
from archinstall.lib.args import ArchConfig, arch_config_handler
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.installer import Installer
from archinstall.lib.models import Bootloader, User
from archinstall.lib.profile import ProfileConfiguration, profile_handler
from archinstall.lib.models.device_model import DiskLayoutConfiguration
from archinstall.lib.models.network_configuration import NetworkConfiguration
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.output import debug, error, info
from archinstall.lib.profile import profile_handler
from archinstall.tui import Tui
info("Minimal only supports:")
info(" * Being installed to a single disk")
if archinstall.arguments.get('help', None):
info(" - Optional disk encryption via --!encryption-password=<password>")
info(" - Optional filesystem type via --filesystem=<fs type>")
info(" - Optional systemd network via --network")
def perform_installation(mountpoint: Path) -> None:
disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config']
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
config: ArchConfig = arch_config_handler.config
disk_config: DiskLayoutConfiguration | None = config.disk_config
if disk_config is None:
error("No disk configuration provided")
return
disk_encryption = config.disk_encryption
with Installer(
mountpoint,
disk_config,
disk_encryption=disk_encryption,
kernels=archinstall.arguments.get('kernels', ['linux'])
kernels=config.kernels
) as installation:
# Strap in the base system, add a boot loader and configure
# some other minor details as specified by this profile and user.
@ -34,9 +39,13 @@ def perform_installation(mountpoint: Path) -> None:
installation.set_hostname('minimal-arch')
installation.add_bootloader(Bootloader.Systemd)
# Optionally enable networking:
if archinstall.arguments.get('network', None):
installation.copy_iso_network_config(enable_services=True)
network_config: NetworkConfiguration | None = config.network_config
if network_config:
network_config.install_network_config(
installation,
config.profile_config
)
installation.add_additional_packages(['nano', 'wget', 'git'])
@ -53,61 +62,39 @@ def perform_installation(mountpoint: Path) -> None:
info(" * devel (password: devel)")
def prompt_disk_layout() -> None:
fs_type = None
if filesystem := archinstall.arguments.get('filesystem', None):
fs_type = disk.FilesystemType(filesystem)
devices = select_devices()
modifications = suggest_single_disk_layout(devices[0], filesystem_type=fs_type)
archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration(
config_type=disk.DiskLayoutType.Default,
device_modifications=[modifications]
)
def parse_disk_encryption() -> None:
if enc_password := archinstall.arguments.get('!encryption-password', None):
modification: list[disk.DeviceModification] = archinstall.arguments['disk_config']
partitions: list[disk.PartitionModification] = []
# encrypt all partitions except the /boot
for mod in modification:
partitions += [p for p in mod.partitions if p.mountpoint != Path('/boot')]
archinstall.arguments['disk_encryption'] = disk.DiskEncryption(
encryption_type=disk.EncryptionType.Luks,
encryption_password=enc_password,
partitions=partitions
)
def _minimal() -> None:
with Tui():
prompt_disk_layout()
parse_disk_encryption()
disk_config = DiskLayoutConfigurationMenu(disk_layout_config=None).run()
config = ConfigurationOutput(archinstall.arguments)
disk_encryption = None
if disk_config:
disk_encryption = DiskEncryptionMenu(disk_config).run()
arch_config_handler.config.disk_config = disk_config
arch_config_handler.config.disk_encryption = disk_encryption
config = ConfigurationOutput(arch_config_handler.config)
config.write_debug()
config.save()
if archinstall.arguments.get('dry_run'):
if arch_config_handler.args.dry_run:
exit(0)
if not archinstall.arguments.get('silent'):
if not arch_config_handler.args.silent:
with Tui():
if not config.confirm_config():
debug('Installation aborted')
_minimal()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
if arch_config_handler.config.disk_config:
fs_handler = FilesystemHandler(
arch_config_handler.config.disk_config,
arch_config_handler.config.disk_encryption
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.arguments.get('mount_point', Path('/mnt')))
fs_handler.perform_filesystem_operations()
perform_installation(arch_config_handler.args.mountpoint)
_minimal()

View File

@ -1,21 +1,24 @@
from pathlib import Path
import archinstall
from archinstall import debug
from archinstall.lib import disk
from archinstall.lib.args import ArchConfig, arch_config_handler
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.installer import Installer
from archinstall.lib.models.device_model import DiskLayoutConfiguration
from archinstall.lib.output import debug, error
from archinstall.tui import Tui
def ask_user_questions() -> None:
with Tui():
global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments)
global_menu = GlobalMenu(arch_config_handler.config)
global_menu.disable_all()
global_menu.set_enabled('archinstall-language', True)
global_menu.set_enabled('disk_config', True)
global_menu.set_enabled('disk_encryption', True)
global_menu.set_enabled('_disk_config', True)
global_menu.set_enabled('_disk_encryption', True)
global_menu.set_enabled('swap', True)
global_menu.set_enabled('save_config', True)
global_menu.set_enabled('install', True)
@ -30,18 +33,24 @@ def perform_installation(mountpoint: Path) -> None:
Only requirement is that the block devices are
formatted and setup prior to entering this function.
"""
disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config']
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
config: ArchConfig = arch_config_handler.config
if not config.disk_config:
error("No disk configuration provided")
return
disk_config: DiskLayoutConfiguration = config.disk_config
disk_encryption = config.disk_encryption
with Installer(
mountpoint,
disk_config,
disk_encryption=disk_encryption,
kernels=archinstall.arguments.get('kernels', ['linux'])
kernels=config.kernels
) as installation:
# Mount all the drives to the desired mountpoint
# This *can* be done outside of the installation, but the installer can deal with it.
if archinstall.arguments.get('disk_config'):
if disk_config:
installation.mount_ordered_layout()
# to generate a fstab directory holder. Avoids an error on exit and at the same time checks the procedure
@ -50,33 +59,35 @@ def perform_installation(mountpoint: Path) -> None:
target.parent.mkdir(parents=True)
# For support reasons, we'll log the disk layout post installation (crash or no crash)
debug(f"Disk states after installing:\n{disk.disk_layouts()}")
debug(f"Disk states after installing:\n{disk_layouts()}")
def _only_hd() -> None:
if not archinstall.arguments.get('silent'):
if not arch_config_handler.args.silent:
ask_user_questions()
config = ConfigurationOutput(archinstall.arguments)
config = ConfigurationOutput(arch_config_handler.config)
config.write_debug()
config.save()
if archinstall.arguments.get('dry_run'):
if arch_config_handler.args.dry_run:
exit(0)
if not archinstall.arguments.get('silent'):
if not arch_config_handler.args.silent:
with Tui():
if not config.confirm_config():
debug('Installation aborted')
_only_hd()
fs_handler = disk.FilesystemHandler(
archinstall.arguments['disk_config'],
archinstall.arguments.get('disk_encryption', None)
)
if arch_config_handler.config.disk_config:
fs_handler = FilesystemHandler(
arch_config_handler.config.disk_config,
arch_config_handler.config.disk_encryption
)
fs_handler.perform_filesystem_operations()
perform_installation(archinstall.arguments.get('mount_point', Path('/mnt')))
fs_handler.perform_filesystem_operations()
perform_installation(arch_config_handler.args.mountpoint)
_only_hd()

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "archinstall"
dynamic = ["version"]
version = "3.0.2"
description = "Arch Linux installer - guided, templates etc."
authors = [
{name = "Anton Hvornum", email = "anton@hvornum.se"},
@ -44,7 +44,6 @@ doc = ["sphinx"]
archinstall = "archinstall:run_as_a_module"
[tool.setuptools.dynamic]
version = {attr = "archinstall.__version__"}
readme = {file = ["README.rst", "USAGE.rst"]}
[tool.setuptools]
@ -106,6 +105,11 @@ module = "archinstall.lib.disk.*"
# 'Any' imports are allowed because pyparted doesn't have type hints
disallow_any_unimported = false
[[tool.mypy.overrides]]
module = "archinstall.lib.models.*"
# 'Any' imports are allowed because pyparted doesn't have type hints
disallow_any_unimported = false
[[tool.mypy.overrides]]
module = "archinstall.lib.packages"
disallow_any_explicit = true

View File

@ -3,6 +3,7 @@
"archinstall-language": "English",
"audio_config": {"audio": "pipewire"},
"bootloader": "Systemd-boot",
"services": ["service_1", "service_2"],
"disk_config": {
"config_type": "default_layout",
"device_modifications": [
@ -107,7 +108,8 @@
"sys_lang": "en_US"
},
"mirror_config": {
"mirror-regions": {
"custom_mirrors": [],
"mirror_regions": {
"Australia": [
"http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch"
]
@ -150,7 +152,10 @@
"main": "Desktop"
}
},
"custom_commands": [
"echo 'Hello, World!'"
],
"swap": false,
"timezone": "UTC",
"version": "2.8.6"
"version": "3.0.2"
}

View File

@ -2,16 +2,14 @@ from pathlib import Path
from pytest import MonkeyPatch
import archinstall
from archinstall.default_profiles.profile import GreeterType
from archinstall.lib.args import ArchConfig, ArchConfigHandler, Arguments
from archinstall.lib.disk import DiskLayoutConfiguration, DiskLayoutType
from archinstall.lib.hardware import GfxDriver
from archinstall.lib.locale import LocaleConfiguration
from archinstall.lib.mirrors import MirrorConfiguration
from archinstall.lib.models import Audio, AudioConfiguration, Bootloader, NetworkConfiguration, User
from archinstall.lib.mirrors import MirrorConfiguration, MirrorRegion
from archinstall.lib.models import Audio, AudioConfiguration, Bootloader, DiskLayoutConfiguration, DiskLayoutType, NetworkConfiguration, User
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.network_configuration import Nic, NicType
from archinstall.lib.profile.profile_model import ProfileConfiguration
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib.translationhandler import translation_handler
@ -27,7 +25,7 @@ def test_default_args(monkeypatch: MonkeyPatch) -> None:
silent=False,
dry_run=False,
script='guided',
mount_point=Path('/mnt'),
mountpoint=Path('/mnt'),
skip_ntp=False,
debug=False,
offline=False,
@ -77,7 +75,7 @@ def test_correct_parsing_args(
silent=True,
dry_run=True,
script='execution_script',
mount_point=Path('/tmp'),
mountpoint=Path('/mnt'),
skip_ntp=True,
debug=True,
offline=True,
@ -102,13 +100,13 @@ def test_config_file_parsing(
])
handler = ArchConfigHandler()
arch_config = handler.arch_config
arch_config = handler.config
# TODO: Use the real values from the test fixture instead of clearing out the entries
arch_config.disk_config.device_modifications = [] # type: ignore[union-attr]
assert arch_config == ArchConfig(
version=archinstall.__version__,
version='3.0.2',
locale_config=LocaleConfiguration(
kb_layout='us',
sys_lang='en_US',
@ -141,7 +139,12 @@ def test_config_file_parsing(
greeter=GreeterType.Lightdm
),
mirror_config=MirrorConfiguration(
mirror_regions=[],
mirror_regions=[
MirrorRegion(
name='Australia',
urls=['http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch']
)
],
custom_mirrors=[]
),
network_config=NetworkConfiguration(
@ -170,6 +173,9 @@ def test_config_file_parsing(
swap=False,
timezone='UTC',
additional_repositories=["testing"],
_users=[User(username='user_name', password='user_pwd', sudo=True)],
_disk_encryption=None
users=[User(username='user_name', password='user_pwd', sudo=True)],
disk_encryption=None,
services=['service_1', 'service_2'],
root_password='super_pwd',
custom_commands=["echo 'Hello, World!'"]
)

View File

@ -0,0 +1,58 @@
import json
from pathlib import Path
from pytest import MonkeyPatch
from archinstall.lib.args import ArchConfigHandler
from archinstall.lib.configuration import ConfigurationOutput
def test_user_config_roundtrip(
monkeypatch: MonkeyPatch,
config_fixture: Path,
) -> None:
monkeypatch.setattr('sys.argv', ['archinstall', '--config', str(config_fixture)])
handler = ArchConfigHandler()
arch_config = handler.config
config_output = ConfigurationOutput(arch_config)
test_out_dir = Path('/tmp/')
test_out_file = test_out_dir / config_output.user_configuration_file
config_output.save(test_out_dir)
result = json.loads(test_out_file.read_text())
expected = json.loads(config_fixture.read_text())
# the parsed config will check if the given device exists otherwise
# it will ignore the modification; as this test will run on various local systems
# and the CI pipeline there's no good way specify a real device so we'll simply
# copy the expected result to the actual result
result['disk_config']['config_type'] = expected['disk_config']['config_type']
result['disk_config']['device_modifications'] = expected['disk_config']['device_modifications']
assert sorted(result.items()) == sorted(expected.items())
def test_creds_roundtrip(
monkeypatch: MonkeyPatch,
creds_fixture: Path,
) -> None:
monkeypatch.setattr('sys.argv', ['archinstall', '--creds', str(creds_fixture)])
handler = ArchConfigHandler()
arch_config = handler.config
config_output = ConfigurationOutput(arch_config)
test_out_dir = Path('/tmp/')
test_out_file = test_out_dir / config_output.user_credentials_file
config_output.save(test_out_dir)
result = json.loads(test_out_file.read_text())
expected = json.loads(creds_fixture.read_text())
assert sorted(result.items()) == sorted(expected.items())