#!/usr/bin/env python3 """Генерация release/manifest-VERSION.json (SHA256 git blob = checkout на Linux).""" from __future__ import annotations import argparse import hashlib import json import os import subprocess import sys from pathlib import Path MANIFEST_FILES = ( "ssh-monitor", "sac-client.sh", "update_ssh_monitor.sh", "ssh-monitor-watchdog", "ssh-monitor-perms.sh", ) def repo_root() -> Path: return Path( subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip() ) def read_version(root: Path, explicit: str | None) -> str: if explicit: return explicit.strip() vt = root / "version.txt" if vt.is_file(): v = vt.read_text(encoding="utf-8").strip() if v: return v ssh = root / "ssh-monitor" if ssh.is_file(): for line in ssh.read_text(encoding="utf-8", errors="replace").splitlines(): if line.strip().startswith("SSH_MONITOR_VERSION="): v = line.split("=", 1)[1].strip().strip("\"'") if v: return v raise SystemExit("ERROR: укажите VERSION или заполните version.txt / SSH_MONITOR_VERSION") def file_bytes_from_git(root: Path, name: str) -> bytes | None: for ref in (f":{name}", f"HEAD:{name}"): try: return subprocess.check_output(["git", "-C", str(root), "show", ref]) except subprocess.CalledProcessError: continue return None def sha256_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def sha256_release_file(root: Path, name: str) -> str: blob = file_bytes_from_git(root, name) if blob is not None: return sha256_bytes(blob) path = root / name if not path.is_file(): raise SystemExit(f"ERROR: нет файла {name} (ни в git index/HEAD, ни на диске)") h = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def main() -> None: parser = argparse.ArgumentParser(description="Сгенерировать release/manifest-VERSION.json") parser.add_argument("version", nargs="?", help="Версия (по умолчанию из version.txt)") parser.add_argument( "--pin-commit", action="store_true", help="Записать git_commit=HEAD (после коммита; в pre-commit не используется)", ) args = parser.parse_args() root = repo_root() version = read_version(root, args.version) for name in MANIFEST_FILES: if file_bytes_from_git(root, name) is None and not (root / name).is_file(): raise SystemExit(f"ERROR: нет файла {name}") git_commit = "" if args.pin_commit: try: git_commit = subprocess.check_output( ["git", "-C", str(root), "rev-parse", "HEAD"], text=True ).strip() except subprocess.CalledProcessError: pass manifest = { "version": version, "git_commit": git_commit, "files": {name: f"sha256:{sha256_release_file(root, name)}" for name in MANIFEST_FILES}, } out = root / "release" / f"manifest-{version}.json" out.parent.mkdir(parents=True, exist_ok=True) with out.open("w", encoding="utf-8") as f: json.dump(manifest, f, indent=2, ensure_ascii=False) f.write("\n") pin = git_commit[:12] if git_commit else "(empty)" print(f"Wrote {out.relative_to(root)} git_commit={pin}") if __name__ == "__main__": main()