Skip to content

Commit afe3c74

Browse files
committed
Merge remote-tracking branch 'origin/master' into refactor-luks
2 parents 509c1b8 + 1764b49 commit afe3c74

22 files changed

Lines changed: 499 additions & 256 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
default_stages: ['pre-commit']
22
repos:
33
- repo: https://github.com/astral-sh/ruff-pre-commit
4-
rev: v0.15.6
4+
rev: v0.15.7
55
hooks:
66
# fix unused imports and sort them
77
- id: ruff

archinstall/default_profiles/desktop.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ def packages(self) -> list[str]:
3131
'openssh',
3232
'htop',
3333
'wget',
34-
'iwd',
35-
'wireless_tools',
3634
'smartmontools',
3735
'xdg-utils',
3836
]

archinstall/lib/args.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ def _process_creds_data(self, creds_data: str) -> dict[str, Any] | None:
501501
lambda p=prompt: get_password( # type: ignore[misc]
502502
header=p,
503503
allow_skip=False,
504-
skip_confirmation=True,
504+
no_confirmation=True,
505505
)
506506
)
507507

archinstall/lib/global_menu.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from archinstall.lib.interactions.system_conf import select_kernel, select_swap
1212
from archinstall.lib.locale.locale_menu import LocaleMenu
1313
from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey
14-
from archinstall.lib.mirrors import MirrorListHandler, MirrorMenu
14+
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
15+
from archinstall.lib.mirror.mirror_menu import MirrorMenu
1516
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
1617
from archinstall.lib.models.authentication import AuthenticationConfiguration
1718
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration

archinstall/lib/installer.py

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from archinstall.lib.exceptions import DiskError, HardwareIncompatibilityError, RequirementError, ServiceException, SysCallError
3131
from archinstall.lib.hardware import SysInfo
3232
from archinstall.lib.locale.utils import verify_keyboard_layout, verify_x11_keyboard_layout
33-
from archinstall.lib.mirrors import MirrorListHandler
33+
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
3434
from archinstall.lib.models.application import ZramAlgorithm
3535
from archinstall.lib.models.bootloader import Bootloader
3636
from archinstall.lib.models.device import (
@@ -805,14 +805,6 @@ def post_install_enable_networkd_resolved(*args: str, **kwargs: str) -> None:
805805

806806
return True
807807

808-
def configure_nm_iwd(self) -> None:
809-
# Create NetworkManager config directory and write iwd backend conf
810-
nm_conf_dir = self.target / 'etc/NetworkManager/conf.d'
811-
nm_conf_dir.mkdir(parents=True, exist_ok=True)
812-
813-
iwd_backend_conf = nm_conf_dir / 'wifi_backend.conf'
814-
iwd_backend_conf.write_text('[device]\nwifi.backend=iwd\n')
815-
816808
def mkinitcpio(self, flags: list[str]) -> bool:
817809
for plugin in plugins.values():
818810
if hasattr(plugin, 'on_mkinitcpio'):
@@ -1012,27 +1004,19 @@ def setup_btrfs_snapshot(
10121004
self._configure_grub_btrfsd(snapshot_type)
10131005
self.enable_service('grub-btrfsd.service')
10141006

1015-
def setup_swap(self, kind: str = 'zram', algo: ZramAlgorithm = ZramAlgorithm.ZSTD) -> None:
1016-
if kind == 'zram':
1017-
info('Setting up swap on zram')
1018-
self.pacman.strap('zram-generator')
1019-
# Get RAM size in MB from hardware info
1020-
ram_kb = SysInfo.mem_total()
1021-
# Convert KB to MB and divide by 2, with minimum of 4096 MB
1022-
size_mb = max(ram_kb // 2048, 4096)
1023-
info(f'Zram size: {size_mb} from RAM: {ram_kb}')
1024-
info(f'Zram compression algorithm: {algo.value}')
1025-
1026-
with open(f'{self.target}/etc/systemd/zram-generator.conf', 'w') as zram_conf:
1027-
zram_conf.write('[zram0]\n')
1028-
zram_conf.write(f'zram-size = {size_mb}\n')
1029-
zram_conf.write(f'compression-algorithm = {algo.value}\n')
1030-
1031-
self.enable_service('systemd-zram-setup@zram0.service')
1032-
1033-
self._zram_enabled = True
1034-
else:
1035-
raise ValueError('Archinstall currently only supports setting up swap on zram')
1007+
def setup_swap(self, algo: ZramAlgorithm = ZramAlgorithm.ZSTD) -> None:
1008+
info('Setting up swap on zram')
1009+
self.pacman.strap('zram-generator')
1010+
1011+
info(f'Zram compression algorithm: {algo.value}')
1012+
1013+
with open(f'{self.target}/etc/systemd/zram-generator.conf', 'w') as zram_conf:
1014+
zram_conf.write('[zram0]\n')
1015+
zram_conf.write(f'compression-algorithm = {algo.value}\n')
1016+
1017+
self.enable_service('systemd-zram-setup@zram0.service')
1018+
1019+
self._zram_enabled = True
10361020

10371021
def _get_efi_partition(self) -> PartitionModification | None:
10381022
for layout in self._disk_config.device_modifications:

archinstall/lib/menu/helpers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from textual.validation import ValidationResult, Validator
55

66
from archinstall.lib.translationhandler import tr
7-
from archinstall.tui.ui.components import InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen
7+
from archinstall.tui.ui.components import InputInfo, InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen
88
from archinstall.tui.ui.menu_item import MenuItemGroup
99
from archinstall.tui.ui.result import Result, ResultType
1010

@@ -138,6 +138,7 @@ def __init__(
138138
allow_skip: bool = True,
139139
allow_reset: bool = False,
140140
validator_callback: Callable[[str], str | None] | None = None,
141+
info_callback: Callable[[str], InputInfo | None] | None = None,
141142
):
142143
self._header = header
143144
self._placeholder = placeholder
@@ -146,6 +147,7 @@ def __init__(
146147
self._allow_skip = allow_skip
147148
self._allow_reset = allow_reset
148149
self._validator_callback = validator_callback
150+
self._info_callback = info_callback
149151

150152
async def show(self) -> Result[str]:
151153
validator = GenericValidator(self._validator_callback) if self._validator_callback else None
@@ -158,6 +160,7 @@ async def show(self) -> Result[str]:
158160
allow_skip=self._allow_skip,
159161
allow_reset=self._allow_reset,
160162
validator=validator,
163+
info_callback=self._info_callback,
161164
).run()
162165

163166
if result.type_ == ResultType.Reset:

archinstall/lib/menu/util.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,37 @@
33
from pathlib import Path
44

55
from archinstall.lib.menu.helpers import Confirmation, Input
6-
from archinstall.lib.models.users import Password
6+
from archinstall.lib.models.users import Password, PasswordStrength
77
from archinstall.lib.translationhandler import tr
8-
from archinstall.tui.ui.components import tui
8+
from archinstall.tui.ui.components import InputInfo, InputInfoType, tui
99
from archinstall.tui.ui.result import ResultType
1010

1111

1212
async def get_password(
1313
header: str | None = None,
1414
allow_skip: bool = False,
1515
preset: str | None = None,
16-
skip_confirmation: bool = False,
16+
no_confirmation: bool = False,
1717
) -> Password | None:
18+
def password_hint(value: str) -> InputInfo | None:
19+
if not value:
20+
return None
21+
strength = PasswordStrength.strength(value)
22+
if strength in (PasswordStrength.VERY_WEAK, PasswordStrength.WEAK):
23+
return InputInfo(message=tr('Password strength: Weak'), info_type=InputInfoType.MsgError)
24+
elif strength == PasswordStrength.MODERATE:
25+
return InputInfo(message=tr('Password strength: Moderate'), info_type=InputInfoType.MsgWarning)
26+
elif strength == PasswordStrength.STRONG:
27+
return InputInfo(message=tr('Password strength: Strong'), info_type=InputInfoType.MsgInfo)
28+
return None
29+
1830
while True:
1931
result = await Input(
2032
header=header,
2133
allow_skip=allow_skip,
2234
default_value=preset,
2335
password=True,
36+
info_callback=password_hint,
2437
).show()
2538

2639
if result.type_ == ResultType.Skip:
@@ -38,7 +51,7 @@ async def get_password(
3851
password = Password(plaintext=result.get_value())
3952
break
4053

41-
if skip_confirmation:
54+
if no_confirmation:
4255
return password
4356

4457
confirmation_header = f'{tr("Password")}: {password.hidden()}\n\n'
@@ -49,13 +62,16 @@ def _validate(value: str) -> str | None:
4962
return tr('The password did not match, please try again')
5063
return None
5164

52-
_ = await Input(
65+
result = await Input(
5366
header=confirmation_header,
54-
allow_skip=False,
67+
allow_skip=allow_skip,
5568
password=True,
5669
validator_callback=_validate,
5770
).show()
5871

72+
if result.type_ == ResultType.Skip:
73+
return None
74+
5975
return password
6076

6177

archinstall/lib/mirror/__init__.py

Whitespace-only changes.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import time
2+
import urllib
3+
from pathlib import Path
4+
5+
from archinstall.lib.models import MirrorRegion
6+
from archinstall.lib.models.mirrors import MirrorStatusEntryV3, MirrorStatusListV3
7+
from archinstall.lib.networking import fetch_data_from_url
8+
from archinstall.lib.output import debug, info
9+
10+
11+
class MirrorListHandler:
12+
def __init__(
13+
self,
14+
local_mirrorlist: Path = Path('/etc/pacman.d/mirrorlist'),
15+
offline: bool = False,
16+
verbose: bool = False,
17+
) -> None:
18+
self._local_mirrorlist = local_mirrorlist
19+
self._status_mappings: dict[str, list[MirrorStatusEntryV3]] | None = None
20+
self._fetched_remote: bool = False
21+
self.offline = offline
22+
self.verbose = verbose
23+
24+
def _mappings(self) -> dict[str, list[MirrorStatusEntryV3]]:
25+
if self._status_mappings is None:
26+
self.load_mirrors()
27+
28+
assert self._status_mappings is not None
29+
return self._status_mappings
30+
31+
def get_mirror_regions(self) -> list[MirrorRegion]:
32+
available_mirrors = []
33+
mappings = self._mappings()
34+
35+
for region_name, status_entry in mappings.items():
36+
urls = [entry.server_url for entry in status_entry]
37+
region = MirrorRegion(region_name, urls)
38+
available_mirrors.append(region)
39+
40+
return available_mirrors
41+
42+
def load_mirrors(self) -> None:
43+
if self.offline:
44+
self._fetched_remote = False
45+
self.load_local_mirrors()
46+
else:
47+
self._fetched_remote = self.load_remote_mirrors()
48+
debug(f'load mirrors: {self._fetched_remote}')
49+
if not self._fetched_remote:
50+
self.load_local_mirrors()
51+
52+
def load_remote_mirrors(self) -> bool:
53+
url = 'https://archlinux.org/mirrors/status/json/'
54+
attempts = 3
55+
56+
for attempt_nr in range(attempts):
57+
try:
58+
mirrorlist = fetch_data_from_url(url)
59+
self._status_mappings = self._parse_remote_mirror_list(mirrorlist)
60+
return True
61+
except Exception as e:
62+
debug(f'Error while fetching mirror list: {e}')
63+
time.sleep(attempt_nr + 1)
64+
65+
debug('Unable to fetch mirror list remotely, falling back to local mirror list')
66+
return False
67+
68+
def load_local_mirrors(self) -> None:
69+
with self._local_mirrorlist.open('r') as fp:
70+
mirrorlist = fp.read()
71+
self._status_mappings = self._parse_local_mirrors(mirrorlist)
72+
73+
def get_status_by_region(self, region: str, speed_sort: bool) -> list[MirrorStatusEntryV3]:
74+
mappings = self._mappings()
75+
region_list = mappings[region]
76+
77+
# Only sort if we have remote mirror data with score/speed info
78+
# Local mirrors lack this data and can be modified manually before-hand
79+
# Or reflector potentially ran already
80+
if self._fetched_remote and speed_sort:
81+
info('Sorting your selected mirror list based on the speed between you and the individual mirrors (this might take a while)')
82+
# Sort by speed descending (higher is better in bitrate form core.db download)
83+
return sorted(region_list, key=lambda mirror: -mirror.speed)
84+
# just return as-is without sorting?
85+
return region_list
86+
87+
def _parse_remote_mirror_list(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]]:
88+
context = {'verbose': self.verbose}
89+
mirror_status = MirrorStatusListV3.model_validate_json(mirrorlist, context=context)
90+
91+
sorting_placeholder: dict[str, list[MirrorStatusEntryV3]] = {}
92+
93+
for mirror in mirror_status.urls:
94+
# We filter out mirrors that have bad criteria values
95+
if any(
96+
[
97+
mirror.active is False, # Disabled by mirror-list admins
98+
mirror.last_sync is None, # Has not synced recently
99+
# mirror.score (error rate) over time reported from backend:
100+
# https://github.com/archlinux/archweb/blob/31333d3516c91db9a2f2d12260bd61656c011fd1/mirrors/utils.py#L111C22-L111C66
101+
(mirror.score is None or mirror.score >= 100),
102+
]
103+
):
104+
continue
105+
106+
if mirror.country == '':
107+
# TODO: This should be removed once RFC!29 is merged and completed
108+
# Until then, there are mirrors which lacks data in the backend
109+
# and there is no way of knowing where they're located.
110+
# So we have to assume world-wide
111+
mirror.country = 'Worldwide'
112+
113+
if mirror.url.startswith('http'):
114+
sorting_placeholder.setdefault(mirror.country, []).append(mirror)
115+
116+
sorted_by_regions: dict[str, list[MirrorStatusEntryV3]] = dict(
117+
{region: unsorted_mirrors for region, unsorted_mirrors in sorted(sorting_placeholder.items(), key=lambda item: item[0])}
118+
)
119+
120+
return sorted_by_regions
121+
122+
def _parse_local_mirrors(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]]:
123+
lines = mirrorlist.splitlines()
124+
125+
# remove empty lines
126+
# lines = [line for line in lines if line]
127+
128+
mirror_list: dict[str, list[MirrorStatusEntryV3]] = {}
129+
130+
current_region = ''
131+
132+
for line in lines:
133+
line = line.strip()
134+
135+
if line.startswith('## '):
136+
current_region = line.replace('## ', '').strip()
137+
mirror_list.setdefault(current_region, [])
138+
139+
if line.startswith('Server = '):
140+
if not current_region:
141+
current_region = 'Local'
142+
mirror_list.setdefault(current_region, [])
143+
144+
url = line.removeprefix('Server = ')
145+
146+
mirror_entry = MirrorStatusEntryV3(
147+
url=url.removesuffix('$repo/os/$arch'),
148+
protocol=urllib.parse.urlparse(url).scheme,
149+
active=True,
150+
country=current_region or 'Worldwide',
151+
# The following values are normally populated by
152+
# archlinux.org mirror-list endpoint, and can't be known
153+
# from just the local mirror-list file.
154+
country_code='WW',
155+
isos=True,
156+
ipv4=True,
157+
ipv6=True,
158+
details='Locally defined mirror',
159+
)
160+
161+
mirror_list[current_region].append(mirror_entry)
162+
163+
return mirror_list

0 commit comments

Comments
 (0)