"""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