feat(installer): add one-file installer build and custom install ISO

Replace old_installer.py with a curses TUI installer packaged as a
one-file PyInstaller binary (python/installer/build.py, wrapped by
python/installer/package.nix). The default .#installer is patched to run
on foreign Linux live media; the new .#installer-nixos variant keeps its
Nix store interpreter so it runs on NixOS.

Add systems/iso, a minimal NixOS install CD with kernel 6.18 and ZFS 2.4
matching the deployed systems and the installer on PATH; build it with
nix build .#iso. Shared logging and subprocess helpers move out of
common.py into python/logging_config.py and python/process.py.
This commit is contained in:
2026-07-02 14:17:49 -04:00
committed by Richie
parent fbf288649a
commit b252b0bf6e
11 changed files with 559 additions and 851 deletions
+5 -25
View File
@@ -5,32 +5,12 @@ from __future__ import annotations
import curses
import logging
from collections import defaultdict
from subprocess import PIPE, Popen
from python.process import run_output
logger = logging.getLogger(__name__)
def bash_wrapper(command: str) -> str:
"""Execute a bash command and capture the output.
Args:
command (str): The bash command to be executed.
Returns:
Tuple[str, int]: A tuple containing the output of the command (stdout) as a string,
the error output (stderr) as a string (optional), and the return code as an integer.
"""
logger.debug(f"running {command=}")
# This is a acceptable risk
process = Popen(command.split(), stdout=PIPE, stderr=PIPE)
output, _ = process.communicate()
if process.returncode != 0:
error = f"Failed to run command {command=} return code {process.returncode=}"
raise RuntimeError(error)
return output.decode()
class Cursor:
"""Cursor class."""
@@ -144,7 +124,7 @@ def get_device(raw_device: str) -> dict[str, str]:
def get_devices() -> list[dict[str, str]]:
"""Get a list of devices."""
# --bytes
raw_devices = bash_wrapper("lsblk --paths --pairs").splitlines()
raw_devices = run_output(("lsblk", "--paths", "--pairs")).splitlines()
return [get_device(raw_device) for raw_device in raw_devices]
@@ -305,12 +285,12 @@ def get_device_id_mapping() -> dict[str, set[str]]:
Returns:
list[str]: the list of device ids
"""
device_ids = bash_wrapper("find /dev/disk/by-id -type l").splitlines()
device_ids = run_output(("find", "/dev/disk/by-id", "-type", "l")).splitlines()
device_id_mapping: dict[str, set[str]] = defaultdict(set)
for device_id in device_ids:
device = bash_wrapper(f"readlink -f {device_id}").strip()
device = run_output(("readlink", "-f", device_id)).strip()
device_id_mapping[device].add(device_id)
return device_id_mapping