From 513b5d80c8fe58c05735035ea6491bd3674805e9 Mon Sep 17 00:00:00 2001 From: Zeev Diukman Date: Mon, 19 Jan 2026 08:10:52 +0200 Subject: [PATCH] feat: Add Arch Linux installation script with Btrfs seed/sprout support. --- __pycache__/z.cpython-314.pyc | Bin 0 -> 14500 bytes z.py | 305 ++++++++++++++++++++++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 __pycache__/z.cpython-314.pyc create mode 100755 z.py diff --git a/__pycache__/z.cpython-314.pyc b/__pycache__/z.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04489a7da7216e4fbeb087b393da0f8307ecc856 GIT binary patch literal 14500 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 Cn^vO$ literal 0 HcmV?d00001 diff --git a/z.py b/z.py new file mode 100755 index 0000000..50752d6 --- /dev/null +++ b/z.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 + +import subprocess +import sys +import os +import shlex +import getpass + +# Configuration variables +selected_disk = "/dev/vda" +seed_device = "/dev/vda1" +sprout_device = "/dev/vda2" +efi_device = "/dev/vda3" + +default_packages = [ + "base", "linux", "linux-firmware", "btrfs-progs", "nano", "sudo", + "networkmanager", "efibootmgr", "grub", "os-prober", "base-devel", "git" +] + +def run_command(command, check=True, shell=False, capture_output=False): + """Result wrapper for subprocess.run""" + 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 select_option(options, prompt_text, default_val=None): + """Generic selection loop""" + if not options: + print("No options available!") + sys.exit(1) + + for i, opt in enumerate(options): + # opt can be a dict or string, we display it accordingly + display = opt['display'] if isinstance(opt, dict) and 'display' in opt else str(opt) + # If it's the raw disk line from earlier, use that + if isinstance(opt, dict) and 'raw' in opt: + display = opt['raw'] + + print(f"{i + 1}) {display}") + + default_idx = 1 + if default_val: + for i, opt in enumerate(options): + val_to_check = opt['name'] if isinstance(opt, dict) and 'name' in opt else \ + (opt['path'] if isinstance(opt, dict) and 'path' in opt else str(opt)) + # Check prefix match like input script + if str(val_to_check).startswith(default_val): + default_idx = i + 1 + break + + while True: + try: + choice = input(f"{prompt_text} (default {default_idx}): ").strip() + except KeyboardInterrupt: + sys.exit(1) + + if not choice: + choice = str(default_idx) + + if choice.isdigit(): + idx = int(choice) + if 1 <= idx <= len(options): + return options[idx - 1] + + print("Invalid selection.") + +def main(): + global selected_disk, seed_device, sprout_device, efi_device + + # 1. Select Disk + print("Available storage disks:") + disks = get_disks() + if not disks: + print("No disks found!") + sys.exit(1) + + choice = select_option(disks, "Select a disk to choose partitions from", selected_disk) + selected_disk = choice['name'] + + # 2. Select Partitions + def select_part(header, prompt, current_val): + print(f"\n{header}") + parts = get_partitions(selected_disk) + if not parts: + print(f"No partitions found on {selected_disk}!") + sys.exit(1) + + choice = select_option(parts, prompt, current_val) + return choice['path'] + + seed_device = select_part("--- Select Seed Partition ---", "Seed device: ", seed_device) + sprout_device = select_part("--- Select Sprout Partition ---", "Sprout device: ", sprout_device) + efi_device = select_part("--- Select EFI Partition ---", "EFI device: ", efi_device) + + # Get Sprout PARTUUID + cmd_uuid = f"blkid -s PARTUUID -o value {sprout_device}" + sprout_partuuid = run_command(cmd_uuid, shell=True, capture_output=True).stdout.strip() + print(f"Sprout PARTUUID: {sprout_partuuid}") + + print("\nConfiguration Summary:") + print(f"Seed device: {seed_device}") + print(f"Sprout device: {sprout_device}") + print(f"EFI device: {efi_device}\n") + + resp = input("Confirm formatting and installation? (yes/no/skip): ").lower() + if resp not in ["yes", "y", "skip"]: + print("Aborting.") + sys.exit(1) + + # Check mountpoint + mount_check = subprocess.run(["mountpoint", "-q", "/mnt"], check=False) + if mount_check.returncode == 0: + print("/mnt is already mounted. Unmounting...") + run_command("umount -R /mnt", check=False) + + if resp == "skip": + print("Skipping formatting") + else: + run_command(f"mkfs.btrfs -f -L SEED {seed_device}", shell=True) + run_command(f"mkfs.btrfs -f -L SPROUT {sprout_device}", shell=True) + run_command(f"mkfs.fat -F 32 -n EFI {efi_device}", shell=True) + print("Filesystems created successfully.") + + # Initial Mount + run_command(f"mount -o subvol=/ {seed_device} /mnt", shell=True) + + # Check for @ subvolume + subvol_list = run_command("btrfs subvolume list /mnt", shell=True, capture_output=True).stdout + if any(line.endswith(" @") or line.endswith("path @") for line in subvol_list.splitlines()): + run_command("btrfs subvolume delete /mnt/@", shell=True) + + run_command("btrfs su cr /mnt/@", shell=True) + run_command("umount -R /mnt", shell=True) + run_command(f"mount -o subvol=/@ {seed_device} /mnt", shell=True) + + # Packages + pkg_input = input("Enter packages to install (space-separated): ").strip() + packages = pkg_input.split() if pkg_input else default_packages + + if packages == default_packages: + print(f"No packages specified. Defaulting to: {' '.join(packages)}") + else: + print(f"The following packages will be installed: {' '.join(packages)}") + + cont = input("Continue with installation? (Yes/no): ").lower() + if cont == "no": + print("Aborting.") + sys.exit(1) + + # Pacstrap + run_command(["pacstrap", "-K", "/mnt"] + packages) + run_command(f"mount -m {efi_device} /mnt/efi", shell=True) + + # fstab + with open("/mnt/etc/fstab", "w") as f: + subprocess.run(["genfstab", "-U", "/mnt"], stdout=f, check=True) + + # User Configuration + print("--- System Configuration ---") + hostname = input("Enter hostname (default: arch-z): ").strip() or "arch-z" + user = input("Enter username (default: zeev): ").strip() or "zeev" + timezone = input("Enter timezone (default: Europe/Helsinki): ").strip() or "Europe/Helsinki" + + print("Set root password:") + while True: + p1 = getpass.getpass("Password: ") + p2 = getpass.getpass("Confirm Password: ") + if p1 == p2: + root_pass = p1 + break + print("Passwords do not match. Try again.") + + print(f"Set password for user {user}:") + while True: + p1 = getpass.getpass("Password: ") + p2 = getpass.getpass("Confirm Password: ") + if p1 == p2: + user_pass = p1 + break + print("Passwords do not match. Try again.") + + # 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 '{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/{timezone} /etc/localtime", + f'sed -i "s/^HOOKS=.*/HOOKS=({mkinitcpio_hooks})/" /etc/mkinitcpio.conf', + f"echo 'root:{root_pass}' | chpasswd", + f"useradd -m -G wheel -s /usr/bin/bash {user}", + f"echo '{user}:{user_pass}' | chpasswd", + f"echo '{user} ALL=(ALL:ALL) ALL' > /etc/sudoers.d/{user}", + "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) + # arch-chroot /mnt /usr/bin/bash -c "$cmd" + # We pass the full script as one argument to bash -c + run_command(["arch-chroot", "/mnt", "/usr/bin/bash", "-c", full_script]) + + # Final cleanup + print("--- Finalizing Seed/Sprout setup ---") + print("Unmounting /mnt...") + run_command("umount -R /mnt", shell=True) + + print(f"Converting {seed_device} to a seed device...") + run_command(f"btrfstune -S 1 {seed_device}", shell=True) + + print("Mounting seed device to add sprout...") + run_command(f"mount -o subvol=/@ {seed_device} /mnt", shell=True) + + print(f"Adding {sprout_device} as sprout device...") + run_command(f"btrfs device add -f {sprout_device} /mnt", shell=True) + + print("Unmounting and remounting sprout device...") + run_command("umount -R /mnt", shell=True) + run_command(f"mount -o subvol=/@ {sprout_device} /mnt", shell=True) + + print("Mounting EFI partition...") + run_command(f"mount -m {efi_device} /mnt/efi", shell=True) + + print("Generating final fstab with PARTUUIDs...") + with open("/mnt/etc/fstab", "w") as f: + subprocess.run(["genfstab", "-t", "PARTUUID", "/mnt"], stdout=f, check=True) + + print("\n################################################################") + print("# INSTALLATION COMPLETE #") + print("################################################################\n") + + reboot_ans = input("Do you want to reboot now? (y/N): ").lower() + if reboot_ans in ["y", "yes"]: + run_command("reboot", shell=True) + else: + print("You can reboot manually by typing 'reboot'.") + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nInterrupted.") + sys.exit(1)