Skip to content

Commit eae6bf5

Browse files
committed
feat: add partition detection to GUI installer + sync archiso files
1 parent cbd4ac2 commit eae6bf5

4 files changed

Lines changed: 249 additions & 32 deletions

File tree

‎archiso-mochios/airootfs/usr/lib/mochiinstall/installer.py‎

Lines changed: 143 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,95 @@
1212

1313
CUSTOM_EXTRAS = {"steam"}
1414

15+
FALLBACK_MIRRORS = [
16+
"https://mirrors.kernel.org/archlinux/$repo/os/$arch",
17+
"https://mirror.rackspace.com/archlinux/$repo/os/$arch",
18+
"https://archlinux.uk.mirror.allworldit.com/archlinux/$repo/os/$arch",
19+
"https://mirror.one.com/archlinux/$repo/os/$arch",
20+
"https://archlinux.thaller.ws/archlinux/$repo/os/$arch",
21+
"https://ftp.linux.org.tr/archlinux/$repo/os/$arch",
22+
]
23+
24+
25+
def detect_microcode():
26+
try:
27+
with open("/proc/cpuinfo") as f:
28+
cpuinfo = f.read().lower()
29+
if "authenticamd" in cpuinfo:
30+
return "amd-ucode"
31+
elif "genuineintel" in cpuinfo:
32+
return "intel-ucode"
33+
except Exception:
34+
pass
35+
return None
36+
37+
38+
def check_network(lfn, retries=3, delay=5):
39+
import urllib.request
40+
for attempt in range(retries):
41+
try:
42+
urllib.request.urlopen("https://archlinux.org", timeout=10)
43+
lfn("network connectivity verified")
44+
return True
45+
except Exception:
46+
if attempt < retries - 1:
47+
lfn(f"[yellow]no network, retrying in {delay}s... ({attempt+1}/{retries})[/]")
48+
time.sleep(delay)
49+
return False
50+
51+
52+
def rank_mirrors(lfn):
53+
if not shutil.which("reflector"):
54+
lfn("[yellow]reflector not found, skipping mirror ranking[/]")
55+
return False
56+
lfn("ranking mirrors by speed...")
57+
r = subprocess.run(
58+
["reflector", "--latest", "20", "--protocol", "https", "--sort", "rate",
59+
"--save", "/etc/pacman.d/mirrorlist"],
60+
capture_output=True, text=True, timeout=60
61+
)
62+
if r.returncode == 0:
63+
lfn("mirrors ranked successfully")
64+
return True
65+
lfn(f"[yellow]reflector failed: {r.stderr.strip()[:200]}[/]")
66+
return False
67+
68+
69+
def set_fallback_mirrors(lfn):
70+
lfn("setting fallback mirrors...")
71+
mirrorlist = "/etc/pacman.d/mirrorlist"
72+
try:
73+
with open(mirrorlist, "w") as f:
74+
f.write("## mochios fallback mirrors\n")
75+
for m in FALLBACK_MIRRORS:
76+
f.write(f"Server = {m}\n")
77+
lfn("fallback mirrors configured")
78+
except Exception as e:
79+
lfn(f"[yellow]could not write mirrorlist: {e}[/]")
80+
81+
82+
def pacstrap_with_fallback(target, pkgs, abort_flag, lfn, timeout=900):
83+
try:
84+
abortable_run(["pacstrap", "-K", target] + pkgs, abort_flag, timeout=timeout, log_fn=lfn)
85+
return True
86+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
87+
err = str(e)[:300]
88+
lfn(f"[yellow]pacstrap failed, trying fallback mirrors... ({err})[/]")
89+
set_fallback_mirrors(lfn)
90+
try:
91+
abortable_run(["pacstrap", "-K", target] + pkgs, abort_flag, timeout=timeout, log_fn=lfn)
92+
return True
93+
except Exception as e2:
94+
lfn(f"[red]pacstrap failed even with fallback mirrors: {e2}[/]")
95+
raise
96+
1597
_CLEANUP_TARGET = [None]
1698

1799
def _unmount_all():
18100
target = _CLEANUP_TARGET[0]
19101
if not target or not os.path.ismount(target):
20102
return
21-
for mp in [f"{target}/var/cache", f"{target}/var", f"{target}/home", f"{target}/boot", target]:
103+
for mp in [f"{target}/boot", f"{target}/var/cache", f"{target}/var", f"{target}/home", target]:
22104
subprocess.run(["umount", "-l", mp], capture_output=True, check=False, timeout=10)
23105
subprocess.run(["umount", "-l", "/mnt/btrfs_tmp"], capture_output=True, check=False, timeout=10)
24106
subprocess.run(["swapoff", "-a"], capture_output=True, check=False, timeout=10)
@@ -141,6 +223,45 @@ def retry_cmd(cmd, lfn, max_retries=5, delay=2, **kwargs):
141223
raise
142224

143225

226+
def detect_partitions(disk):
227+
"""detect existing partitions on a disk. returns list of dicts with name, size, type, mountpoint, fstype."""
228+
partitions = []
229+
try:
230+
r = subprocess.run(
231+
["lsblk", "-Jno", "NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,LABEL", disk],
232+
capture_output=True, text=True, timeout=5
233+
)
234+
import json
235+
data = json.loads(r.stdout)
236+
for dev in data.get("blockdevices", []):
237+
for child in dev.get("children", []):
238+
partitions.append({
239+
"name": f"/dev/{child['name']}",
240+
"size": child.get("size", "?"),
241+
"type": child.get("type", "?"),
242+
"fstype": child.get("fstype", ""),
243+
"mountpoint": child.get("mountpoint", ""),
244+
"label": child.get("label", ""),
245+
})
246+
except Exception:
247+
pass
248+
return partitions
249+
250+
251+
def detect_disk_partitions_summary(disk):
252+
"""return a human-readable summary of partitions on a disk."""
253+
parts = detect_partitions(disk)
254+
if not parts:
255+
return "no partitions found (empty or unformatted disk)"
256+
lines = []
257+
for p in parts:
258+
label = f" [{p['label']}]" if p["label"] else ""
259+
mount = f" mounted at {p['mountpoint']}" if p["mountpoint"] else ""
260+
fstype = f" ({p['fstype']})" if p["fstype"] else ""
261+
lines.append(f" {p['name']} {p['size']}{fstype}{label}{mount}")
262+
return "\n".join(lines)
263+
264+
144265
def select_disk(disk):
145266
if not disk or not disk.startswith("/dev/"):
146267
raise ValueError(f"invalid disk: {disk}")
@@ -194,7 +315,7 @@ def setup_btrfs(root_part, target, lfn):
194315
tmp = "/mnt/btrfs_tmp"
195316
os.makedirs(tmp, exist_ok=True)
196317
run(["mount", "-o", "subvol=/", root_part, tmp])
197-
for sv in ["root_a", "root_b", "home", "snapshots", "var", "cache"]:
318+
for sv in ["root_a", "root_b", "home", "var", "cache"]:
198319
run(["btrfs", "subvolume", "delete", f"{tmp}/{sv}"], check=False)
199320
run(["btrfs", "subvolume", "create", f"{tmp}/{sv}"])
200321
run(["umount", tmp])
@@ -241,13 +362,17 @@ def pacstrap_base(target, lfn, de="kde", greeter="sddm", bootloader="limine", ke
241362
"linux-firmware",
242363
"sudo", "vim", "nano",
243364
"gptfdisk",
244-
"networkmanager", "dhcpcd", "reflector",
365+
"networkmanager", "dhcpcd",
245366
"sbctl",
246367
"zsh", "git", "flatpak",
247368
]
369+
ucode = detect_microcode()
370+
if ucode:
371+
base.append(ucode)
372+
lfn(f"detected CPU microcode: {ucode}")
248373
if filesystem == "btrfs":
249374
base.extend(["btrfs-progs", "snapper"])
250-
abortable_run(["pacstrap", "-K", target] + base, abort_flag, timeout=900, log_fn=lfn)
375+
pacstrap_with_fallback(target, base, abort_flag, lfn, timeout=900)
251376

252377
# step 2: desktop environment
253378
de_pkgs = {
@@ -264,7 +389,7 @@ def pacstrap_base(target, lfn, de="kde", greeter="sddm", bootloader="limine", ke
264389
}
265390
de_choice = de_pkgs.get(de, de_pkgs["kde"])
266391
lfn(f"installing {de} desktop...")
267-
abortable_run(["pacstrap", "-K", target] + de_choice, abort_flag, timeout=600, log_fn=lfn)
392+
pacstrap_with_fallback(target, de_choice, abort_flag, lfn, timeout=600)
268393

269394
# step 3: greeter + bootloader + extras
270395
greeter_pkgs = {
@@ -282,7 +407,7 @@ def pacstrap_base(target, lfn, de="kde", greeter="sddm", bootloader="limine", ke
282407
misc = greeter_pkgs.get(greeter, greeter_pkgs["sddm"]) + bl_pkgs.get(bootloader, []) + (extra_pkgs or [])
283408
if misc:
284409
lfn("installing greeter, bootloader, and extras...")
285-
abortable_run(["pacstrap", "-K", target] + misc, abort_flag, timeout=300, log_fn=lfn)
410+
pacstrap_with_fallback(target, misc, abort_flag, lfn, timeout=300)
286411

287412

288413
def configure_system(target, config, lfn, efi_uuid, swap_uuid, root_uuid):
@@ -567,7 +692,7 @@ def install_mochiboot(target, disk, root_uuid, lfn, ch, kernels=None, filesystem
567692

568693
def cleanup_mounts(target):
569694
import subprocess as _sp
570-
for mp in [f"{target}/var/cache", f"{target}/var", f"{target}/home", f"{target}/boot", target]:
695+
for mp in [f"{target}/boot", f"{target}/var/cache", f"{target}/var", f"{target}/home", target]:
571696
_sp.run(["umount", "-l", mp], capture_output=True, check=False, timeout=10)
572697
_sp.run(["umount", "-l", "/mnt/btrfs_tmp"], capture_output=True, check=False, timeout=10)
573698

@@ -652,13 +777,21 @@ def do_install(target="/mnt/mochios", config=None, log_fn=None, abort_flag=None)
652777
# ensure [mochi] repo is available for pacstrap
653778
mochi_conf = "\n[mochi]\nSigLevel = Optional TrustAll\nServer = https://github.com/iluxz/MochiOS/raw/gh-pages/os/x86_64\n"
654779
already_has_mochi = False
655-
if os.path.exists("/etc/pacman.conf"):
656-
with open("/etc/pacman.conf") as check:
780+
target_pacman = f"{target}/etc/pacman.conf"
781+
if os.path.exists(target_pacman):
782+
with open(target_pacman) as check:
657783
already_has_mochi = "[mochi]" in check.read()
658784
if not already_has_mochi:
659-
with open("/etc/pacman.conf", "a") as f:
785+
with open(target_pacman, "a") as f:
660786
f.write(mochi_conf)
661787

788+
# rank mirrors and check network before pacstrap
789+
if not check_network(log_fn):
790+
log_fn("[red]no network connectivity — pacstrap will likely fail[/]")
791+
log_fn("[yellow]connect to wifi or ethernet and try again[/]")
792+
raise RuntimeError("no network connectivity")
793+
rank_mirrors(log_fn)
794+
662795
pacstrap_base(target, log_fn, de=config.get("de", "kde"), greeter=config.get("greeter", "sddm"), bootloader=config.get("bootloader", "limine"), kernels=config.get("kernels", ["linux"]), extra_pkgs=repo_extra, abort_flag=abort_flag, filesystem=filesystem)
663796

664797
log_fn("relaxing SigLevel for custom packages...")

‎archiso-mochios/airootfs/usr/lib/mochiinstall/mochiinstall_app.py‎

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313

1414
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
1515
from mochi_ascii import MOCHI_ASCII
16-
from installer import do_install, set_progress_cb
16+
from installer import do_install, set_progress_cb, detect_partitions, detect_disk_partitions_summary
1717

1818

19-
STEP_NAMES = ["Hostname", "Disk", "Filesystem", "Kernel", "Extras", "Desktop & Boot", "User", "Review"]
19+
STEP_NAMES = ["Hostname", "Disk", "Partitions", "Filesystem", "Kernel", "Extras", "Desktop & Boot", "User", "Review"]
2020

2121

2222
class LogUpdate(Message):
@@ -111,12 +111,12 @@ def on_mount(self) -> None:
111111
self.render_step()
112112

113113
def render_step(self) -> None:
114-
if not (0 <= self.step < 8):
114+
if not (0 <= self.step < 9):
115115
self.app.pop_screen()
116116
return
117117
self.step_bar.update(self.step_bar.render_step(self.step))
118118
self.body.remove_children()
119-
step_fn = [self.step_hostname, self.step_disk, self.step_fs, self.step_kernel, self.step_extra, self.step_de_boot, self.step_user, self.step_summary][self.step]
119+
step_fn = [self.step_hostname, self.step_disk, self.step_partition, self.step_fs, self.step_kernel, self.step_extra, self.step_de_boot, self.step_user, self.step_summary][self.step]
120120
step_fn()
121121
self._focus_step()
122122

@@ -129,15 +129,17 @@ def _focus_step(self) -> None:
129129
elif self.step == 1:
130130
self._try_focus_sel("#disk_list")
131131
elif self.step == 2:
132-
self._try_focus_sel("#fs")
132+
self._try_focus_sel("#disk_mode")
133133
elif self.step == 3:
134-
self._try_focus_sel("#kernel")
134+
self._try_focus_sel("#fs")
135135
elif self.step == 4:
136-
self._try_focus_sel("#extra_pkgs")
136+
self._try_focus_sel("#kernel")
137137
elif self.step == 5:
138+
self._try_focus_sel("#extra_pkgs")
139+
elif self.step == 6:
138140
self._try_focus_sel("#de")
139141
self._try_focus_sel("#bootloader")
140-
elif self.step == 6:
142+
elif self.step == 7:
141143
self._try_focus("#username", Input)
142144

143145
def _try_focus(self, selector, widget_type):
@@ -159,7 +161,7 @@ def _push_next(self) -> bool:
159161
hint = self.query_one("#disk_hint", Label) if self.body.query("#disk_hint") else None
160162
if hint: hint.update("[red]select a disk first![/]")
161163
return False
162-
if self.step == 6:
164+
if self.step == 7:
163165
pw = self.config.get("password", "")
164166
pw_c = self.config.get("password_confirm", "")
165167
hint = self.query_one("#pw_hint", Label) if self.body.query("#pw_hint") else None
@@ -170,7 +172,7 @@ def _push_next(self) -> bool:
170172
if hint: hint.update("[red]passwords do not match[/]")
171173
return False
172174
self.collect_current()
173-
if self.step == 7:
175+
if self.step == 8:
174176
if not self.config.get("disk"):
175177
self.step = 1
176178
self.render_step()
@@ -293,6 +295,19 @@ def step_disk(self) -> None:
293295
self._nav(can_next=has_disk, next_label="next")
294296
self.body.mount(self._wrap(*kids))
295297

298+
def step_partition(self) -> None:
299+
"""show partition info for selected disk."""
300+
disk = self.config.get("disk", "")
301+
kids = [Static(f"[bold]partitions on {disk}[/]")]
302+
303+
part_summary = detect_disk_partitions_summary(disk)
304+
kids.append(Static(f"[dim]{part_summary}[/]"))
305+
kids.append(Static(""))
306+
kids.append(Static("[dim]full 'use existing partition' mode coming soon — currently wipes the whole disk[/]"))
307+
308+
self._nav()
309+
self.body.mount(self._wrap(*kids))
310+
296311
def step_fs(self) -> None:
297312
fs = self.config.get("filesystem", "btrfs")
298313
self.body.mount(self._wrap(
@@ -383,7 +398,8 @@ def step_summary(self) -> None:
383398
Static("[bold underline]review configuration[/]"),
384399
Static(""),
385400
Static(f" hostname: [white]{c['hostname']}[/]"),
386-
Static(f" disk: [red]{c['disk'] or 'NOT SELECTED'}[/] {'[red bold](required!)' if not disk_set else '(will be wiped)'}"),
401+
Static(f" disk: [red]{c['disk'] or 'NOT SELECTED'}[/] {'[red bold](required!)' if not disk_set else '[dim](will be wiped)[/]'}"),
402+
Static(f" filesystem: [white]{c.get('filesystem', 'btrfs')}[/]"),
387403
Static(f" desktop: [white]{c['de']}[/]"),
388404
Static(f" greeter: [white]{c.get('greeter', 'sddm')}[/]"),
389405
Static(f" kernels: [white]{kernels}[/]"),
@@ -404,17 +420,17 @@ def collect_current(self) -> None:
404420
sel = self.query_one("#disk_list", SelectionList)
405421
if sel.selected:
406422
self.config["disk"] = sel.selected[0]
407-
elif self.step == 2:
423+
elif self.step == 3:
408424
sel = self.query_one("#fs", SelectionList)
409425
if sel.selected:
410426
self.config["filesystem"] = sel.selected[0]
411-
elif self.step == 3:
427+
elif self.step == 4:
412428
sel = self.query_one("#kernel", SelectionList)
413429
self.config["kernels"] = sel.selected if sel.selected else ["linux"]
414-
elif self.step == 4:
430+
elif self.step == 5:
415431
sel = self.query_one("#extra_pkgs", SelectionList)
416432
self.config["extra_pkgs"] = sel.selected if sel.selected else []
417-
elif self.step == 5:
433+
elif self.step == 6:
418434
sel_de = self.query_one("#de", SelectionList)
419435
sel_bl = self.query_one("#bootloader", SelectionList)
420436
sel_gr = self.query_one("#greeter", SelectionList)
@@ -424,7 +440,7 @@ def collect_current(self) -> None:
424440
self.config["bootloader"] = sel_bl.selected[0]
425441
if sel_gr.selected:
426442
self.config["greeter"] = sel_gr.selected[0]
427-
elif self.step == 6:
443+
elif self.step == 7:
428444
u = self.query_one("#username", Input)
429445
p = self.query_one("#password", Input)
430446
pc = self.query_one("#password_confirm", Input)

0 commit comments

Comments
 (0)