feat: verbose installation logs, improved auto-partitioning logic, and UI tweaks
This commit is contained in:
@@ -1,16 +1,55 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
def run_command(cmd, check=True):
|
||||||
logger.info(f"Running command: {' '.join(cmd)}")
|
logger.info(f"Running command: {' '.join(cmd)}")
|
||||||
try:
|
|
||||||
result = subprocess.run(cmd, check=check, capture_output=True, text=True)
|
process = subprocess.Popen(
|
||||||
return result
|
cmd,
|
||||||
except subprocess.CalledProcessError as e:
|
stdout=subprocess.PIPE,
|
||||||
logger.error(f"Command failed: {e.stderr}")
|
stderr=subprocess.PIPE,
|
||||||
raise
|
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):
|
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}p{partition_number}"
|
||||||
return f"{disk_device}{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):
|
def auto_partition_disk(disk_device):
|
||||||
"""
|
"""
|
||||||
Automatically partitions the disk with a standard layout:
|
Automatically partitions the disk:
|
||||||
1. EFI System Partition (1GB)
|
1. EFI System Partition (2GB standard, 1GB small)
|
||||||
2. Swap (4GB) - simpler fixed size for now
|
2. Root (Remaining)
|
||||||
3. 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}")
|
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)
|
# 1. Zap the disk (destroy all data)
|
||||||
run_command(["sgdisk", "-Z", disk_device])
|
run_command(["sgdisk", "-Z", disk_device])
|
||||||
|
|
||||||
# 2. Create new GPT table
|
# 2. Create new GPT table
|
||||||
run_command(["sgdisk", "-o", disk_device])
|
run_command(["sgdisk", "-o", disk_device])
|
||||||
|
|
||||||
# 3. Create EFI Partition (Part 1, 1GB, Type EF00)
|
# 3. Create EFI Partition (Part 1, Start)
|
||||||
# -n <partnum>:<start>:<end>
|
run_command(["sgdisk", "-n", f"1:0:+{efi_mb}M", "-t", "1:ef00", "-c", "1:EFI System", disk_device])
|
||||||
run_command(["sgdisk", "-n", "1:0:+1024M", "-t", "1:ef00", "-c", "1:EFI System", disk_device])
|
|
||||||
|
|
||||||
# 4. Create Swap Partition (Part 2, 4GB, Type 8200)
|
# 4. Create Swap Partition (Part 3, End) - If enabled
|
||||||
run_command(["sgdisk", "-n", "2:0:+4096M", "-t", "2:8200", "-c", "2:Swap", disk_device])
|
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)
|
# 5. Create Root Partition (Part 2, Fill Gap)
|
||||||
run_command(["sgdisk", "-n", "3:0:0", "-t", "3:8300", "-c", "3:Root", disk_device])
|
# 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
|
# Inform kernel of changes
|
||||||
run_command(["partprobe", disk_device])
|
run_command(["partprobe", disk_device])
|
||||||
|
|
||||||
# Wait a bit for nodes to appear? Usually partprobe handles it but sometimes there's a race.
|
|
||||||
import time
|
import time
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# 6. Format Partitions
|
# 6. Format Partitions
|
||||||
efi_part = get_partition_device(disk_device, 1)
|
efi_part = get_partition_device(disk_device, 1)
|
||||||
swap_part = get_partition_device(disk_device, 2)
|
root_part = get_partition_device(disk_device, 2)
|
||||||
root_part = get_partition_device(disk_device, 3)
|
swap_part = get_partition_device(disk_device, 3) if use_swap else None
|
||||||
|
|
||||||
logger.info("Formatting EFI partition...")
|
logger.info("Formatting EFI partition...")
|
||||||
run_command(["mkfs.vfat", "-F32", efi_part])
|
run_command(["mkfs.vfat", "-F32", efi_part])
|
||||||
|
|
||||||
logger.info("Formatting Swap partition...")
|
if use_swap:
|
||||||
run_command(["mkswap", swap_part])
|
logger.info("Formatting Swap partition...")
|
||||||
|
run_command(["mkswap", swap_part])
|
||||||
|
|
||||||
logger.info("Formatting Root partition...")
|
logger.info("Formatting Root partition...")
|
||||||
run_command(["mkfs.ext4", "-F", root_part])
|
run_command(["mkfs.ext4", "-F", root_part])
|
||||||
|
|
||||||
logger.info("Partitioning and formatting complete.")
|
logger.info("Partitioning and formatting complete.")
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
"efi": efi_part,
|
"efi": efi_part,
|
||||||
"swap": swap_part,
|
|
||||||
"root": root_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"):
|
def mount_partitions(partition_info, mount_root="/mnt"):
|
||||||
"""
|
"""
|
||||||
Mounts the partitions into mount_root.
|
Mounts the partitions into mount_root.
|
||||||
partition_info is the dict returned by auto_partition_disk.
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -95,8 +190,9 @@ def mount_partitions(partition_info, mount_root="/mnt"):
|
|||||||
|
|
||||||
run_command(["mount", partition_info["efi"], efi_mount])
|
run_command(["mount", partition_info["efi"], efi_mount])
|
||||||
|
|
||||||
# 3. Enable Swap (optional, but might as well)
|
# 3. Enable Swap
|
||||||
run_command(["swapon", partition_info["swap"]])
|
if partition_info.get("swap"):
|
||||||
|
run_command(["swapon", partition_info["swap"]])
|
||||||
|
|
||||||
logger.info(f"Partitions mounted at {mount_root}")
|
logger.info(f"Partitions mounted at {mount_root}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,58 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
def run_command(cmd, check=True):
|
||||||
logger.info(f"Running command: {' '.join(cmd)}")
|
logger.info(f"Running command: {' '.join(cmd)}")
|
||||||
try:
|
|
||||||
result = subprocess.run(cmd, check=check, capture_output=True, text=True)
|
process = subprocess.Popen(
|
||||||
return result
|
cmd,
|
||||||
except subprocess.CalledProcessError as e:
|
stdout=subprocess.PIPE,
|
||||||
logger.error(f"Command failed: {e.stderr}")
|
stderr=subprocess.PIPE,
|
||||||
raise
|
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
|
@contextmanager
|
||||||
def mount_pseudo_fs(mount_root):
|
def mount_pseudo_fs(mount_root):
|
||||||
@@ -86,12 +126,16 @@ def configure_system(mount_root, partition_info):
|
|||||||
|
|
||||||
root_uuid = get_uuid(partition_info["root"])
|
root_uuid = get_uuid(partition_info["root"])
|
||||||
efi_uuid = get_uuid(partition_info["efi"])
|
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"""
|
fstab_content = f"""
|
||||||
UUID={root_uuid} / ext4 defaults 1 1
|
UUID={root_uuid} / ext4 defaults 1 1
|
||||||
UUID={efi_uuid} /boot/efi vfat defaults 0 2
|
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)
|
os.makedirs(os.path.join(mount_root, "etc"), exist_ok=True)
|
||||||
with open(os.path.join(mount_root, "etc/fstab"), "w") as f:
|
with open(os.path.join(mount_root, "etc/fstab"), "w") as f:
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ def get_total_memory() -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def calculate_auto_partitions(disk_device):
|
def calculate_auto_partitions(disk_device):
|
||||||
"""
|
"""
|
||||||
Generates an automatic partition layout.
|
Generates an automatic partition layout.
|
||||||
- 2GB EFI
|
- 2GB EFI (or 1GB if disk < required)
|
||||||
- RAM + 2GB Swap
|
- Root (Rest)
|
||||||
- Rest Root (ext4)
|
- RAM + 2GB Swap (at end) (or 0 if disk < required)
|
||||||
"""
|
"""
|
||||||
disk_size = 0
|
disk_size = 0
|
||||||
try:
|
try:
|
||||||
@@ -87,16 +87,24 @@ def calculate_auto_partitions(disk_device):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
ram_size = get_total_memory()
|
ram_size = get_total_memory()
|
||||||
|
disk_mb = disk_size / (1024*1024)
|
||||||
|
|
||||||
# Sizes in bytes
|
# Defaults
|
||||||
efi_size = 2 * 1024 * 1024 * 1024
|
efi_size = 2 * 1024 * 1024 * 1024
|
||||||
swap_size = ram_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
|
total_required = efi_size + swap_size + min_root_size
|
||||||
min_root = 10 * 1024 * 1024 * 1024 # minimum 10GB for root
|
|
||||||
if disk_size < (efi_size + swap_size + min_root):
|
use_swap = True
|
||||||
print("Disk too small for automatic partitioning scheme.")
|
|
||||||
return []
|
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
|
root_size = disk_size - efi_size - swap_size
|
||||||
|
|
||||||
@@ -110,15 +118,6 @@ def calculate_auto_partitions(disk_device):
|
|||||||
"bytes": efi_size,
|
"bytes": efi_size,
|
||||||
"style_class": "part-efi",
|
"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",
|
"type": "partition",
|
||||||
"name": "Root",
|
"name": "Root",
|
||||||
@@ -127,8 +126,19 @@ def calculate_auto_partitions(disk_device):
|
|||||||
"size": f"{root_size / (1024**3):.1f} GB",
|
"size": f"{root_size / (1024**3):.1f} GB",
|
||||||
"bytes": root_size,
|
"bytes": root_size,
|
||||||
"style_class": "part-root",
|
"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
|
return partitions
|
||||||
|
|
||||||
@@ -525,6 +535,7 @@ class PartitioningPage(Adw.Bin):
|
|||||||
# Size entry
|
# Size entry
|
||||||
size_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
size_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||||
size_entry = Gtk.Entry(text=str(int(data["bytes"] / (1024*1024))))
|
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(size_entry)
|
||||||
size_box.append(Gtk.Label(label="MB"))
|
size_box.append(Gtk.Label(label="MB"))
|
||||||
box.append(Gtk.Label(label="Size:"))
|
box.append(Gtk.Label(label="Size:"))
|
||||||
|
|||||||
Reference in New Issue
Block a user