#!/usr/bin/env python3 from __future__ import annotations import argparse import json import os import subprocess import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent DEFAULT_SMOKE_PACKS = [ "root", "patch1", "patch2", "season3_eu", "metin2_patch_snow", "metin2_patch_snow_dungeon", "metin2_patch_etc_costume1", "metin2_patch_pet1", "metin2_patch_pet2", "metin2_patch_ramadan_costume", "metin2_patch_flame", "metin2_patch_flame_dungeon", "locale", "uiscript", "uiloading", "ETC", "item", "effect", "icon", "property", ] def env_path(name: str) -> Path | None: value = os.environ.get(name, "").strip() return Path(value).expanduser().resolve() if value else None def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Run the Linux headless E2E, runtime gate, and optional Wine .m2p smoke on a self-hosted runner." ) parser.add_argument( "--runtime-root", type=Path, default=env_path("M2_RUNTIME_ROOT"), help="Client runtime root containing assets/, config/, and pack/. Defaults to $M2_RUNTIME_ROOT.", ) parser.add_argument( "--client-repo", type=Path, default=env_path("M2_CLIENT_REPO"), help="Path to m2dev-client-src. Defaults to $M2_CLIENT_REPO.", ) parser.add_argument( "--master-key", type=Path, default=env_path("M2_MASTER_KEY_PATH"), help="Path to runtime master key file. Defaults to $M2_MASTER_KEY_PATH.", ) parser.add_argument( "--key-id", type=str, default=os.environ.get("M2_KEY_ID", "1"), help="Runtime key id for Wine smoke. Defaults to $M2_KEY_ID or 1.", ) parser.add_argument( "--timeout", type=int, default=int(os.environ.get("M2_TIMEOUT", "20")), help="Wine smoke timeout in seconds. Defaults to $M2_TIMEOUT or 20.", ) parser.add_argument( "--skip-headless-e2e", action="store_true", help="Skip scripts/headless_e2e.py.", ) parser.add_argument( "--skip-runtime-gate", action="store_true", help="Skip scripts/validate_runtime_gate.py.", ) parser.add_argument( "--skip-runtime-smoke", action="store_true", help="Skip scripts/runtime_smoke_wine.py.", ) parser.add_argument( "--smoke-pack", dest="smoke_packs", action="append", default=[], help="Pack basename for Wine smoke. Repeatable. Defaults to a known startup-safe set or $M2_RUNTIME_SMOKE_PACKS.", ) parser.add_argument( "--json", action="store_true", help="Emit machine-readable JSON output.", ) return parser.parse_args() def resolve_smoke_packs(cli_values: list[str]) -> list[str]: if cli_values: return cli_values env_value = os.environ.get("M2_RUNTIME_SMOKE_PACKS", "").strip() if env_value: return [part.strip() for part in env_value.split(",") if part.strip()] return list(DEFAULT_SMOKE_PACKS) def run_json(cmd: list[str]) -> dict: completed = subprocess.run( cmd, cwd=REPO_ROOT, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) if completed.returncode != 0: detail = completed.stderr.strip() or completed.stdout.strip() or f"exit code {completed.returncode}" raise RuntimeError(f"command failed: {' '.join(cmd)}\n{detail}") stdout = completed.stdout.strip() return json.loads(stdout) if stdout else {"ok": True} def main() -> int: args = parse_args() smoke_packs = resolve_smoke_packs(args.smoke_packs) summary: dict[str, object] = { "ok": True, "runtime_root": str(args.runtime_root) if args.runtime_root else None, "client_repo": str(args.client_repo) if args.client_repo else None, "smoke_packs": smoke_packs, "steps": {}, } try: if not args.skip_headless_e2e: summary["steps"]["headless_e2e"] = run_json([sys.executable, "scripts/headless_e2e.py"]) else: summary["steps"]["headless_e2e"] = {"ok": True, "skipped": True} if not args.skip_runtime_gate: if not args.runtime_root: raise ValueError("runtime root is required for runtime gate") summary["steps"]["runtime_gate"] = run_json( [ sys.executable, "scripts/validate_runtime_gate.py", "--runtime-root", str(args.runtime_root), "--json", ] ) else: summary["steps"]["runtime_gate"] = {"ok": True, "skipped": True} if not args.skip_runtime_smoke: if not args.runtime_root: raise ValueError("runtime root is required for runtime smoke") if not args.client_repo: raise ValueError("client repo is required for runtime smoke") if not args.master_key: raise ValueError("master key is required for runtime smoke") cmd = [ sys.executable, "scripts/runtime_smoke_wine.py", "--runtime-root", str(args.runtime_root), "--client-repo", str(args.client_repo), "--master-key", str(args.master_key), "--key-id", args.key_id, "--timeout", str(args.timeout), "--json", ] for pack in smoke_packs: cmd.extend(["--pack", pack]) summary["steps"]["runtime_smoke"] = run_json(cmd) else: summary["steps"]["runtime_smoke"] = {"ok": True, "skipped": True} summary["ok"] = all(bool(step.get("ok", False)) for step in summary["steps"].values()) except Exception as exc: summary["ok"] = False summary["error"] = str(exc) if args.json: print(json.dumps(summary, indent=2)) else: print(f"ok={summary['ok']}") for name, result in summary["steps"].items(): print(f"{name}: ok={result.get('ok')} skipped={result.get('skipped', False)}") if "error" in summary: print(f"error: {summary['error']}") return 0 if summary["ok"] else 1 if __name__ == "__main__": sys.exit(main())