feat: Implement core Arch Linux installation process including disk setup, package installation, and system configuration.

This commit is contained in:
Zeev Diukman 2026-01-19 08:58:54 +02:00
parent 54b1012d74
commit 201b28daef

222
z.py
View file

@ -10,9 +10,139 @@ import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Optional from typing import List, Optional
# ... (imports continue) # Configuration variables
default_packages = [
"base", "linux", "linux-firmware", "btrfs-progs", "nano", "sudo",
"networkmanager", "efibootmgr", "grub", "os-prober", "base-devel", "git"
]
# ... (existing code) @dataclass
class InstallConfig:
seed_device: str
sprout_device: str
efi_device: str
hostname: str = "arch-z"
username: str = "zeev"
timezone: str = "Europe/Helsinki"
root_password: str = ""
user_password: str = ""
packages: List[str] = field(default_factory=lambda: list(default_packages))
dry_run: bool = False
def run_command(command, check=True, shell=False, capture_output=False, dry_run=False):
"""Result wrapper for subprocess.run"""
if dry_run:
print(f"[DRY RUN] Would execute: {command}")
# Return a dummy completed process for dry runs so logic doesn't crash on attribute access
return subprocess.CompletedProcess(args=command, returncode=0, stdout="", stderr="")
try:
# If command is a string and shell is False, split it (naive splitting)
# But better to rely on caller passing list if shell=False
if isinstance(command, str) and not shell:
cmd_list = shlex.split(command)
else:
cmd_list = command
result = subprocess.run(
cmd_list,
check=check,
shell=shell,
text=True,
capture_output=capture_output
)
return result
except subprocess.CalledProcessError as e:
print(f"Error executing command: {command}")
print(f"Error output: {e.stderr}")
if check:
sys.exit(1)
return e
def get_disks():
"""Returns a list of dictionaries with disk info."""
cmd = ["lsblk", "-p", "-dno", "NAME,SIZE,MODEL"]
result = run_command(cmd, capture_output=True)
disks = []
if result.stdout:
lines = result.stdout.strip().split('\n')
for line in lines:
parts = line.split(maxsplit=2)
if len(parts) >= 2:
name = parts[0]
size = parts[1]
model = parts[2] if len(parts) > 2 else ""
disks.append({"name": name, "size": size, "model": model, "raw": line})
return disks
def get_partitions(disk):
"""Returns a list of partitions for the given disk."""
# lsblk -p -nlo NAME,SIZE,TYPE "$selected_disk" | awk '$3=="part" {printf "%s (%s)\n", $1, $2}'
cmd = f"lsblk -p -nlo NAME,SIZE,TYPE {disk}"
result = run_command(cmd, shell=True, capture_output=True)
parts = []
if result.stdout:
lines = result.stdout.strip().split('\n')
for line in lines:
# We want to match awk '$3=="part"' logic
columns = line.split()
if len(columns) >= 3 and columns[2] == "part":
# Create display string "NAME (SIZE)"
display = f"{columns[0]} ({columns[1]})"
parts.append({"path": columns[0], "display": display})
return parts
def run_live_command(command, log_func=None, check=True, shell=False, dry_run=False):
"""Executes a command and streams output to log_func."""
if dry_run:
if log_func:
log_func(f"[DRY RUN] Would execute: {command}")
return
# Use shell=True if command is a string, consistent with run_command logic preference
# though run_command defaults shell=False. We follow the caller's instructions.
# Needs to handle list vs string same as run_command
if isinstance(command, str) and not shell:
cmd_list = shlex.split(command)
else:
cmd_list = command
process = subprocess.Popen(
cmd_list,
shell=shell,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1 # Line buffered
)
if log_func:
for line in process.stdout:
log_func(line.rstrip())
return_code = process.wait()
if check and return_code != 0:
if log_func:
log_func(f"Command failed with return code {return_code}")
# Mimic subprocess.CalledProcessError
raise subprocess.CalledProcessError(return_code, command)
def check_dependencies(log_func=print):
"""Checks if required system tools are available."""
required_tools = [
"lsblk", "btrfs", "mkfs.btrfs", "mkfs.fat",
"pacstrap", "genfstab", "arch-chroot"
]
missing = []
for tool in required_tools:
if not shutil.which(tool):
missing.append(tool)
if missing:
msg = f"Error: Missing required tools: {', '.join(missing)}\nPlease install: btrfs-progs, dosfstools, arch-install-scripts"
log_func(msg)
raise RuntimeError(msg)
def cleanup_mount(mount_point, log_func=print): def cleanup_mount(mount_point, log_func=print):
""" """
@ -48,7 +178,55 @@ def cleanup_mount(mount_point, log_func=print):
return False return False
return True return True
# ... (perform_installation continues) def perform_installation(config: InstallConfig, log_func=print):
"""
Executes the installation process based on the provided configuration.
log_func is a function to display messages (defaults to print).
"""
try:
check_dependencies(log_func)
except RuntimeError:
# If check fails, we stop. In TUI this will define the error.
return
# helper for dry_run propagation
def run(cmd, **kwargs):
# Merge dry_run into kwargs if not explicitly set
if 'dry_run' not in kwargs:
kwargs['dry_run'] = config.dry_run
# If capturing output, we use the standard run_command (buffered)
if kwargs.get('capture_output'):
return run_command(cmd, **kwargs)
# Otherwise, use live command for streaming logs
return run_live_command(cmd, log_func=log_func, **kwargs)
log_func("--- Starting Installation ---")
# Get Sprout PARTUUID
if config.dry_run:
sprout_partuuid = "DRY-RUN-UUID-1234"
else:
cmd_uuid = f"blkid -s PARTUUID -o value {config.sprout_device}"
sprout_partuuid = run(cmd_uuid, shell=True, capture_output=True).stdout.strip()
log_func(f"Sprout PARTUUID: {sprout_partuuid}")
# Check mountpoint
# For dry_run we might want to skip real checks or mock them
if not config.dry_run:
mount_check = subprocess.run(["mountpoint", "-q", "/mnt"], check=False)
if mount_check.returncode == 0:
log_func("/mnt is already mounted. Unmounting...")
cleanup_mount("/mnt", log_func)
log_func("Creating filesystems...")
run(f"mkfs.btrfs -f -L SEED {config.seed_device}", shell=True)
run(f"mkfs.btrfs -f -L SPROUT {config.sprout_device}", shell=True)
run(f"mkfs.fat -F 32 -n EFI {config.efi_device}", shell=True)
log_func("Filesystems created successfully.")
# Initial Mount # Initial Mount
run(f"mount -o subvol=/ {config.seed_device} /mnt", shell=True) run(f"mount -o subvol=/ {config.seed_device} /mnt", shell=True)
@ -63,7 +241,43 @@ def cleanup_mount(mount_point, log_func=print):
cleanup_mount("/mnt", log_func) cleanup_mount("/mnt", log_func)
run(f"mount -o subvol=/@ {config.seed_device} /mnt", shell=True) run(f"mount -o subvol=/@ {config.seed_device} /mnt", shell=True)
# ... (skipping to end of chroot) # Pacstrap
log_func(f"Installing packages: {' '.join(config.packages)}")
run(["pacstrap", "-K", "/mnt"] + config.packages)
run(f"mount -m {config.efi_device} /mnt/efi", shell=True)
# fstab
if not config.dry_run:
with open("/mnt/etc/fstab", "w") as f:
subprocess.run(["genfstab", "-U", "/mnt"], stdout=f, check=True)
else:
log_func("[DRY RUN] Would generate fstab")
# Chroot function
mkinitcpio_hooks = "base udev autodetect microcode modconf kms keyboard block btrfs filesystems"
grub_options = "--target=x86_64-efi --efi-directory=/efi --boot-directory=/boot --bootloader-id=GRUB"
install_script = [
"hwclock --systohc",
f"echo '{config.hostname}' > /etc/hostname",
"echo 'KEYMAP=us' > /etc/vconsole.conf",
"sed -i 's/^#en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen",
"locale-gen",
"echo 'LANG=en_US.UTF-8' > /etc/locale.conf",
f"ln -sf /usr/share/zoneinfo/{config.timezone} /etc/localtime",
f'sed -i "s/^HOOKS=.*/HOOKS=({mkinitcpio_hooks})/" /etc/mkinitcpio.conf',
f"echo 'root:{config.root_password}' | chpasswd",
f"useradd -m -G wheel -s /usr/bin/bash {config.username}",
f"echo '{config.username}:{config.user_password}' | chpasswd",
f"echo '{config.username} ALL=(ALL:ALL) ALL' > /etc/sudoers.d/{config.username}",
"systemctl enable systemd-timesyncd",
f"grub-install {grub_options}",
"echo 'GRUB_DISABLE_OS_PROBER=false' >> /etc/default/grub",
"grub-mkconfig -o /boot/grub/grub.cfg",
f"sed -i 's/root=UUID=[A-Fa-f0-9-]*/root=PARTUUID={sprout_partuuid}/g' /boot/grub/grub.cfg",
"passwd -l root",
"mkinitcpio -P"
]
full_script = "\n".join(install_script) full_script = "\n".join(install_script)
# arch-chroot /mnt /usr/bin/bash -c "$cmd" # arch-chroot /mnt /usr/bin/bash -c "$cmd"