Fix 2379 - Mirror and region definitions (#3223)

* Fix 2379 - Mirror and repository settings

* Fix alignment
This commit is contained in:
Daniel Girtler 2025-03-05 22:19:50 +11:00 committed by GitHub
parent 5f7b16b152
commit 697ccd1ac5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1027 additions and 758 deletions

View File

@ -14,6 +14,7 @@ from pydantic.dataclasses import dataclass as p_dataclass
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.gen import Repository
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.output import error, warn
@ -61,7 +62,6 @@ class ArchConfig:
parallel_downloads: int = 0
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)
@ -92,7 +92,6 @@ class ArchConfig:
'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(),
@ -135,7 +134,14 @@ class ArchConfig:
arch_config.profile_config = ProfileConfiguration.parse_arg(profile_config)
if mirror_config := args_config.get('mirror_config', None):
arch_config.mirror_config = MirrorConfiguration.parse_args(mirror_config)
backwards_compatible_repo = []
if additional_repositories := args_config.get('additional-repositories', []):
backwards_compatible_repo = [Repository(r) for r in additional_repositories]
arch_config.mirror_config = MirrorConfiguration.parse_args(
mirror_config,
backwards_compatible_repo
)
if net_config := args_config.get('network_config', None):
arch_config.network_config = NetworkConfiguration.parse_arg(net_config)
@ -181,9 +187,6 @@ class ArchConfig:
if timezone := args_config.get('timezone', 'UTC'):
arch_config.timezone = timezone
if additional_repositories := args_config.get('additional-repositories', []):
arch_config.additional_repositories = additional_repositories
if services := args_config.get('services', []):
arch_config.services = services

View File

@ -23,7 +23,6 @@ from .interactions import (
ask_hostname,
ask_ntp,
ask_to_configure_network,
select_additional_repositories,
select_kernel,
)
from .locale.locale_menu import LocaleMenu
@ -75,7 +74,7 @@ class GlobalMenu(AbstractMenu):
key='locale_config'
),
MenuItem(
text=str(_('Mirrors')),
text=str(_('Mirrors and repositories')),
action=self._mirror_configuration,
preview_action=self._prev_mirror_config,
key='mirror_config'
@ -177,13 +176,6 @@ class GlobalMenu(AbstractMenu):
preview_action=self._prev_additional_pkgs,
key='packages'
),
MenuItem(
text=str(_('Optional repositories')),
action=select_additional_repositories,
value=[],
preview_action=self._prev_additional_repos,
key='additional_repositories'
),
MenuItem(
text=str(_('Timezone')),
action=ask_for_a_timezone,
@ -324,12 +316,6 @@ class GlobalMenu(AbstractMenu):
return output
return None
def _prev_additional_repos(self, item: MenuItem) -> str | None:
if item.value:
repos = ', '.join(item.value)
return f'{_("Additional repositories")}: {repos}'
return None
def _prev_tz(self, item: MenuItem) -> str | None:
if item.value:
return f'{_("Timezone")}: {item.value}'
@ -546,10 +532,27 @@ class GlobalMenu(AbstractMenu):
mirror_config: MirrorConfiguration = item.value
output = ''
if mirror_config.regions:
output += '{}: {}\n\n'.format(str(_('Mirror regions')), mirror_config.regions)
if mirror_config.custom_mirrors:
table = FormattedOutput.as_table(mirror_config.custom_mirrors)
output += '{}\n{}'.format(str(_('Custom mirrors')), table)
if mirror_config.mirror_regions:
title = str(_('Selected mirror regions'))
divider = '-' * len(title)
regions = mirror_config.region_names
output += f'{title}\n{divider}\n{regions}\n\n'
if mirror_config.custom_servers:
title = str(_('Custom servers'))
divider = '-' * len(title)
servers = mirror_config.custom_server_urls
output += f'{title}\n{divider}\n{servers}\n\n'
if mirror_config.optional_repositories:
title = str(_('Optional repositories'))
divider = '-' * len(title)
repos = ', '.join([r.value for r in mirror_config.optional_repositories])
output += f'{title}\n{divider}\n{repos}\n\n'
if mirror_config.custom_repositories:
title = str(_('Custom repositories'))
table = FormattedOutput.as_table(mirror_config.custom_repositories)
output += f'{title}:\n\n{table}'
return output.strip()

View File

@ -25,6 +25,7 @@ from archinstall.lib.models.device_model import (
SubvolumeModification,
Unit,
)
from archinstall.lib.models.gen import Repository
from archinstall.tui.curses_menu import Tui
from . import pacman
@ -482,7 +483,11 @@ class Installer:
def post_install_check(self, *args: str, **kwargs: str) -> list[str]:
return [step for step, flag in self.helper_flags.items() if flag is False]
def set_mirrors(self, mirror_config: MirrorConfiguration, on_target: bool = False) -> None:
def set_mirrors(
self,
mirror_config: MirrorConfiguration,
on_target: bool = False
) -> None:
"""
Set the mirror configuration for the installation.
@ -492,28 +497,35 @@ class Installer:
:on_target: Whether to set the mirrors on the target system or the live system.
:param on_target: bool
"""
debug('Setting mirrors')
debug('Setting mirrors on ' + ('target' if on_target else 'live system'))
for plugin in plugins.values():
if hasattr(plugin, 'on_mirrors'):
if result := plugin.on_mirrors(mirror_config):
mirror_config = result
mirrorlist_config = mirror_config.mirrorlist_config(speed_sort=True)
pacman_config = mirror_config.pacman_config()
root = self.target if on_target else Path('/')
mirrorlist_config = (root / 'etc/pacman.d/mirrorlist')
pacman_config = root / 'etc/pacman.conf'
if pacman_config:
debug(f'Pacman config: {pacman_config}')
repositories_config = mirror_config.repositories_config()
if repositories_config:
debug(f'Pacman config: {repositories_config}')
with open(root / 'etc/pacman.conf', 'a') as fp:
fp.write(pacman_config)
with open(pacman_config, 'a') as fp:
fp.write(repositories_config)
if mirrorlist_config:
debug(f'Mirrorlist: {mirrorlist_config}')
regions_config = mirror_config.regions_config(speed_sort=True)
if regions_config:
debug(f'Mirrorlist:\n{regions_config}')
mirrorlist_config.write_text(regions_config)
(root / 'etc/pacman.d/mirrorlist').write_text(mirrorlist_config)
custom_servers = mirror_config.custom_servers_config()
if custom_servers:
debug(f'Custom servers:\n{custom_servers}')
content = mirrorlist_config.read_text()
mirrorlist_config.write_text(f'{custom_servers}\n\n{content}')
def genfstab(self, flags: str = '-pU') -> None:
fstab_path = self.target / "etc" / "fstab"
@ -791,8 +803,7 @@ class Installer:
def minimal_installation(
self,
testing: bool = False,
multilib: bool = False,
optional_repositories: list[Repository] = [],
mkinitcpio: bool = True,
hostname: str | None = None,
locale_config: LocaleConfiguration | None = LocaleConfiguration.default()
@ -827,21 +838,11 @@ class Installer:
else:
debug('Archinstall will not install any ucode.')
# Determine whether to enable multilib/testing repositories before running pacstrap if testing flag is set.
debug(f'Optional respotories: {optional_repositories}')
# This action takes place on the host system as pacstrap copies over package repository lists.
pacman_conf = pacman.Config(self.target)
if multilib:
info("The multilib flag is set. This system will be installed with the multilib repository enabled.")
pacman_conf.enable(pacman.Repo.MULTILIB)
else:
info("The multilib flag is not set. This system will be installed without multilib repositories enabled.")
if testing:
info("The testing flag is set. This system will be installed with testing repositories enabled.")
pacman_conf.enable(pacman.Repo.TESTING)
else:
info("The testing flag is not set. This system will be installed without testing repositories enabled.")
pacman_conf.enable(optional_repositories)
pacman_conf.apply()
self.pacman.strap(self._base_packages)

View File

@ -13,7 +13,6 @@ from .general_conf import (
ask_for_audio_selection,
ask_hostname,
ask_ntp,
select_additional_repositories,
select_archinstall_language,
)
from .manage_users_conf import UserList, ask_for_additional_users

View File

@ -168,7 +168,7 @@ def ask_additional_packages_to_install(
preset: list[str] = [],
repositories: set[Repository] = set()
) -> list[str]:
Tui.print('Loading packages...', clear_screen=True)
Tui.print(str(_('Loading packages...')), clear_screen=True)
repositories |= {Repository.Core, Repository.Extra}
packages = list_available_packages(tuple(repositories))
@ -266,37 +266,6 @@ def add_number_of_parallel_downloads(preset: int | None = None) -> int | None:
return downloads
def select_additional_repositories(preset: list[str]) -> list[str]:
"""
Allows the user to select additional repositories (multilib, and testing) if desired.
:return: The string as a selected repository
:rtype: string
"""
repositories = ["multilib", "testing"]
items = [MenuItem(r, value=r) for r in repositories]
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(preset)
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min('Additional repositories'),
allow_reset=True,
allow_skip=True,
multi=True
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return []
case ResultType.Selection:
return result.get_values()
def ask_chroot() -> bool:
prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) + '\n'
group = MenuItemGroup.yes_no()

View File

@ -1,10 +1,24 @@
import time
import urllib
from pathlib import Path
from typing import TYPE_CHECKING, override
from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, ResultType, SelectMenu
from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, ResultType, SelectMenu, Tui
from .menu import AbstractSubMenu, ListManager
from .models.mirrors import CustomMirror, MirrorConfiguration, MirrorRegion, SignCheck, SignOption, mirror_list_handler
from .output import FormattedOutput
from .models.gen import Repository
from .models.mirrors import (
CustomRepository,
CustomServer,
MirrorConfiguration,
MirrorRegion,
MirrorStatusEntryV3,
MirrorStatusListV3,
SignCheck,
SignOption,
)
from .networking import fetch_data_from_url
from .output import FormattedOutput, debug
if TYPE_CHECKING:
from collections.abc import Callable
@ -14,50 +28,50 @@ if TYPE_CHECKING:
_: Callable[[str], DeferredTranslation]
class CustomMirrorList(ListManager):
def __init__(self, custom_mirrors: list[CustomMirror]):
class CustomMirrorRepositoriesList(ListManager):
def __init__(self, custom_repositories: list[CustomRepository]):
self._actions = [
str(_('Add a custom mirror')),
str(_('Change custom mirror')),
str(_('Delete custom mirror'))
str(_('Add a custom repository')),
str(_('Change custom repository')),
str(_('Delete custom repository'))
]
super().__init__(
custom_mirrors,
custom_repositories,
[self._actions[0]],
self._actions[1:],
''
)
@override
def selected_action_display(self, selection: CustomMirror) -> str:
def selected_action_display(self, selection: CustomRepository) -> str:
return selection.name
@override
def handle_action(
self,
action: str,
entry: CustomMirror | None,
data: list[CustomMirror]
) -> list[CustomMirror]:
entry: CustomRepository | None,
data: list[CustomRepository]
) -> list[CustomRepository]:
if action == self._actions[0]: # add
new_mirror = self._add_custom_mirror()
if new_mirror is not None:
data = [d for d in data if d.name != new_mirror.name]
data += [new_mirror]
elif action == self._actions[1] and entry: # modify mirror
new_mirror = self._add_custom_mirror(entry)
if new_mirror is not None:
new_repo = self._add_custom_repository()
if new_repo is not None:
data = [d for d in data if d.name != new_repo.name]
data += [new_repo]
elif action == self._actions[1] and entry: # modify repo
new_repo = self._add_custom_repository(entry)
if new_repo is not None:
data = [d for d in data if d.name != entry.name]
data += [new_mirror]
data += [new_repo]
elif action == self._actions[2] and entry: # delete
data = [d for d in data if d != entry]
return data
def _add_custom_mirror(self, preset: CustomMirror | None = None) -> CustomMirror | None:
def _add_custom_repository(self, preset: CustomRepository | None = None) -> CustomRepository | None:
edit_result = EditMenu(
str(_('Mirror name')),
str(_('Repository name')),
alignment=Alignment.CENTER,
allow_skip=True,
default_text=preset.name if preset else None
@ -133,7 +147,66 @@ class CustomMirrorList(ListManager):
case _:
raise ValueError('Unhandled return type')
return CustomMirror(name, url, sign_check, sign_opt)
return CustomRepository(name, url, sign_check, sign_opt)
class CustomMirrorServersList(ListManager):
def __init__(self, custom_servers: list[CustomServer]):
self._actions = [
str(_('Add a custom server')),
str(_('Change custom server')),
str(_('Delete custom server'))
]
super().__init__(
custom_servers,
[self._actions[0]],
self._actions[1:],
''
)
@override
def selected_action_display(self, selection: CustomServer) -> str:
return selection.url
@override
def handle_action(
self,
action: str,
entry: CustomServer | None,
data: list[CustomServer]
) -> list[CustomServer]:
if action == self._actions[0]: # add
new_server = self._add_custom_server()
if new_server is not None:
data = [d for d in data if d.url != new_server.url]
data += [new_server]
elif action == self._actions[1] and entry: # modify repo
new_server = self._add_custom_server(entry)
if new_server is not None:
data = [d for d in data if d.url != entry.url]
data += [new_server]
elif action == self._actions[2] and entry: # delete
data = [d for d in data if d != entry]
return data
def _add_custom_server(self, preset: CustomServer | None = None) -> CustomServer | None:
edit_result = EditMenu(
str(_('Server url')),
alignment=Alignment.CENTER,
allow_skip=True,
default_text=preset.url if preset else None
).input()
match edit_result.type_:
case ResultType.Selection:
uri = edit_result.text()
return CustomServer(uri)
case ResultType.Skip:
return preset
return None
class MirrorMenu(AbstractSubMenu):
@ -158,18 +231,32 @@ class MirrorMenu(AbstractSubMenu):
def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=str(_('Mirror region')),
text=str(_('Select regions')),
action=select_mirror_regions,
value=self._mirror_config.mirror_regions,
preview_action=self._prev_regions,
key='mirror_regions'
),
MenuItem(
text=str(_('Custom mirrors')),
text=str(_('Add custom servers')),
action=add_custom_mirror_servers,
value=self._mirror_config.custom_servers,
preview_action=self._prev_custom_servers,
key='custom_servers'
),
MenuItem(
text=str(_('Optional repositories')),
action=select_optional_repositories,
value=[],
preview_action=self._prev_additional_repos,
key='optional_repositories'
),
MenuItem(
text=str(_('Add custom repository')),
action=select_custom_mirror,
value=self._mirror_config.custom_mirrors,
value=self._mirror_config.custom_repositories,
preview_action=self._prev_custom_mirror,
key='custom_mirrors'
key='custom_repositories'
)
]
@ -187,14 +274,29 @@ class MirrorMenu(AbstractSubMenu):
return output
def _prev_additional_repos(self, item: MenuItem) -> str | None:
if item.value:
repositories: list[Repository] = item.value
repos = ', '.join([repo.value for repo in repositories])
return f'{_("Additional repositories")}: {repos}'
return None
def _prev_custom_mirror(self, item: MenuItem) -> str | None:
if not item.value:
return None
custom_mirrors: list[CustomMirror] = item.value
custom_mirrors: list[CustomRepository] = item.value
output = FormattedOutput.as_table(custom_mirrors)
return output.strip()
def _prev_custom_servers(self, item: MenuItem) -> str | None:
if not item.value:
return None
custom_servers: list[CustomServer] = item.value
output = '\n'.join([server.url for server in custom_servers])
return output.strip()
@override
def run(self) -> MirrorConfiguration:
super().run()
@ -202,6 +304,8 @@ class MirrorMenu(AbstractSubMenu):
def select_mirror_regions(preset: list[MirrorRegion]) -> list[MirrorRegion]:
Tui.print(str(_('Loading mirror regions...')), clear_screen=True)
mirror_list_handler.load_mirrors()
available_regions = mirror_list_handler.get_mirror_regions()
@ -234,6 +338,182 @@ def select_mirror_regions(preset: list[MirrorRegion]) -> list[MirrorRegion]:
return selected_mirrors
def select_custom_mirror(preset: list[CustomMirror] = []):
custom_mirrors = CustomMirrorList(preset).run()
def add_custom_mirror_servers(preset: list[CustomServer] = []):
custom_mirrors = CustomMirrorServersList(preset).run()
return custom_mirrors
def select_custom_mirror(preset: list[CustomRepository] = []):
custom_mirrors = CustomMirrorRepositoriesList(preset).run()
return custom_mirrors
def select_optional_repositories(preset: list[Repository]) -> list[Repository]:
"""
Allows the user to select additional repositories (multilib, and testing) if desired.
:return: The string as a selected repository
:rtype: Repository
"""
repositories = [Repository.Multilib, Repository.Testing]
items = [MenuItem(r.value, value=r) for r in repositories]
group = MenuItemGroup(items, sort_items=True)
group.set_selected_by_value(preset)
result = SelectMenu(
group,
alignment=Alignment.CENTER,
frame=FrameProperties.min('Additional repositories'),
allow_reset=True,
allow_skip=True,
multi=True
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Reset:
return []
case ResultType.Selection:
return result.get_values()
class MirrorListHandler:
def __init__(
self,
local_mirrorlist: Path = Path('/etc/pacman.d/mirrorlist'),
) -> None:
self._local_mirrorlist = local_mirrorlist
self._status_mappings: dict[str, list[MirrorStatusEntryV3]] | None = None
def _mappings(self) -> dict[str, list[MirrorStatusEntryV3]]:
if self._status_mappings is None:
self.load_mirrors()
assert self._status_mappings is not None
return self._status_mappings
def get_mirror_regions(self) -> list[MirrorRegion]:
available_mirrors = []
mappings = self._mappings()
for region_name, status_entry in mappings.items():
urls = [entry.server_url for entry in status_entry]
region = MirrorRegion(region_name, urls)
available_mirrors.append(region)
return available_mirrors
def load_mirrors(self) -> None:
from .args import arch_config_handler
if arch_config_handler.args.offline:
self.load_local_mirrors()
else:
if not self.load_remote_mirrors():
self.load_local_mirrors()
def load_remote_mirrors(self) -> bool:
url = "https://archlinux.org/mirrors/status/json/"
attempts = 3
for attempt_nr in range(attempts):
try:
mirrorlist = fetch_data_from_url(url)
self._status_mappings = self._parse_remote_mirror_list(mirrorlist)
return True
except Exception as e:
debug(f'Error while fetching mirror list: {e}')
time.sleep(attempt_nr + 1)
debug('Unable to fetch mirror list remotely, falling back to local mirror list')
return False
def load_local_mirrors(self) -> None:
with self._local_mirrorlist.open('r') as fp:
mirrorlist = fp.read()
self._status_mappings = self._parse_locale_mirrors(mirrorlist)
def get_status_by_region(self, region: str, speed_sort: bool) -> list[MirrorStatusEntryV3]:
mappings = self._mappings()
region_list = mappings[region]
return sorted(region_list, key=lambda mirror: (mirror.score, mirror.speed))
def _parse_remote_mirror_list(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]] | None:
mirror_status = MirrorStatusListV3.model_validate_json(mirrorlist)
sorting_placeholder: dict[str, list[MirrorStatusEntryV3]] = {}
for mirror in mirror_status.urls:
# We filter out mirrors that have bad criteria values
if any([
mirror.active is False, # Disabled by mirror-list admins
mirror.last_sync is None, # Has not synced recently
# mirror.score (error rate) over time reported from backend:
# https://github.com/archlinux/archweb/blob/31333d3516c91db9a2f2d12260bd61656c011fd1/mirrors/utils.py#L111C22-L111C66
(mirror.score is None or mirror.score >= 100),
]):
continue
if mirror.country == "":
# TODO: This should be removed once RFC!29 is merged and completed
# Until then, there are mirrors which lacks data in the backend
# and there is no way of knowing where they're located.
# So we have to assume world-wide
mirror.country = "Worldwide"
if mirror.url.startswith('http'):
sorting_placeholder.setdefault(mirror.country, []).append(mirror)
sorted_by_regions: dict[str, list[MirrorStatusEntryV3]] = dict({
region: unsorted_mirrors
for region, unsorted_mirrors in sorted(sorting_placeholder.items(), key=lambda item: item[0])
})
return sorted_by_regions
def _parse_locale_mirrors(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]] | None:
lines = mirrorlist.splitlines()
# remove empty lines
# lines = [line for line in lines if line]
mirror_list: dict[str, list[MirrorStatusEntryV3]] = {}
current_region = ''
for line in lines:
line = line.strip()
if line.startswith('## '):
current_region = line.replace('## ', '').strip()
mirror_list.setdefault(current_region, [])
if line.startswith('Server = '):
if not current_region:
current_region = 'Local'
mirror_list.setdefault(current_region, [])
url = line.removeprefix('Server = ')
mirror_entry = MirrorStatusEntryV3(
url=url.removesuffix('$repo/os/$arch'),
protocol=urllib.parse.urlparse(url).scheme,
active=True,
country=current_region or 'Worldwide',
# The following values are normally populated by
# archlinux.org mirror-list endpoint, and can't be known
# from just the local mirror-list file.
country_code='WW',
isos=True,
ipv4=True,
ipv6=True,
details='Locally defined mirror',
)
mirror_list[current_region].append(mirror_entry)
return mirror_list
mirror_list_handler = MirrorListHandler()

View File

@ -27,9 +27,9 @@ from .device_model import (
Unit,
_DeviceInfo,
)
from .gen import LocalPackage, PackageSearch, PackageSearchResult
from .gen import LocalPackage, PackageSearch, PackageSearchResult, Repository
from .locale import LocaleConfiguration
from .mirrors import CustomMirror, MirrorConfiguration, MirrorRegion
from .mirrors import CustomRepository, MirrorConfiguration, MirrorRegion
from .network_configuration import NetworkConfiguration, Nic, NicType
from .profile_model import ProfileConfiguration
from .users import PasswordStrength, User

View File

@ -1,17 +1,16 @@
import datetime
import http.client
import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any, override
from pydantic import BaseModel, field_validator, model_validator
from ..networking import DownloadTimer, fetch_data_from_url, ping
from ..models.gen import Repository
from ..networking import DownloadTimer, ping
from ..output import debug
if TYPE_CHECKING:
@ -153,146 +152,6 @@ class MirrorRegion:
return self.name == other.name
class MirrorListHandler:
def __init__(
self,
local_mirrorlist: Path = Path('/etc/pacman.d/mirrorlist'),
) -> None:
self._local_mirrorlist = local_mirrorlist
self._status_mappings: dict[str, list[MirrorStatusEntryV3]] | None = None
def _mappings(self) -> dict[str, list[MirrorStatusEntryV3]]:
if self._status_mappings is None:
self.load_mirrors()
assert self._status_mappings is not None
return self._status_mappings
def get_mirror_regions(self) -> list[MirrorRegion]:
available_mirrors = []
mappings = self._mappings()
for region_name, status_entry in mappings.items():
urls = [entry.server_url for entry in status_entry]
region = MirrorRegion(region_name, urls)
available_mirrors.append(region)
return available_mirrors
def load_mirrors(self) -> None:
from ..args import arch_config_handler
if arch_config_handler.args.offline:
self.load_local_mirrors()
else:
if not self.load_remote_mirrors():
self.load_local_mirrors()
def load_remote_mirrors(self) -> bool:
url = "https://archlinux.org/mirrors/status/json/"
attempts = 3
for attempt_nr in range(attempts):
try:
mirrorlist = fetch_data_from_url(url)
self._status_mappings = self._parse_remote_mirror_list(mirrorlist)
return True
except Exception as e:
debug(f'Error while fetching mirror list: {e}')
time.sleep(attempt_nr + 1)
debug('Unable to fetch mirror list remotely, falling back to local mirror list')
return False
def load_local_mirrors(self) -> None:
with self._local_mirrorlist.open('r') as fp:
mirrorlist = fp.read()
self._status_mappings = self._parse_locale_mirrors(mirrorlist)
def get_status_by_region(self, region: str, speed_sort: bool) -> list[MirrorStatusEntryV3]:
mappings = self._mappings()
region_list = mappings[region]
return sorted(region_list, key=lambda mirror: (mirror.score, mirror.speed))
def _parse_remote_mirror_list(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]] | None:
mirror_status = MirrorStatusListV3.model_validate_json(mirrorlist)
sorting_placeholder: dict[str, list[MirrorStatusEntryV3]] = {}
for mirror in mirror_status.urls:
# We filter out mirrors that have bad criteria values
if any([
mirror.active is False, # Disabled by mirror-list admins
mirror.last_sync is None, # Has not synced recently
# mirror.score (error rate) over time reported from backend:
# https://github.com/archlinux/archweb/blob/31333d3516c91db9a2f2d12260bd61656c011fd1/mirrors/utils.py#L111C22-L111C66
(mirror.score is None or mirror.score >= 100),
]):
continue
if mirror.country == "":
# TODO: This should be removed once RFC!29 is merged and completed
# Until then, there are mirrors which lacks data in the backend
# and there is no way of knowing where they're located.
# So we have to assume world-wide
mirror.country = "Worldwide"
if mirror.url.startswith('http'):
sorting_placeholder.setdefault(mirror.country, []).append(mirror)
sorted_by_regions: dict[str, list[MirrorStatusEntryV3]] = dict({
region: unsorted_mirrors
for region, unsorted_mirrors in sorted(sorting_placeholder.items(), key=lambda item: item[0])
})
return sorted_by_regions
def _parse_locale_mirrors(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]] | None:
lines = mirrorlist.splitlines()
# remove empty lines
# lines = [line for line in lines if line]
mirror_list: dict[str, list[MirrorStatusEntryV3]] = {}
current_region = ''
for line in lines:
line = line.strip()
if line.startswith('## '):
current_region = line.replace('## ', '').strip()
mirror_list.setdefault(current_region, [])
if line.startswith('Server = '):
if not current_region:
current_region = 'Local'
mirror_list.setdefault(current_region, [])
url = line.removeprefix('Server = ')
mirror_entry = MirrorStatusEntryV3(
url=url.removesuffix('$repo/os/$arch'),
protocol=urllib.parse.urlparse(url).scheme,
active=True,
country=current_region or 'Worldwide',
# The following values are normally populated by
# archlinux.org mirror-list endpoint, and can't be known
# from just the local mirror-list file.
country_code='WW',
isos=True,
ipv4=True,
ipv6=True,
details='Locally defined mirror',
)
mirror_list[current_region].append(mirror_entry)
return mirror_list
mirror_list_handler = MirrorListHandler()
class SignCheck(Enum):
Never = 'Never'
Optional = 'Optional'
@ -305,7 +164,7 @@ class SignOption(Enum):
@dataclass
class CustomMirror:
class CustomRepository:
name: str
url: str
sign_check: SignCheck
@ -328,11 +187,11 @@ class CustomMirror:
}
@classmethod
def parse_args(cls, args: list[dict[str, str]]) -> list['CustomMirror']:
def parse_args(cls, args: list[dict[str, str]]) -> list['CustomRepository']:
configs = []
for arg in args:
configs.append(
CustomMirror(
CustomRepository(
arg['name'],
arg['url'],
SignCheck(arg['sign_check']),
@ -343,14 +202,41 @@ class CustomMirror:
return configs
@dataclass
class CustomServer:
url: str
def table_data(self) -> dict[str, str]:
return {'Url': self.url}
def json(self) -> dict[str, str]:
return {'url': self.url}
@classmethod
def parse_args(cls, args: list[dict[str, str]]) -> list['CustomServer']:
configs = []
for arg in args:
configs.append(
CustomServer(arg['url'])
)
return configs
@dataclass
class MirrorConfiguration:
mirror_regions: list[MirrorRegion] = field(default_factory=list)
custom_mirrors: list[CustomMirror] = field(default_factory=list)
custom_servers: list[CustomServer] = field(default_factory=list)
optional_repositories: list[Repository] = field(default_factory=list)
custom_repositories: list[CustomRepository] = field(default_factory=list)
@property
def regions(self) -> str:
return ', '.join([m.name for m in self.mirror_regions])
def region_names(self) -> str:
return '\n'.join([m.name for m in self.mirror_regions])
@property
def custom_server_urls(self) -> str:
return '\n'.join([s.url for s in self.custom_servers])
def json(self) -> dict[str, Any]:
regions = {}
@ -359,10 +245,22 @@ class MirrorConfiguration:
return {
'mirror_regions': regions,
'custom_mirrors': [c.json() for c in self.custom_mirrors]
'custom_servers': self.custom_servers,
'optional_repositories': [r.value for r in self.optional_repositories],
'custom_repositories': [c.json() for c in self.custom_repositories]
}
def mirrorlist_config(self, speed_sort: bool = True) -> str:
def custom_servers_config(self) -> str:
config = '## Custom Servers\n'
for server in self.custom_servers:
config += f'Server = {server.url}\n'
return config.strip()
def regions_config(self, speed_sort: bool = True) -> str:
from ..mirrors import mirror_list_handler
config = ''
for mirror_region in self.mirror_regions:
@ -376,23 +274,24 @@ class MirrorConfiguration:
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:
def repositories_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'
for repo in self.custom_repositories:
config += f'\n\n[{repo.name}]\n'
config += f'SigLevel = {repo.sign_check.value} {repo.sign_option.value}\n'
config += f'Server = {repo.url}\n'
return config
@classmethod
def parse_args(cls, args: dict[str, Any]) -> 'MirrorConfiguration':
def parse_args(
cls,
args: dict[str, Any],
backwards_compatible_repo: list[Repository] = []
) -> 'MirrorConfiguration':
config = MirrorConfiguration()
mirror_regions = args.get('mirror_regions', [])
@ -400,7 +299,21 @@ class MirrorConfiguration:
for region, urls in mirror_regions.items():
config.mirror_regions.append(MirrorRegion(region, urls))
if args.get('custom_servers', []):
config.custom_servers = CustomServer.parse_args(args['custom_servers'])
# backwards compatibility with the new custom_repository
if 'custom_mirrors' in args:
config.custom_mirrors = CustomMirror.parse_args(args['custom_mirrors'])
config.custom_repositories = CustomRepository.parse_args(args['custom_mirrors'])
if 'custom_repositories' in args:
config.custom_repositories = CustomRepository.parse_args(args['custom_repositories'])
if 'optional_repositories' in args:
config.optional_repositories = [Repository(r) for r in args['optional_repositories']]
if backwards_compatible_repo:
for r in backwards_compatible_repo:
if r not in config.optional_repositories:
config.optional_repositories.append(r)
return config

View File

@ -10,7 +10,6 @@ from ..general import SysCommand
from ..output import error, info, warn
from ..plugins import plugins
from .config import Config
from .repo import Repo
if TYPE_CHECKING:
from archinstall.lib.translationhandler import DeferredTranslation

View File

@ -2,33 +2,36 @@ import re
from pathlib import Path
from shutil import copy2
from .repo import Repo
from ..models.gen import Repository
class Config:
def __init__(self, target: Path):
self.path = Path("/etc") / "pacman.conf"
self.chroot_path = target / "etc" / "pacman.conf"
self.repos: list[Repo] = []
self._repositories: list[Repository] = []
def enable(self, repo: Repo) -> None:
self.repos.append(repo)
def enable(self, repo: Repository | list[Repository]) -> None:
if not isinstance(repo, list):
repo = [repo]
self._repositories += repo
def apply(self) -> None:
if not self.repos:
if not self._repositories:
return
if Repo.TESTING in self.repos:
if Repo.MULTILIB in self.repos:
repos_pattern = f'({Repo.MULTILIB}|.+-{Repo.TESTING})'
if Repository.Testing in self._repositories:
if Repository.Multilib in self._repositories:
repos_pattern = f'({Repository.Multilib.value}|.+-{Repository.Testing.value})'
else:
repos_pattern = f'(?!{Repo.MULTILIB}).+-{Repo.TESTING}'
repos_pattern = f'(?!{Repository.Multilib.value}).+-{Repository.Testing.value}'
else:
repos_pattern = Repo.MULTILIB
repos_pattern = Repository.Multilib.value
pattern = re.compile(rf"^#\s*\[{repos_pattern}\]$")
lines = iter(self.path.read_text().splitlines(keepends=True))
with open(self.path, 'w') as f:
for line in lines:
if pattern.match(line):
@ -39,5 +42,5 @@ class Config:
f.write(line)
def persist(self) -> None:
if self.repos:
if self._repositories:
copy2(self.path, self.chroot_path)

View File

@ -1,6 +0,0 @@
from enum import StrEnum, auto
class Repo(StrEnum):
MULTILIB = auto()
TESTING = auto()

View File

@ -51,13 +51,11 @@ def perform_installation(mountpoint: Path) -> None:
error("No disk configuration provided")
return
# Retrieve list of additional repositories and set boolean values appropriately
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
optional_repositories = config.mirror_config.optional_repositories if config.mirror_config else []
with Installer(
mountpoint,
@ -80,8 +78,7 @@ def perform_installation(mountpoint: Path) -> None:
installation.set_mirrors(mirror_config, on_target=False)
installation.minimal_installation(
testing=enable_testing,
multilib=enable_multilib,
optional_repositories=optional_repositories,
mkinitcpio=run_mkinitcpio,
hostname=arch_config_handler.config.hostname,
locale_config=locale_config

View File

@ -750,12 +750,13 @@ class SelectMenu(AbstractCurses):
return 0
lines = header.split('\n') if header else []
table_header = [line for line in lines if '|' in line]
table_header = [line for line in lines if '-' in line]
longest_header = len(table_header[0]) if table_header else 0
longest_entry = self._item_group.get_max_width()
delta = abs(longest_header - longest_entry)
offset = delta + 3 # 3 because it seems to align it...
delta = longest_header - longest_entry
offset = delta + 2 # 2 because it seems to align it...
return offset

View File

@ -53,165 +53,168 @@ The contents of :code:`https://domain.lan/config.json`:
.. code-block:: json
{
"__separator__": null,
"additional-repositories": [],
"archinstall-language": "English",
"audio_config": null,
"bootloader": "Systemd-boot",
"config_version": "2.6.0",
"debug": false,
"disk_config": {
"config_type": "manual_partitioning",
"device_modifications": [
{
"device": "/dev/sda",
"partitions": [
{
"btrfs": [],
"flags": [
"boot"
],
"fs_type": "fat32",
"length": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 99982592
},
"mount_options": [],
"mountpoint": "/boot",
"obj_id": "369f31a8-2781-4d6b-96e7-75680552b7c9",
"start": {
"sector_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 512
},
"total_size": null,
"unit": "sectors",
"value": 34
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"flags": [],
"fs_type": "fat32",
"length": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 100000000
},
"mount_options": [],
"mountpoint": "/efi",
"obj_id": "13cf2c96-8b0f-4ade-abaa-c530be589aad",
"start": {
"sector_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 512
},
"total_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 16106127360
},
"unit": "MB",
"value": 100
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"flags": [],
"fs_type": "ext4",
"length": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 15805127360
},
"mount_options": [],
"mountpoint": "/",
"obj_id": "3e75d045-21a4-429d-897e-8ec19a006e8b",
"start": {
"sector_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 512
},
"total_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 16106127360
},
"unit": "MB",
"value": 301
},
"status": "create",
"type": "primary"
}
],
"wipe": false
}
]
},
"disk_encryption": {
"encryption_type": "luks",
"partitions": [
"3e75d045-21a4-429d-897e-8ec19a006e8b"
]
},
"hostname": "archlinux",
"kernels": [
"linux"
],
"locale_config": {
"kb_layout": "us",
"sys_enc": "UTF-8",
"sys_lang": "en_US"
},
"mirror_config": {
"custom_mirrors": [],
"mirror_regions": {
"Sweden": [
"https://mirror.osbeck.com/archlinux/$repo/os/$arch",
"https://mirror.bahnhof.net/pub/archlinux/$repo/os/$arch",
"https://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch",
"https://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch",
"https://ftp.ludd.ltu.se/mirrors/archlinux/$repo/os/$arch",
"https://ftp.acc.umu.se/mirror/archlinux/$repo/os/$arch",
"http://mirror.bahnhof.net/pub/archlinux/$repo/os/$arch",
"http://ftpmirror.infania.net/mirror/archlinux/$repo/os/$arch",
"http://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch",
"http://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch",
"http://ftp.acc.umu.se/mirror/archlinux/$repo/os/$arch"
]
}
},
"network_config": {},
"no_pkg_lookups": false,
"ntp": true,
"offline": false,
"packages": [],
"parallel downloads": 0,
"profile_config": null,
"save_config": null,
"script": "guided",
"silent": false,
"swap": true,
"timezone": "UTC",
"version": "2.6.0"
}
{
"additional-repositories": [],
"archinstall-language": "English",
"audio_config": null,
"bootloader": "Systemd-boot",
"debug": false,
"disk_config": {
"config_type": "manual_partitioning",
"device_modifications": [
{
"device": "/dev/sda",
"partitions": [
{
"btrfs": [],
"flags": [
"boot"
],
"fs_type": "fat32",
"length": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 99982592
},
"mount_options": [],
"mountpoint": "/boot",
"obj_id": "369f31a8-2781-4d6b-96e7-75680552b7c9",
"start": {
"sector_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 512
},
"total_size": null,
"unit": "sectors",
"value": 34
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"flags": [],
"fs_type": "fat32",
"length": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 100000000
},
"mount_options": [],
"mountpoint": "/efi",
"obj_id": "13cf2c96-8b0f-4ade-abaa-c530be589aad",
"start": {
"sector_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 512
},
"total_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 16106127360
},
"unit": "MB",
"value": 100
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"flags": [],
"fs_type": "ext4",
"length": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 15805127360
},
"mount_options": [],
"mountpoint": "/",
"obj_id": "3e75d045-21a4-429d-897e-8ec19a006e8b",
"start": {
"sector_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 512
},
"total_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 16106127360
},
"unit": "MB",
"value": 301
},
"status": "create",
"type": "primary"
}
],
"wipe": false
}
]
},
"disk_encryption": {
"encryption_type": "luks",
"partitions": [
"3e75d045-21a4-429d-897e-8ec19a006e8b"
]
},
"hostname": "archlinux",
"kernels": [
"linux"
],
"locale_config": {
"kb_layout": "us",
"sys_enc": "UTF-8",
"sys_lang": "en_US"
},
"mirror_config": {
"custom_servers": [
{
"url": "https://mymirror.com/$repo/os/$arch"
}
],
"mirror_regions": {
"Australia": [
"http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch"
]
},
"optional_repositories": [
"testing"
],
"custom_repositories": [
{
"name": "myrepo",
"url": "https://myrepo.com/$repo/os/$arch",
"sign_check": "Required",
"sign_option": "TrustAll"
}
]
},
"network_config": {},
"no_pkg_lookups": false,
"ntp": true,
"offline": false,
"packages": [],
"parallel downloads": 0,
"profile_config": null,
"save_config": null,
"script": "guided",
"silent": false,
"swap": true,
"timezone": "UTC",
"version": "2.6.0"
}
``--config`` options
^^^^^^^^^^^^^^^^^^^^

View File

@ -1,133 +1,148 @@
{
"__separator__": null,
"config_version": "2.8.6",
"additional-repositories": [],
"archinstall-language": "English",
"audio_config": {"audio": "pipewire"},
"bootloader": "Systemd-boot",
"debug": false,
"disk_config": {
"config_type": "default_layout",
"device_modifications": [
{
"device": "/dev/sda",
"partitions": [
{
"btrfs": [],
"flags": [
"boot"
],
"fs_type": "fat32",
"size": {
"sector_size": null,
"unit": "MiB",
"value": 512
},
"mount_options": [],
"mountpoint": "/boot",
"obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0",
"start": {
"sector_size": null,
"unit": "MiB",
"value": 1
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"flags": [],
"fs_type": "ext4",
"size": {
"sector_size": null,
"unit": "GiB",
"value": 20
},
"mount_options": [],
"mountpoint": "/",
"obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208",
"start": {
"sector_size": null,
"unit": "MiB",
"value": 513
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"flags": [],
"fs_type": "ext4",
"size": {
"sector_size": null,
"unit": "Percent",
"value": 100
},
"mount_options": [],
"mountpoint": "/home",
"obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f",
"start": {
"sector_size": null,
"unit": "GiB",
"value": 20
},
"status": "create",
"type": "primary"
}
],
"wipe": true
}
]
},
"hostname": "archlinux",
"kernels": [
"linux"
],
"locale_config": {
"kb_layout": "us",
"sys_enc": "UTF-8",
"sys_lang": "en_US"
},
"mirror_config": {
"mirror-regions": {
"Australia": [
"http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch"
]
"archinstall-language": "English",
"audio_config": {
"audio": "pipewire"
},
"bootloader": "Systemd-boot",
"debug": false,
"disk_config": {
"config_type": "default_layout",
"device_modifications": [
{
"device": "/dev/sda",
"partitions": [
{
"btrfs": [],
"flags": [
"boot"
],
"fs_type": "fat32",
"size": {
"sector_size": null,
"unit": "MiB",
"value": 512
},
"mount_options": [],
"mountpoint": "/boot",
"obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0",
"start": {
"sector_size": null,
"unit": "MiB",
"value": 1
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"flags": [],
"fs_type": "ext4",
"size": {
"sector_size": null,
"unit": "GiB",
"value": 20
},
"mount_options": [],
"mountpoint": "/",
"obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208",
"start": {
"sector_size": null,
"unit": "MiB",
"value": 513
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"flags": [],
"fs_type": "ext4",
"size": {
"sector_size": null,
"unit": "Percent",
"value": 100
},
"mount_options": [],
"mountpoint": "/home",
"obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f",
"start": {
"sector_size": null,
"unit": "GiB",
"value": 20
},
"status": "create",
"type": "primary"
}
],
"wipe": true
}
},
"network_config": {
"type": "manual",
"nics": [
{
"iface": "eno1",
"ip": "192.168.1.15/24",
"dhcp": true,
"gateway": "192.168.1.1",
"dns": [
"192.168.1.1",
"9.9.9.9"
]
}
]
},
"hostname": "archlinux",
"kernels": [
"linux"
],
"locale_config": {
"kb_layout": "us",
"sys_enc": "UTF-8",
"sys_lang": "en_US"
},
"mirror_config": {
"custom_servers": [
{
"url": "https://mymirror.com/$repo/os/$arch"
}
],
"mirror_regions": {
"Australia": [
"http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch"
]
},
"no_pkg_lookups": false,
"ntp": true,
"offline": false,
"packages": [],
"parallel downloads": 0,
"profile_config": {
"gfx_driver": "All open-source (default)",
"greeter": "sddm",
"profile": {
"details": [
"KDE Plasma"
],
"main": "Desktop"
}
},
"script": "guided",
"silent": false,
"swap": true,
"timezone": "UTC",
"version": "2.8.6"
"optional_repositories": [
"testing"
],
"custom_repositories": [
{
"name": "myrepo",
"url": "https://myrepo.com/$repo/os/$arch",
"sign_check": "Required",
"sign_option": "TrustAll"
}
]
},
"network_config": {
"type": "manual",
"nics": [
{
"iface": "eno1",
"ip": "192.168.1.15/24",
"dhcp": true,
"gateway": "192.168.1.1",
"dns": [
"192.168.1.1",
"9.9.9.9"
]
}
]
},
"no_pkg_lookups": false,
"ntp": true,
"offline": false,
"packages": [],
"parallel downloads": 0,
"profile_config": {
"gfx_driver": "All open-source (default)",
"greeter": "sddm",
"profile": {
"details": [
"KDE Plasma"
],
"main": "Desktop"
}
},
"script": "guided",
"silent": false,
"swap": true,
"timezone": "UTC",
"version": "2.8.6"
}

View File

@ -50,13 +50,11 @@ def perform_installation(mountpoint: Path) -> None:
error("No disk configuration provided")
return
# Retrieve list of additional repositories and set boolean values appropriately
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
optional_repositories = config.mirror_config.optional_repositories if config.mirror_config else []
with Installer(
mountpoint,
@ -79,8 +77,7 @@ def perform_installation(mountpoint: Path) -> None:
installation.set_mirrors(mirror_config, on_target=False)
installation.minimal_installation(
testing=enable_testing,
multilib=enable_multilib,
optional_repositories=optional_repositories,
mkinitcpio=run_mkinitcpio,
hostname=arch_config_handler.config.hostname,
locale_config=locale_config

View File

@ -13,6 +13,11 @@ def creds_fixture() -> Path:
return Path(__file__).parent / 'data' / 'test_creds.json'
@pytest.fixture(scope='session')
def mirror_backwards_config() -> Path:
return Path(__file__).parent / 'data' / 'test_config_mirror_backwards.json'
@pytest.fixture(scope='session')
def mirrorlist_no_country_fixture() -> Path:
return Path(__file__).parent / 'data' / 'mirrorlists' / 'test_no_country'

View File

@ -1,161 +1,182 @@
{
"additional-repositories": ["testing"],
"archinstall-language": "English",
"audio_config": {"audio": "pipewire"},
"bootloader": "Systemd-boot",
"services": ["service_1", "service_2"],
"disk_config": {
"config_type": "default_layout",
"device_modifications": [
{
"device": "/dev/sda",
"partitions": [
{
"btrfs": [],
"dev_path": null,
"flags": [
"boot"
],
"fs_type": "fat32",
"size": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 512
},
"mount_options": [],
"mountpoint": "/boot",
"obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0",
"start": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 1
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"dev_path": null,
"flags": [],
"fs_type": "ext4",
"size": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "GiB",
"value": 32
},
"mount_options": [],
"mountpoint": "/",
"obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208",
"start": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 513
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"dev_path": null,
"flags": [],
"fs_type": "ext4",
"size": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "GiB",
"value": 32
},
"mount_options": [],
"mountpoint": "/home",
"obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f",
"start": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 33281
},
"status": "create",
"type": "primary"
}
],
"wipe": true
}
]
},
"hostname": "archy",
"kernels": [
"linux-zen"
],
"locale_config": {
"kb_layout": "us",
"sys_enc": "UTF-8",
"sys_lang": "en_US"
},
"mirror_config": {
"custom_mirrors": [],
"mirror_regions": {
"Australia": [
"http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch"
]
"archinstall-language": "English",
"audio_config": {
"audio": "pipewire"
},
"bootloader": "Systemd-boot",
"services": [
"service_1",
"service_2"
],
"disk_config": {
"config_type": "default_layout",
"device_modifications": [
{
"device": "/dev/sda",
"partitions": [
{
"btrfs": [],
"dev_path": null,
"flags": [
"boot"
],
"fs_type": "fat32",
"size": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 512
},
"mount_options": [],
"mountpoint": "/boot",
"obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0",
"start": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 1
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"dev_path": null,
"flags": [],
"fs_type": "ext4",
"size": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "GiB",
"value": 32
},
"mount_options": [],
"mountpoint": "/",
"obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208",
"start": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 513
},
"status": "create",
"type": "primary"
},
{
"btrfs": [],
"dev_path": null,
"flags": [],
"fs_type": "ext4",
"size": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "GiB",
"value": 32
},
"mount_options": [],
"mountpoint": "/home",
"obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f",
"start": {
"sector_size": {
"unit": "B",
"value": 512
},
"unit": "MiB",
"value": 33281
},
"status": "create",
"type": "primary"
}
],
"wipe": true
}
},
"network_config": {
"type": "manual",
"nics": [
]
},
"hostname": "archy",
"kernels": [
"linux-zen"
],
"locale_config": {
"kb_layout": "us",
"sys_enc": "UTF-8",
"sys_lang": "en_US"
},
"mirror_config": {
"custom_servers": [
{
"iface": "eno1",
"ip": "192.168.1.15/24",
"dhcp": true,
"gateway": "192.168.1.1",
"dns": [
"192.168.1.1",
"9.9.9.9"
]
"url": "https://mymirror.com/$repo/os/$arch"
}
],
"mirror_regions": {
"Australia": [
"http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch"
]
},
"ntp": true,
"packages": ["firefox"],
"parallel_downloads": 66,
"profile_config": {
"gfx_driver": "All open-source",
"greeter": "lightdm-gtk-greeter",
"profile": {
"custom_settings": {
"Hyprland": {
"seat_access": "polkit"
},
"Sway": {
"seat_access": "seatd"
}
},
"details": [
"Sway",
"Hyprland"
],
"main": "Desktop"
}
},
"custom_commands": [
"echo 'Hello, World!'"
"optional_repositories": [
"testing"
],
"swap": false,
"timezone": "UTC",
"version": "3.0.2"
"custom_repositories": [
{
"name": "myrepo",
"url": "https://myrepo.com/$repo/os/$arch",
"sign_check": "Required",
"sign_option": "TrustAll"
}
]
},
"network_config": {
"type": "manual",
"nics": [
{
"iface": "eno1",
"ip": "192.168.1.15/24",
"dhcp": true,
"gateway": "192.168.1.1",
"dns": [
"192.168.1.1",
"9.9.9.9"
]
}
]
},
"ntp": true,
"packages": [
"firefox"
],
"parallel_downloads": 66,
"profile_config": {
"gfx_driver": "All open-source",
"greeter": "lightdm-gtk-greeter",
"profile": {
"custom_settings": {
"Hyprland": {
"seat_access": "polkit"
},
"Sway": {
"seat_access": "seatd"
}
},
"details": [
"Sway",
"Hyprland"
],
"main": "Desktop"
}
},
"custom_commands": [
"echo 'Hello, World!'"
],
"swap": false,
"timezone": "UTC",
"version": "3.0.2"
}

View File

@ -0,0 +1,18 @@
{
"additional-repositories": ["testing"],
"mirror_config": {
"custom_mirrors": [
{
"name": "my_mirror",
"sign_check": "Optional",
"sign_option": "TrustedOnly",
"url": "example.com"
}
],
"mirror_regions": {
"Australia": [
"http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch"
]
}
}
}

View File

@ -5,9 +5,10 @@ from pytest import MonkeyPatch
from archinstall.default_profiles.profile import GreeterType
from archinstall.lib.args import ArchConfig, ArchConfigHandler, Arguments
from archinstall.lib.hardware import GfxDriver
from archinstall.lib.mirrors import MirrorConfiguration, MirrorRegion
from archinstall.lib.models import Audio, AudioConfiguration, Bootloader, DiskLayoutConfiguration, DiskLayoutType, NetworkConfiguration, User
from archinstall.lib.mirrors import CustomRepository, MirrorConfiguration, MirrorRegion, SignCheck, SignOption
from archinstall.lib.models import Audio, AudioConfiguration, Bootloader, DiskLayoutConfiguration, DiskLayoutType, NetworkConfiguration, Repository, User
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import CustomServer
from archinstall.lib.models.network_configuration import Nic, NicType
from archinstall.lib.models.profile_model import ProfileConfiguration
from archinstall.lib.profile.profiles_handler import profile_handler
@ -149,7 +150,16 @@ def test_config_file_parsing(
urls=['http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch']
)
],
custom_mirrors=[]
custom_servers=[CustomServer('https://mymirror.com/$repo/os/$arch')],
optional_repositories=[Repository.Testing],
custom_repositories=[
CustomRepository(
name='myrepo',
url='https://myrepo.com/$repo/os/$arch',
sign_check=SignCheck.Required,
sign_option=SignOption.TrustAll
)
]
),
network_config=NetworkConfiguration(
type=NicType.MANUAL,
@ -176,10 +186,42 @@ def test_config_file_parsing(
parallel_downloads=66,
swap=False,
timezone='UTC',
additional_repositories=["testing"],
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!'"]
)
def test_mirror_backwards_config_file_parsing(
monkeypatch: MonkeyPatch,
mirror_backwards_config: Path,
) -> None:
monkeypatch.setattr('sys.argv', [
'archinstall',
'--config',
str(mirror_backwards_config),
])
handler = ArchConfigHandler()
arch_config = handler.config
assert arch_config.mirror_config == MirrorConfiguration(
mirror_regions=[
MirrorRegion(
name='Australia',
urls=['http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch']
)
],
custom_servers=[],
optional_repositories=[Repository.Testing],
custom_repositories=[
CustomRepository(
name='my_mirror',
url='example.com',
sign_check=SignCheck.Optional,
sign_option=SignOption.TrustedOnly
)
]
)

View File

@ -37,7 +37,13 @@ def test_user_config_roundtrip(
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())
assert json.dumps(
result['mirror_config'],
sort_keys=True
) == json.dumps(
expected['mirror_config'],
sort_keys=True
)
def test_creds_roundtrip(

View File

@ -1,6 +1,6 @@
from pathlib import Path
from archinstall.lib.models.mirrors import MirrorListHandler
from archinstall.lib.mirrors import MirrorListHandler
def test_mirrorlist_no_country(mirrorlist_no_country_fixture: Path) -> None: