Skip to content

Commit 0949576

Browse files
committed
feat: implement existing partition install mode in TUI, GUI, and backend
1 parent d78795e commit 0949576

6 files changed

Lines changed: 444 additions & 106 deletions

File tree

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

Lines changed: 112 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,24 @@ def detect_disk_partitions_summary(disk):
262262
return "\n".join(lines)
263263

264264

265+
def detect_existing_parts(disk):
266+
"""detect EFI, swap, and data partitions on a disk for existing mode.
267+
returns dict with 'efi', 'swap', 'data' keys (partition paths or None)."""
268+
parts = detect_partitions(disk)
269+
result = {"efi": None, "swap": None, "data": None}
270+
for p in parts:
271+
fstype = p.get("fstype", "")
272+
label = p.get("label", "").upper()
273+
name = p["name"]
274+
if fstype == "vfat" and (label == "EFI" or "efi" in name.lower()):
275+
result["efi"] = name
276+
elif fstype == "swap" or label == "SWAP":
277+
result["swap"] = name
278+
elif fstype in ("ext4", "btrfs", "xfs") and result["data"] is None:
279+
result["data"] = name
280+
return result
281+
282+
265283
def select_disk(disk):
266284
if not disk or not disk.startswith("/dev/"):
267285
raise ValueError(f"invalid disk: {disk}")
@@ -441,25 +459,22 @@ def ch(cmd, to=120, check=True, input_data=None):
441459
ch(["mkinitcpio", "-P"], to=300)
442460

443461
lfn("writing fstab...")
462+
fstab_lines = []
463+
if efi_uuid:
464+
fstab_lines.append(f"UUID={efi_uuid} /boot vfat rw,noatime,fmask=0022,dmask=0022 0 2")
465+
if swap_uuid:
466+
fstab_lines.append(f"UUID={swap_uuid} swap swap defaults 0 0")
444467
if filesystem == "btrfs":
445-
fstab = (
446-
f"UUID={efi_uuid} /boot vfat rw,noatime,fmask=0022,dmask=0022 0 2\n"
447-
f"UUID={swap_uuid} swap swap defaults 0 0\n"
448-
f"UUID={root_uuid} /mnt/btrfs btrfs rw,noatime,compress=zstd,subvolid=5 0 0\n"
449-
f"UUID={root_uuid} / btrfs rw,noatime,compress=zstd,subvol=root_a 0 0\n"
450-
f"UUID={root_uuid} /home btrfs rw,noatime,compress=zstd,subvol=home 0 0\n"
451-
f"UUID={root_uuid} /var btrfs rw,noatime,compress=zstd,subvol=var 0 0\n"
452-
f"UUID={root_uuid} /var/cache btrfs rw,noatime,compress=zstd,subvol=cache 0 0\n"
453-
)
468+
fstab_lines.append(f"UUID={root_uuid} /mnt/btrfs btrfs rw,noatime,compress=zstd,subvolid=5 0 0")
469+
fstab_lines.append(f"UUID={root_uuid} / btrfs rw,noatime,compress=zstd,subvol=root_a 0 0")
470+
fstab_lines.append(f"UUID={root_uuid} /home btrfs rw,noatime,compress=zstd,subvol=home 0 0")
471+
fstab_lines.append(f"UUID={root_uuid} /var btrfs rw,noatime,compress=zstd,subvol=var 0 0")
472+
fstab_lines.append(f"UUID={root_uuid} /var/cache btrfs rw,noatime,compress=zstd,subvol=cache 0 0")
454473
os.makedirs(f"{target}/mnt/btrfs", exist_ok=True)
455474
else:
456-
fstab = (
457-
f"UUID={efi_uuid} /boot vfat rw,noatime,fmask=0022,dmask=0022 0 2\n"
458-
f"UUID={swap_uuid} swap swap defaults 0 0\n"
459-
f"UUID={root_uuid} / ext4 rw,noatime 0 1\n"
460-
f"UUID={root_uuid} /home ext4 rw,noatime 0 2\n"
461-
)
462-
(t / "etc/fstab").write_text(fstab)
475+
fstab_lines.append(f"UUID={root_uuid} / ext4 rw,noatime 0 1")
476+
fstab_lines.append(f"UUID={root_uuid} /home ext4 rw,noatime 0 2")
477+
(t / "etc/fstab").write_text("\n".join(fstab_lines) + "\n")
463478
ch(["systemctl", "daemon-reload"])
464479

465480
if filesystem == "btrfs":
@@ -735,40 +750,94 @@ def do_install(target="/mnt/mochios", config=None, log_fn=None, abort_flag=None)
735750
_unmount_all()
736751

737752
disk = select_disk(config.get("disk"))
738-
bios_part, efi_part, swap_part, root_part_p = partition_disk(disk, log_fn)
739-
mounts_cleanup_needed = True
753+
disk_mode = config.get("disk_mode", "wipe")
740754

741-
if abort_flag and abort_flag():
742-
raise RuntimeError("installation aborted")
743-
format_efi(efi_part, log_fn)
744-
format_swap(swap_part, log_fn)
755+
if disk_mode == "existing":
756+
existing = detect_existing_parts(disk)
757+
efi_part = existing["efi"]
758+
swap_part = existing["swap"]
759+
root_part_p = config.get("existing_root") or existing["data"]
745760

746-
filesystem = config.get("filesystem", "btrfs")
747-
if filesystem == "ext4":
748-
format_ext4(root_part_p, log_fn)
749-
else:
750-
format_btrfs(root_part_p, log_fn)
761+
if not root_part_p:
762+
raise RuntimeError("no suitable root partition found on disk")
751763

752-
if abort_flag and abort_flag():
753-
raise RuntimeError("installation aborted")
754-
_CLEANUP_TARGET[0] = target
755-
if filesystem == "ext4":
756-
setup_ext4(root_part_p, target, log_fn)
764+
log_fn(f"using existing partitions on {disk}:")
765+
log_fn(f" root: {root_part_p}")
766+
if efi_part:
767+
log_fn(f" efi: {efi_part}")
768+
else:
769+
log_fn(" [yellow]no EFI partition found — you may need to set up boot manually[/]")
770+
if swap_part:
771+
log_fn(f" swap: {swap_part}")
772+
else:
773+
log_fn(" [yellow]no swap partition found — no swap will be active[/]")
774+
775+
mounts_cleanup_needed = True
776+
777+
if abort_flag and abort_flag():
778+
raise RuntimeError("installation aborted")
779+
780+
filesystem = config.get("filesystem", "btrfs")
781+
if filesystem == "ext4":
782+
format_ext4(root_part_p, log_fn)
783+
else:
784+
format_btrfs(root_part_p, log_fn)
785+
786+
_CLEANUP_TARGET[0] = target
787+
if filesystem == "ext4":
788+
setup_ext4(root_part_p, target, log_fn)
789+
else:
790+
setup_btrfs(root_part_p, target, log_fn)
791+
792+
root_uuid = get_uuid(root_part_p)
793+
efi_uuid = get_uuid(efi_part) if efi_part else ""
794+
swap_uuid = get_uuid(swap_part) if swap_part else ""
795+
796+
if filesystem == "btrfs":
797+
log_fn("configuring snapper for root...")
798+
run(["arch-chroot", target, "snapper", "-c", "root", "create-config", "/"], check=False, timeout=30)
799+
run(["arch-chroot", target, "systemctl", "enable", "snapper-timeline.timer"], check=False)
800+
run(["arch-chroot", target, "systemctl", "enable", "snapper-cleanup.timer"], check=False)
801+
802+
if swap_part:
803+
run(["swapon", swap_part], check=False)
804+
if efi_part:
805+
run(["mount", efi_part, f"{target}/boot"])
757806
else:
758-
setup_btrfs(root_part_p, target, log_fn)
807+
bios_part, efi_part, swap_part, root_part_p = partition_disk(disk, log_fn)
808+
mounts_cleanup_needed = True
809+
810+
if abort_flag and abort_flag():
811+
raise RuntimeError("installation aborted")
812+
format_efi(efi_part, log_fn)
813+
format_swap(swap_part, log_fn)
814+
815+
filesystem = config.get("filesystem", "btrfs")
816+
if filesystem == "ext4":
817+
format_ext4(root_part_p, log_fn)
818+
else:
819+
format_btrfs(root_part_p, log_fn)
820+
821+
if abort_flag and abort_flag():
822+
raise RuntimeError("installation aborted")
823+
_CLEANUP_TARGET[0] = target
824+
if filesystem == "ext4":
825+
setup_ext4(root_part_p, target, log_fn)
826+
else:
827+
setup_btrfs(root_part_p, target, log_fn)
759828

760-
efi_uuid = get_uuid(efi_part)
761-
swap_uuid = get_uuid(swap_part)
762-
root_uuid = get_uuid(root_part_p)
829+
efi_uuid = get_uuid(efi_part)
830+
swap_uuid = get_uuid(swap_part)
831+
root_uuid = get_uuid(root_part_p)
763832

764-
if filesystem == "btrfs":
765-
log_fn("configuring snapper for root...")
766-
run(["arch-chroot", target, "snapper", "-c", "root", "create-config", "/"], check=False, timeout=30)
767-
run(["arch-chroot", target, "systemctl", "enable", "snapper-timeline.timer"], check=False)
768-
run(["arch-chroot", target, "systemctl", "enable", "snapper-cleanup.timer"], check=False)
833+
if filesystem == "btrfs":
834+
log_fn("configuring snapper for root...")
835+
run(["arch-chroot", target, "snapper", "-c", "root", "create-config", "/"], check=False, timeout=30)
836+
run(["arch-chroot", target, "systemctl", "enable", "snapper-timeline.timer"], check=False)
837+
run(["arch-chroot", target, "systemctl", "enable", "snapper-cleanup.timer"], check=False)
769838

770-
run(["swapon", swap_part], check=False)
771-
run(["mount", efi_part, f"{target}/boot"])
839+
run(["swapon", swap_part], check=False)
840+
run(["mount", efi_part, f"{target}/boot"])
772841

773842
extra_all = config.get("extra_pkgs", []) or []
774843
repo_extra = [p for p in extra_all if isinstance(p, str) and p not in CUSTOM_EXTRAS]

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

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
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, detect_partitions, detect_disk_partitions_summary
16+
from installer import do_install, set_progress_cb, detect_partitions, detect_disk_partitions_summary, detect_existing_parts
1717

1818

1919
STEP_NAMES = ["Hostname", "Disk", "Partitions", "Filesystem", "Kernel", "Extras", "Desktop & Boot", "User", "Review"]
@@ -161,6 +161,12 @@ def _push_next(self) -> bool:
161161
hint = self.query_one("#disk_hint", Label) if self.body.query("#disk_hint") else None
162162
if hint: hint.update("[red]select a disk first![/]")
163163
return False
164+
if self.step == 2:
165+
mode = self.config.get("disk_mode", "wipe")
166+
if mode == "existing" and not self.config.get("existing_root"):
167+
hint = self.query_one("#part_hint", Label) if self.body.query("#part_hint") else None
168+
if hint: hint.update("[red]select a partition or choose wipe![/]")
169+
return False
164170
if self.step == 7:
165171
pw = self.config.get("password", "")
166172
pw_c = self.config.get("password_confirm", "")
@@ -208,6 +214,11 @@ def on_selection_list_selection_toggled(self, event: SelectionList.SelectionTogg
208214
hint = self.query_one("#disk_hint", Label)
209215
hint.update("")
210216
self._nav(can_next=has_disk, next_label="next")
217+
elif self.step == 2:
218+
sel = self.query_one("#disk_mode", SelectionList)
219+
if sel.selected:
220+
self.config["disk_mode"] = sel.selected[0]
221+
self.render_step()
211222

212223
def on_input_submitted(self, event: Input.Submitted) -> None:
213224
if self.step == 0:
@@ -296,14 +307,36 @@ def step_disk(self) -> None:
296307
self.body.mount(self._wrap(*kids))
297308

298309
def step_partition(self) -> None:
299-
"""show partition info for selected disk."""
310+
"""show partition info and choose wipe vs existing."""
300311
disk = self.config.get("disk", "")
301312
kids = [Static(f"[bold]partitions on {disk}[/]")]
302313

303314
part_summary = detect_disk_partitions_summary(disk)
304315
kids.append(Static(f"[dim]{part_summary}[/]"))
305316
kids.append(Static(""))
306-
kids.append(Static("[dim]full 'use existing partition' mode coming soon — currently wipes the whole disk[/]"))
317+
318+
mode = self.config.get("disk_mode", "wipe")
319+
kids.append(Label("", id="part_hint"))
320+
kids.append(SelectionList(
321+
("wipe entire disk (delete all partitions, create new mochios layout)", "wipe", mode == "wipe"),
322+
("use existing partition (install mochios alongside other OS)", "existing", mode == "existing"),
323+
id="disk_mode",
324+
))
325+
326+
if mode == "existing":
327+
existing = detect_existing_parts(disk)
328+
linux_parts = []
329+
parts = detect_partitions(disk)
330+
for p in parts:
331+
if p["fstype"] in ("ext4", "btrfs", "xfs", ""):
332+
label = f"{p['name']} {p['size']} {p['fstype'] or 'unknown'}"
333+
linux_parts.append((label, p["name"], p["name"] == self.config.get("existing_root", "")))
334+
if linux_parts:
335+
kids.append(Static(""))
336+
kids.append(Static("[bold]select root partition for mochios:[/]"))
337+
kids.append(SelectionList(*linux_parts, id="existing_root"))
338+
else:
339+
kids.append(Static("[yellow]no suitable partitions found — create one first or choose wipe[/]"))
307340

308341
self._nav()
309342
self.body.mount(self._wrap(*kids))
@@ -394,11 +427,14 @@ def step_summary(self) -> None:
394427
disk_set = bool(c["disk"])
395428
extras = ", ".join(c.get("extra_pkgs", [])) or "none"
396429
kernels = ", ".join(c.get("kernels", ["linux"])) or "linux"
430+
disk_mode = c.get("disk_mode", "wipe")
431+
mode_label = "wipe entire disk" if disk_mode == "wipe" else f"use partition {c.get('existing_root', '?')}"
397432
self.body.mount(self._wrap(
398433
Static("[bold underline]review configuration[/]"),
399434
Static(""),
400435
Static(f" hostname: [white]{c['hostname']}[/]"),
401-
Static(f" disk: [red]{c['disk'] or 'NOT SELECTED'}[/] {'[red bold](required!)' if not disk_set else '[dim](will be wiped)[/]'}"),
436+
Static(f" disk: [red]{c['disk'] or 'NOT SELECTED'}[/] {'[red bold](required!)' if not disk_set else ''}"),
437+
Static(f" disk mode: [white]{mode_label}[/]"),
402438
Static(f" filesystem: [white]{c.get('filesystem', 'btrfs')}[/]"),
403439
Static(f" desktop: [white]{c['de']}[/]"),
404440
Static(f" greeter: [white]{c.get('greeter', 'sddm')}[/]"),
@@ -407,7 +443,7 @@ def step_summary(self) -> None:
407443
Static(f" extras: [white]{extras}[/]"),
408444
Static(f" user: [white]{c['username']}[/]"),
409445
Static(""),
410-
Static("[red bold]WARNING: this will erase all data on the selected disk" if disk_set else "[red bold]select a disk first!"),
446+
Static("[red bold]WARNING: this will erase data on the selected disk/partition" if disk_set else "[red bold]select a disk first!"),
411447
))
412448
self._nav(next_label="install!" if disk_set else "", can_next=disk_set)
413449

@@ -420,6 +456,17 @@ def collect_current(self) -> None:
420456
sel = self.query_one("#disk_list", SelectionList)
421457
if sel.selected:
422458
self.config["disk"] = sel.selected[0]
459+
elif self.step == 2:
460+
sel = self.query_one("#disk_mode", SelectionList)
461+
if sel.selected:
462+
self.config["disk_mode"] = sel.selected[0]
463+
if self.config.get("disk_mode") == "existing":
464+
try:
465+
root_sel = self.query_one("#existing_root", SelectionList)
466+
if root_sel.selected:
467+
self.config["existing_root"] = root_sel.selected[0]
468+
except Exception:
469+
pass
423470
elif self.step == 3:
424471
sel = self.query_one("#fs", SelectionList)
425472
if sel.selected:

0 commit comments

Comments
 (0)