feat: Add Arch Linux installation script with Btrfs seed/sprout support.
This commit is contained in:
parent
a20c36ed50
commit
513b5d80c8
2 changed files with 305 additions and 0 deletions
BIN
__pycache__/z.cpython-314.pyc
Normal file
BIN
__pycache__/z.cpython-314.pyc
Normal file
Binary file not shown.
305
z.py
Executable file
305
z.py
Executable file
|
|
@ -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)
|
||||||
Loading…
Reference in a new issue