"""Shared helper to invoke the real m2pack CLI and translate its JSON. m2pack-secure emits its own JSON envelopes with ``--json``. Their shape is not identical to our :class:`~metin_release.result.Result` envelope, so we wrap the raw dict under ``data["m2pack"]`` and promote a few well-known fields (artifact paths, counts) where it makes sense per command. If m2pack exits non-zero or prints non-JSON, we raise :class:`~metin_release.errors.SubprocessError` so the CLI exits with code 1 and a readable error envelope. """ from __future__ import annotations import json import subprocess from pathlib import Path from typing import Any from ..errors import SubprocessError from ..log import get_logger from ..m2pack_binary import resolve_m2pack_binary def run_m2pack(subcommand: str, argv: list[str]) -> dict[str, Any]: """Invoke ``m2pack [argv...] --json`` and return parsed JSON. Raises :class:`SubprocessError` (exit code 1) on any failure: binary missing, non-zero exit, empty stdout, or non-JSON stdout. Missing binary is handled inside :func:`resolve_m2pack_binary` by raising a :class:`~metin_release.errors.ValidationError` which the dispatcher already converts into the standard error envelope. """ log = get_logger() binary: Path = resolve_m2pack_binary() cmd = [str(binary), subcommand, *argv, "--json"] log.debug("running m2pack: %s", " ".join(cmd)) try: proc = subprocess.run( cmd, capture_output=True, text=True, check=False ) except OSError as exc: raise SubprocessError( f"failed to spawn m2pack: {exc}", error_code="m2pack_spawn_failed", ) from exc stdout = (proc.stdout or "").strip() stderr = (proc.stderr or "").strip() if proc.returncode != 0: detail = stderr or stdout or f"exit code {proc.returncode}" raise SubprocessError( f"m2pack {subcommand} failed (rc={proc.returncode}): {detail}", error_code="m2pack_failed", ) if not stdout: raise SubprocessError( f"m2pack {subcommand} produced no stdout; stderr={stderr!r}", error_code="m2pack_empty_output", ) try: parsed = json.loads(stdout) except json.JSONDecodeError as exc: raise SubprocessError( f"m2pack {subcommand} returned non-JSON output: {exc}; " f"first 200 chars={stdout[:200]!r}", error_code="m2pack_invalid_json", ) from exc if not isinstance(parsed, dict): raise SubprocessError( f"m2pack {subcommand} JSON was not an object: {type(parsed).__name__}", error_code="m2pack_invalid_json", ) return parsed