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.
53 lines
1.7 KiB
Python
53 lines
1.7 KiB
Python
"""Small subprocess helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import shutil
|
|
from subprocess import run
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterable, Sequence
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CommandError(RuntimeError):
|
|
"""Raised when an external command fails."""
|
|
|
|
def __init__(self, command: Sequence[str], returncode: int, stderr: str) -> None:
|
|
"""Store command failure details."""
|
|
command_text = " ".join(command)
|
|
super().__init__(f"Failed to run command {command_text!r}: exit {returncode}\n{stderr}")
|
|
self.command = command
|
|
self.returncode = returncode
|
|
self.stderr = stderr
|
|
|
|
|
|
class MissingCommandsError(RuntimeError):
|
|
"""Raised when required external commands are not available."""
|
|
|
|
def __init__(self, commands: Sequence[str]) -> None:
|
|
"""Store missing command details."""
|
|
missing = ", ".join(commands)
|
|
super().__init__(f"Missing required installer commands: {missing}")
|
|
self.commands = commands
|
|
|
|
|
|
def require_commands(commands: Iterable[str]) -> None:
|
|
"""Raise when one or more executables are missing from PATH."""
|
|
missing_commands = sorted({command for command in commands if shutil.which(command) is None})
|
|
if missing_commands:
|
|
raise MissingCommandsError(missing_commands)
|
|
|
|
|
|
def run_output(command: Sequence[str]) -> str:
|
|
"""Run a command and return stdout."""
|
|
logger.debug("running command=%s", command)
|
|
result = run(command, capture_output=True, text=True, check=False)
|
|
if result.returncode != 0:
|
|
raise CommandError(command, result.returncode, result.stderr)
|
|
|
|
return result.stdout
|