feat: verbose installation logs, improved auto-partitioning logic, and UI tweaks

This commit is contained in:
2026-02-03 21:24:31 +01:00
parent 7d43b82ce1
commit 00ba6e5c89
3 changed files with 206 additions and 55 deletions

View File

@@ -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,46 +60,95 @@ 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 <partnum>:<start>:<end>
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])
if use_swap:
logger.info("Formatting Swap partition...")
run_command(["mkswap", swap_part])
@@ -69,16 +157,23 @@ def auto_partition_disk(disk_device):
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,7 +190,8 @@ def mount_partitions(partition_info, mount_root="/mnt"):
run_command(["mount", partition_info["efi"], efi_mount])
# 3. Enable Swap (optional, but might as well)
# 3. Enable Swap
if partition_info.get("swap"):
run_command(["swapon", partition_info["swap"]])
logger.info(f"Partitions mounted at {mount_root}")

View File

@@ -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_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:

View File

@@ -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,14 +87,22 @@ 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):
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 []
@@ -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,9 +126,20 @@ 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:"))