55import re
66import shutil
77import time
8+ import signal
9+ import sys
10+ import atexit
811from pathlib import Path
912
1013CUSTOM_EXTRAS = {"steam" }
1114
15+ _CLEANUP_TARGET = [None ]
16+
17+ def _unmount_all ():
18+ target = _CLEANUP_TARGET [0 ]
19+ if not target or not os .path .ismount (target ):
20+ return
21+ for mp in [f"{ target } /var/cache" , f"{ target } /var" , f"{ target } /home" , f"{ target } /boot" , target ]:
22+ subprocess .run (["umount" , "-l" , mp ], capture_output = True , check = False , timeout = 10 )
23+ subprocess .run (["umount" , "-l" , "/mnt/btrfs_tmp" ], capture_output = True , check = False , timeout = 10 )
24+ subprocess .run (["swapoff" , "-a" ], capture_output = True , check = False , timeout = 10 )
25+
26+ # signal handlers registered at module level (main thread)
27+ # only if not in a daemon thread (TUI runs installer in thread)
28+ def _setup_signal_handlers ():
29+ old = {}
30+ def handler (signum , frame ):
31+ _unmount_all ()
32+ sys .exit (128 + signum )
33+ for sig in (signal .SIGTERM , signal .SIGINT ):
34+ try :
35+ old [sig ] = signal .signal (sig , handler )
36+ except (ValueError , RuntimeError ):
37+ pass
38+ return old
39+
40+ _setup_signal_handlers ()
41+ atexit .register (_unmount_all )
42+
1243_TOTAL_STEPS = 60
1344_STEP_COUNT = [0 ]
1445_PROGRESS_CB = [None ]
@@ -35,7 +66,7 @@ def run(cmd, check=True, capture=False, timeout=None, input_data=None):
3566 return r
3667
3768
38- def abortable_run (cmd , abort_flag , check = True , timeout = None ):
69+ def abortable_run (cmd , abort_flag , check = True , timeout = None , log_fn = None ):
3970 import select as _select
4071 proc = subprocess .Popen (cmd , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , text = True )
4172 out_lines = []
@@ -60,6 +91,10 @@ def abortable_run(cmd, abort_flag, check=True, timeout=None):
6091 line = proc .stdout .readline ()
6192 if line :
6293 out_lines .append (line )
94+ if log_fn :
95+ clean = line .rstrip ()
96+ if clean :
97+ log_fn (f"[dim]{ clean } [/]" )
6398 if proc .poll () is not None :
6499 break
65100 for line in proc .stdout :
@@ -179,13 +214,14 @@ def setup_btrfs(root_part, target, lfn):
179214def pacstrap_base (target , lfn , de = "kde" , greeter = "sddm" , bootloader = "limine" , kernels = None , extra_pkgs = None , abort_flag = None ):
180215 if kernels is None :
181216 kernels = ["linux" ]
182- lfn ("installing base system..." )
183217
184218 kernel_pkgs = []
185219 for k in kernels :
186220 kernel_pkgs .append (k )
187221 kernel_pkgs .append (k + "-headers" )
188222
223+ # step 1: base system
224+ lfn ("installing base system..." )
189225 base = [
190226 "base" ,
191227 * kernel_pkgs ,
@@ -196,6 +232,9 @@ def pacstrap_base(target, lfn, de="kde", greeter="sddm", bootloader="limine", ke
196232 "sbctl" ,
197233 "zsh" , "git" , "flatpak" ,
198234 ]
235+ abortable_run (["pacstrap" , "-K" , target ] + base , abort_flag , timeout = 900 , log_fn = lfn )
236+
237+ # step 2: desktop environment
199238 de_pkgs = {
200239 "kde" : ["plasma-desktop" , "plasma-workspace" , "kdeplasma-addons" ,
201240 "kwin" , "konsole" , "dolphin" , "kate" , "gwenview" ,
@@ -207,6 +246,11 @@ def pacstrap_base(target, lfn, de="kde", greeter="sddm", bootloader="limine", ke
207246 "wofi" , "waybar" , "dunst" , "polkit-kde-agent" ,
208247 "wl-clipboard" , "slurp" , "grim" , "hyprpaper" ],
209248 }
249+ de_choice = de_pkgs .get (de , de_pkgs ["kde" ])
250+ lfn (f"installing { de } desktop..." )
251+ abortable_run (["pacstrap" , "-K" , target ] + de_choice , abort_flag , timeout = 600 , log_fn = lfn )
252+
253+ # step 3: greeter + bootloader + extras
210254 greeter_pkgs = {
211255 "sddm" : ["sddm" ],
212256 "gdm" : ["gdm" ],
@@ -219,8 +263,10 @@ def pacstrap_base(target, lfn, de="kde", greeter="sddm", bootloader="limine", ke
219263 "grub" : ["grub" ],
220264 "mochiboot" : ["mochiboot" ],
221265 }
222- pkgs = base + de_pkgs .get (de , de_pkgs ["kde" ]) + greeter_pkgs .get (greeter , greeter_pkgs ["sddm" ]) + bl_pkgs .get (bootloader , []) + (extra_pkgs or [])
223- abortable_run (["pacstrap" , "-K" , target ] + pkgs , abort_flag , timeout = 900 )
266+ misc = greeter_pkgs .get (greeter , greeter_pkgs ["sddm" ]) + bl_pkgs .get (bootloader , []) + (extra_pkgs or [])
267+ if misc :
268+ lfn ("installing greeter, bootloader, and extras..." )
269+ abortable_run (["pacstrap" , "-K" , target ] + misc , abort_flag , timeout = 300 , log_fn = lfn )
224270
225271
226272def configure_system (target , config , lfn , efi_uuid , swap_uuid , root_uuid ):
@@ -508,6 +554,10 @@ def do_install(target="/mnt/mochios", config=None, log_fn=None, abort_flag=None)
508554 if abort_flag and abort_flag ():
509555 raise RuntimeError ("installation aborted" )
510556 log_fn ("starting mochios installation..." )
557+
558+ # pre-flight: unmount any leftover mounts from failed installs
559+ _unmount_all ()
560+
511561 disk = select_disk (config .get ("disk" ))
512562 bios_part , efi_part , swap_part , root_part_p = partition_disk (disk , log_fn )
513563 mounts_cleanup_needed = True
@@ -520,6 +570,7 @@ def do_install(target="/mnt/mochios", config=None, log_fn=None, abort_flag=None)
520570
521571 if abort_flag and abort_flag ():
522572 raise RuntimeError ("installation aborted" )
573+ _CLEANUP_TARGET [0 ] = target
523574 setup_btrfs (root_part_p , target , log_fn )
524575
525576 efi_uuid = get_uuid (efi_part )
@@ -603,6 +654,7 @@ def do_install(target="/mnt/mochios", config=None, log_fn=None, abort_flag=None)
603654 run (["swapoff" , swap_part ], check = False )
604655 log_fn ("installation complete!" )
605656 mounts_cleanup_needed = False
657+ _CLEANUP_TARGET [0 ] = None
606658 return True
607659
608660 except subprocess .CalledProcessError as e :
0 commit comments