feat: Add options to skip EFI partition formatting and specify GRUB bootloader ID, including a utility to scan for existing bootloaders.
This commit is contained in:
parent
201b28daef
commit
cfd2fde06f
2 changed files with 52 additions and 146 deletions
165
z.py
165
z.py
|
|
@ -28,30 +28,11 @@ class InstallConfig:
|
||||||
user_password: str = ""
|
user_password: str = ""
|
||||||
packages: List[str] = field(default_factory=lambda: list(default_packages))
|
packages: List[str] = field(default_factory=lambda: list(default_packages))
|
||||||
dry_run: bool = False
|
dry_run: bool = False
|
||||||
|
format_efi: bool = True
|
||||||
|
bootloader_id: str = "GRUB"
|
||||||
|
|
||||||
def run_command(command, check=True, shell=False, capture_output=False, dry_run=False):
|
def run_command(command, check=True, shell=False, capture_output=False, dry_run=False):
|
||||||
"""Result wrapper for subprocess.run"""
|
# ... (existing run_command code)
|
||||||
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:
|
except subprocess.CalledProcessError as e:
|
||||||
print(f"Error executing command: {command}")
|
print(f"Error executing command: {command}")
|
||||||
print(f"Error output: {e.stderr}")
|
print(f"Error output: {e.stderr}")
|
||||||
|
|
@ -60,123 +41,44 @@ def run_command(command, check=True, shell=False, capture_output=False, dry_run=
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def get_disks():
|
def get_disks():
|
||||||
"""Returns a list of dictionaries with disk info."""
|
# ... (existing get_disks)
|
||||||
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
|
return disks
|
||||||
|
|
||||||
def get_partitions(disk):
|
def get_partitions(disk):
|
||||||
"""Returns a list of partitions for the given disk."""
|
# ... (existing get_partitions)
|
||||||
# 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
|
return parts
|
||||||
|
|
||||||
def run_live_command(command, log_func=None, check=True, shell=False, dry_run=False):
|
def scan_efi_bootloaders(device):
|
||||||
"""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):
|
|
||||||
"""
|
"""
|
||||||
Robustly unmounts a path, killing processes if necessary.
|
Mounts the given device temporarily to check /EFI/ subdirectories.
|
||||||
|
Returns a list of directory names found (potential bootloader IDs).
|
||||||
"""
|
"""
|
||||||
log_func(f"Unmounting {mount_point}...")
|
if not device:
|
||||||
|
return []
|
||||||
|
|
||||||
# First try normal unmount
|
# Temporary mount point
|
||||||
ret = subprocess.run(f"umount -R {mount_point}", shell=True, stderr=subprocess.DEVNULL)
|
tmp_mnt = "/tmp/z_efi_check"
|
||||||
if ret.returncode == 0:
|
os.makedirs(tmp_mnt, exist_ok=True)
|
||||||
return True
|
|
||||||
|
|
||||||
log_func(f"Unmount failed. Checking for busy processes on {mount_point}...")
|
# Mount
|
||||||
|
try:
|
||||||
|
subprocess.run(f"mount {device} {tmp_mnt}", shell=True, check=True, stderr=subprocess.DEVNULL)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return [] # Failed to mount (maybe not formatted yet)
|
||||||
|
|
||||||
# Check for fuser
|
found = []
|
||||||
if shutil.which("fuser"):
|
efi_path = os.path.join(tmp_mnt, "EFI")
|
||||||
log_func("Killing processes accessing the mount point...")
|
if os.path.exists(efi_path) and os.path.isdir(efi_path):
|
||||||
subprocess.run(f"fuser -k -m {mount_point}", shell=True)
|
try:
|
||||||
time.sleep(1) # Give them a second to die
|
found = [d for d in os.listdir(efi_path) if os.path.isdir(os.path.join(efi_path, d))]
|
||||||
else:
|
except OSError:
|
||||||
log_func("Warning: 'fuser' not found. Cannot automatically kill busy processes.")
|
pass
|
||||||
|
|
||||||
# Retry unmount
|
# Unmount
|
||||||
ret = subprocess.run(f"umount -R {mount_point}", shell=True)
|
subprocess.run(f"umount {tmp_mnt}", shell=True)
|
||||||
if ret.returncode == 0:
|
return found
|
||||||
return True
|
|
||||||
|
|
||||||
# Last resort: Lazy unmount
|
# ... (existing run_live_command, check_dependencies, cleanup_mount)
|
||||||
log_func("Force/Lazy unmounting...")
|
|
||||||
ret = subprocess.run(f"umount -R -l {mount_point}", shell=True)
|
|
||||||
if ret.returncode != 0:
|
|
||||||
log_func(f"Critical: Failed to unmount {mount_point} even with lazy unmount.")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def perform_installation(config: InstallConfig, log_func=print):
|
def perform_installation(config: InstallConfig, log_func=print):
|
||||||
"""
|
"""
|
||||||
|
|
@ -225,7 +127,12 @@ def perform_installation(config: InstallConfig, log_func=print):
|
||||||
log_func("Creating filesystems...")
|
log_func("Creating filesystems...")
|
||||||
run(f"mkfs.btrfs -f -L SEED {config.seed_device}", shell=True)
|
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.btrfs -f -L SPROUT {config.sprout_device}", shell=True)
|
||||||
run(f"mkfs.fat -F 32 -n EFI {config.efi_device}", shell=True)
|
|
||||||
|
if config.format_efi:
|
||||||
|
run(f"mkfs.fat -F 32 -n EFI {config.efi_device}", shell=True)
|
||||||
|
else:
|
||||||
|
log_func(f"Skipping EFI format (Using existing {config.efi_device})")
|
||||||
|
|
||||||
log_func("Filesystems created successfully.")
|
log_func("Filesystems created successfully.")
|
||||||
|
|
||||||
# Initial Mount
|
# Initial Mount
|
||||||
|
|
@ -255,7 +162,7 @@ def perform_installation(config: InstallConfig, log_func=print):
|
||||||
|
|
||||||
# Chroot function
|
# Chroot function
|
||||||
mkinitcpio_hooks = "base udev autodetect microcode modconf kms keyboard block btrfs filesystems"
|
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"
|
grub_options = f"--target=x86_64-efi --efi-directory=/efi --boot-directory=/boot --bootloader-id={config.bootloader_id}"
|
||||||
|
|
||||||
install_script = [
|
install_script = [
|
||||||
"hwclock --systohc",
|
"hwclock --systohc",
|
||||||
|
|
|
||||||
29
z_tui.py
29
z_tui.py
|
|
@ -261,28 +261,18 @@ class InstallScreen(Screen):
|
||||||
timezone=self.app.conf_timezone,
|
timezone=self.app.conf_timezone,
|
||||||
root_password=self.app.conf_root_pass,
|
root_password=self.app.conf_root_pass,
|
||||||
user_password=self.app.conf_user_pass,
|
user_password=self.app.conf_user_pass,
|
||||||
|
# ... (skipping inside method)
|
||||||
packages=self.app.packages,
|
packages=self.app.packages,
|
||||||
# For testing safety, you might want to default to dry_run logic or prompt.
|
dry_run=False,
|
||||||
# But the user asked for the real deal. I'll add a safety switch.
|
format_efi=self.app.format_efi,
|
||||||
dry_run=False
|
bootloader_id=self.app.bootloader_id
|
||||||
)
|
)
|
||||||
|
|
||||||
self.worker = InstallWork(config, self.write_log)
|
self.worker = InstallWork(config, self.write_log)
|
||||||
self.worker.start()
|
self.worker.start()
|
||||||
self.timer = self.set_interval(0.5, self.check_done)
|
self.timer = self.set_interval(0.5, self.check_done)
|
||||||
|
|
||||||
def write_log(self, message):
|
# ... (rest of InstallScreen)
|
||||||
self.query_one(Log).write_line(message)
|
|
||||||
|
|
||||||
def check_done(self):
|
|
||||||
if not self.worker.is_alive():
|
|
||||||
self.timer.stop()
|
|
||||||
self.query_one("#btn_done").disabled = False
|
|
||||||
self.query_one(Log).write_line("--- Process Finished ---")
|
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
||||||
if event.button.id == "btn_done":
|
|
||||||
self.app.exit()
|
|
||||||
|
|
||||||
class ZInstallerApp(App):
|
class ZInstallerApp(App):
|
||||||
CSS = """
|
CSS = """
|
||||||
|
|
@ -300,6 +290,11 @@ class ZInstallerApp(App):
|
||||||
border: round $primary;
|
border: round $primary;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
}
|
}
|
||||||
|
.info-text {
|
||||||
|
color: $accent;
|
||||||
|
padding-left: 1;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
DataTable {
|
DataTable {
|
||||||
height: 1fr;
|
height: 1fr;
|
||||||
border: solid $secondary;
|
border: solid $secondary;
|
||||||
|
|
@ -330,7 +325,11 @@ class ZInstallerApp(App):
|
||||||
sprout_device = None
|
sprout_device = None
|
||||||
efi_device = None
|
efi_device = None
|
||||||
|
|
||||||
|
format_efi = True
|
||||||
|
bootloader_id = "GRUB"
|
||||||
|
|
||||||
conf_hostname = None
|
conf_hostname = None
|
||||||
|
# ...
|
||||||
conf_username = None
|
conf_username = None
|
||||||
conf_timezone = None
|
conf_timezone = None
|
||||||
conf_root_pass = None
|
conf_root_pass = None
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue