From 00ba6e5c897348e353fc7bf8c9eb9d6fc50aa5ce Mon Sep 17 00:00:00 2001 From: N0VA Date: Tue, 3 Feb 2026 21:24:31 +0100 Subject: [PATCH] feat: verbose installation logs, improved auto-partitioning logic, and UI tweaks --- iridium_installer/backend/disk.py | 150 +++++++++++++++++---- iridium_installer/backend/os_install.py | 60 +++++++-- iridium_installer/ui/pages/partitioning.py | 51 ++++--- 3 files changed, 206 insertions(+), 55 deletions(-) diff --git a/iridium_installer/backend/disk.py b/iridium_installer/backend/disk.py index d0f5f4c..14b27ae 100644 --- a/iridium_installer/backend/disk.py +++ b/iridium_installer/backend/disk.py @@ -1,16 +1,55 @@ import subprocess import logging +import os logger = logging.getLogger(__name__) +class CommandResult: + def __init__(self, stdout, stderr, returncode): + self.stdout = stdout + self.stderr = stderr + self.returncode = returncode + def run_command(cmd, check=True): logger.info(f"Running command: {' '.join(cmd)}") - try: - result = subprocess.run(cmd, check=check, capture_output=True, text=True) - return result - except subprocess.CalledProcessError as e: - logger.error(f"Command failed: {e.stderr}") - raise + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1 + ) + + stdout_lines = [] + stderr_lines = [] + + def read_stream(stream, line_list, log_level): + for line in stream: + line_clean = line.strip() + if line_clean: + log_level(line_clean) + line_list.append(line) + + import threading + t1 = threading.Thread(target=read_stream, args=(process.stdout, stdout_lines, logger.info)) + t2 = threading.Thread(target=read_stream, args=(process.stderr, stderr_lines, logger.error)) + + t1.start() + t2.start() + + t1.join() + t2.join() + + returncode = process.wait() + + stdout_str = "".join(stdout_lines) + stderr_str = "".join(stderr_lines) + + if check and returncode != 0: + raise subprocess.CalledProcessError(returncode, cmd, output=stdout_str, stderr=stderr_str) + + return CommandResult(stdout_str, stderr_str, returncode) def get_partition_device(disk_device, partition_number): """ @@ -21,64 +60,120 @@ def get_partition_device(disk_device, partition_number): return f"{disk_device}p{partition_number}" return f"{disk_device}{partition_number}" +def get_total_memory(): + """Returns total system memory in bytes.""" + try: + return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") + except (ValueError, OSError): + with open("/proc/meminfo", "r") as f: + for line in f: + if line.startswith("MemTotal:"): + parts = line.split() + return int(parts[1]) * 1024 + return 0 + +def get_disk_size(disk_device): + """Returns disk size in bytes using blockdev.""" + try: + res = run_command(["blockdev", "--getsize64", disk_device]) + return int(res.stdout.strip()) + except Exception as e: + logger.error(f"Failed to get disk size: {e}") + return 0 + def auto_partition_disk(disk_device): """ - Automatically partitions the disk with a standard layout: - 1. EFI System Partition (1GB) - 2. Swap (4GB) - simpler fixed size for now - 3. Root (Remaining) + Automatically partitions the disk: + 1. EFI System Partition (2GB standard, 1GB small) + 2. Root (Remaining) + 3. Swap (RAM + 2GB, or 0 if small) + + Layout: P1=EFI, P3=Swap (End), P2=Root (Middle) """ logger.info(f"Starting auto-partitioning on {disk_device}") + # Calculate sizes + disk_size = get_disk_size(disk_device) + ram_size = get_total_memory() + + # Defaults + efi_mb = 2048 + swap_mb = int((ram_size / (1024*1024)) + 2048) + min_root_mb = 10240 # 10GB + + total_required_mb = efi_mb + swap_mb + min_root_mb + disk_mb = disk_size / (1024*1024) + + use_swap = True + + if disk_mb < total_required_mb: + logger.warning("Disk too small for standard layout. Adjusting...") + efi_mb = 1024 + use_swap = False + + # Check minimal viability + if disk_mb < (efi_mb + min_root_mb): + raise Exception("Disk too small for installation (Need ~11GB)") + # 1. Zap the disk (destroy all data) run_command(["sgdisk", "-Z", disk_device]) # 2. Create new GPT table run_command(["sgdisk", "-o", disk_device]) - # 3. Create EFI Partition (Part 1, 1GB, Type EF00) - # -n :: - run_command(["sgdisk", "-n", "1:0:+1024M", "-t", "1:ef00", "-c", "1:EFI System", disk_device]) + # 3. Create EFI Partition (Part 1, Start) + run_command(["sgdisk", "-n", f"1:0:+{efi_mb}M", "-t", "1:ef00", "-c", "1:EFI System", disk_device]) - # 4. Create Swap Partition (Part 2, 4GB, Type 8200) - run_command(["sgdisk", "-n", "2:0:+4096M", "-t", "2:8200", "-c", "2:Swap", disk_device]) + # 4. Create Swap Partition (Part 3, End) - If enabled + if use_swap: + # sgdisk negative start is from end of disk + # We use partition 3 for Swap + run_command(["sgdisk", "-n", f"3:-{swap_mb}M:0", "-t", "3:8200", "-c", "3:Swap", disk_device]) - # 5. Create Root Partition (Part 3, Rest, Type 8300) - run_command(["sgdisk", "-n", "3:0:0", "-t", "3:8300", "-c", "3:Root", disk_device]) + # 5. Create Root Partition (Part 2, Fill Gap) + # This fills the space between P1 and P3 (or end if no swap) + run_command(["sgdisk", "-n", "2:0:0", "-t", "2:8300", "-c", "2:Root", disk_device]) # Inform kernel of changes run_command(["partprobe", disk_device]) - # Wait a bit for nodes to appear? Usually partprobe handles it but sometimes there's a race. import time time.sleep(1) # 6. Format Partitions efi_part = get_partition_device(disk_device, 1) - swap_part = get_partition_device(disk_device, 2) - root_part = get_partition_device(disk_device, 3) + root_part = get_partition_device(disk_device, 2) + swap_part = get_partition_device(disk_device, 3) if use_swap else None logger.info("Formatting EFI partition...") run_command(["mkfs.vfat", "-F32", efi_part]) - logger.info("Formatting Swap partition...") - run_command(["mkswap", swap_part]) + if use_swap: + logger.info("Formatting Swap partition...") + run_command(["mkswap", swap_part]) logger.info("Formatting Root partition...") run_command(["mkfs.ext4", "-F", root_part]) logger.info("Partitioning and formatting complete.") - return { + result = { "efi": efi_part, - "swap": swap_part, "root": root_part } + if use_swap: + result["swap"] = swap_part + else: + # If no swap, we should probably return None or handle it in fstab generation + # backend/os_install.py expects "swap" key for UUID. + # We should probably pass a dummy or None, and update os_install to handle it. + result["swap"] = None + + return result def mount_partitions(partition_info, mount_root="/mnt"): """ Mounts the partitions into mount_root. - partition_info is the dict returned by auto_partition_disk. """ import os @@ -95,8 +190,9 @@ def mount_partitions(partition_info, mount_root="/mnt"): run_command(["mount", partition_info["efi"], efi_mount]) - # 3. Enable Swap (optional, but might as well) - run_command(["swapon", partition_info["swap"]]) + # 3. Enable Swap + if partition_info.get("swap"): + run_command(["swapon", partition_info["swap"]]) logger.info(f"Partitions mounted at {mount_root}") diff --git a/iridium_installer/backend/os_install.py b/iridium_installer/backend/os_install.py index 5f9d204..3902674 100644 --- a/iridium_installer/backend/os_install.py +++ b/iridium_installer/backend/os_install.py @@ -1,18 +1,58 @@ import subprocess import logging import os +import sys from contextlib import contextmanager logger = logging.getLogger(__name__) +class CommandResult: + def __init__(self, stdout, stderr, returncode): + self.stdout = stdout + self.stderr = stderr + self.returncode = returncode + def run_command(cmd, check=True): logger.info(f"Running command: {' '.join(cmd)}") - try: - result = subprocess.run(cmd, check=check, capture_output=True, text=True) - return result - except subprocess.CalledProcessError as e: - logger.error(f"Command failed: {e.stderr}") - raise + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1 # Line buffered + ) + + stdout_lines = [] + stderr_lines = [] + + # Helper to read stream + def read_stream(stream, line_list, log_level): + for line in stream: + line_clean = line.strip() + if line_clean: + log_level(line_clean) + line_list.append(line) + + import threading + t1 = threading.Thread(target=read_stream, args=(process.stdout, stdout_lines, logger.info)) + t2 = threading.Thread(target=read_stream, args=(process.stderr, stderr_lines, logger.error)) + + t1.start() + t2.start() + + t1.join() + t2.join() + + returncode = process.wait() + + stdout_str = "".join(stdout_lines) + stderr_str = "".join(stderr_lines) + + if check and returncode != 0: + raise subprocess.CalledProcessError(returncode, cmd, output=stdout_str, stderr=stderr_str) + + return CommandResult(stdout_str, stderr_str, returncode) @contextmanager def mount_pseudo_fs(mount_root): @@ -86,12 +126,16 @@ def configure_system(mount_root, partition_info): root_uuid = get_uuid(partition_info["root"]) efi_uuid = get_uuid(partition_info["efi"]) - swap_uuid = get_uuid(partition_info["swap"]) + + swap_entry = "" + if partition_info.get("swap"): + swap_uuid = get_uuid(partition_info["swap"]) + swap_entry = f"UUID={swap_uuid} none swap defaults 0 0\n" fstab_content = f""" UUID={root_uuid} / ext4 defaults 1 1 UUID={efi_uuid} /boot/efi vfat defaults 0 2 -UUID={swap_uuid} none swap defaults 0 0 +{swap_entry} """ os.makedirs(os.path.join(mount_root, "etc"), exist_ok=True) with open(os.path.join(mount_root, "etc/fstab"), "w") as f: diff --git a/iridium_installer/ui/pages/partitioning.py b/iridium_installer/ui/pages/partitioning.py index bab365e..38534f0 100644 --- a/iridium_installer/ui/pages/partitioning.py +++ b/iridium_installer/ui/pages/partitioning.py @@ -55,12 +55,12 @@ def get_total_memory() -> int: return 0 -def calculate_auto_partitions(disk_device): + def calculate_auto_partitions(disk_device): """ Generates an automatic partition layout. - - 2GB EFI - - RAM + 2GB Swap - - Rest Root (ext4) + - 2GB EFI (or 1GB if disk < required) + - Root (Rest) + - RAM + 2GB Swap (at end) (or 0 if disk < required) """ disk_size = 0 try: @@ -87,16 +87,24 @@ def calculate_auto_partitions(disk_device): return [] ram_size = get_total_memory() + disk_mb = disk_size / (1024*1024) - # Sizes in bytes + # Defaults efi_size = 2 * 1024 * 1024 * 1024 swap_size = ram_size + (2 * 1024 * 1024 * 1024) + min_root_size = 10 * 1024 * 1024 * 1024 # 10GB - # Check if disk is large enough - min_root = 10 * 1024 * 1024 * 1024 # minimum 10GB for root - if disk_size < (efi_size + swap_size + min_root): - print("Disk too small for automatic partitioning scheme.") - return [] + total_required = efi_size + swap_size + min_root_size + + use_swap = True + + if disk_size < total_required: + efi_size = 1 * 1024 * 1024 * 1024 + use_swap = False + swap_size = 0 + if disk_size < (efi_size + min_root_size): + print("Disk too small for automatic partitioning scheme.") + return [] root_size = disk_size - efi_size - swap_size @@ -110,15 +118,6 @@ def calculate_auto_partitions(disk_device): "bytes": efi_size, "style_class": "part-efi", }, - { - "type": "partition", - "name": "Swap", - "filesystem": "swap", - "mount_point": "[SWAP]", - "size": f"{swap_size / (1024**3):.1f} GB", - "bytes": swap_size, - "style_class": "part-swap", - }, { "type": "partition", "name": "Root", @@ -127,8 +126,19 @@ def calculate_auto_partitions(disk_device): "size": f"{root_size / (1024**3):.1f} GB", "bytes": root_size, "style_class": "part-root", - }, + } ] + + if use_swap: + partitions.append({ + "type": "partition", + "name": "Swap", + "filesystem": "swap", + "mount_point": "[SWAP]", + "size": f"{swap_size / (1024**3):.1f} GB", + "bytes": swap_size, + "style_class": "part-swap", + }) return partitions @@ -525,6 +535,7 @@ class PartitioningPage(Adw.Bin): # Size entry size_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) size_entry = Gtk.Entry(text=str(int(data["bytes"] / (1024*1024)))) + size_entry.set_width_chars(15) size_box.append(size_entry) size_box.append(Gtk.Label(label="MB")) box.append(Gtk.Label(label="Size:"))