#!/usr/bin/env python3 """Check whether source extension files still match a 1C patch workspace manifest.""" from __future__ import annotations import argparse import hashlib import json from pathlib import Path from typing import Any def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8-sig")) def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def issue(severity: str, code: str, message: str, *, record: dict[str, Any] | None = None) -> dict[str, Any]: result = {"severity": severity, "code": code, "message": message} if record: result["record"] = record return result def check_workspace_sources(workspace: Path) -> dict[str, Any]: manifest_path = workspace / "manifest.json" findings = [] file_checks = [] if not manifest_path.exists(): findings.append(issue("error", "missing_manifest", f"Workspace manifest.json is missing: {manifest_path}")) return result(workspace, findings, file_checks) manifest = load_json(manifest_path) for record in manifest.get("files") or []: source = Path(str(record.get("source_path") or "")) expected = record.get("sha256") check = { "relative_path": record.get("relative_path"), "source_path": str(source), "expected_sha256": expected, "source_exists": source.exists(), } if not source.exists(): findings.append(issue("error", "source_missing", f"Source file is missing: {source}", record=record)) file_checks.append(check) continue actual = sha256_file(source) check["source_sha256"] = actual check["fresh"] = actual == expected if expected and actual != expected: findings.append(issue("error", "source_hash_mismatch", "Source file changed since patch workspace creation.", record={**record, "current_sha256": actual})) file_checks.append(check) return result(workspace, findings, file_checks, manifest=manifest) def result(workspace: Path, findings: list[dict[str, Any]], file_checks: list[dict[str, Any]], manifest: dict[str, Any] | None = None) -> dict[str, Any]: errors = [row for row in findings if row.get("severity") == "error"] warnings = [row for row in findings if row.get("severity") == "warning"] return { "schema": "onec_patch_source_freshness.v1", "workspace": str(workspace), "manifest_schema": (manifest or {}).get("schema"), "passed": not errors, "findings": findings, "file_checks": file_checks, "counts": { "files": len(file_checks), "errors": len(errors), "warnings": len(warnings), "stale": len([row for row in file_checks if row.get("fresh") is False]), "missing": len([row for row in file_checks if not row.get("source_exists")]), }, } def main() -> int: parser = argparse.ArgumentParser(description="Check 1C patch source freshness.") parser.add_argument("--workspace", type=Path, required=True) parser.add_argument("--output", type=Path) args = parser.parse_args() check = check_workspace_sources(args.workspace) output = json.dumps(check, ensure_ascii=False, indent=2) + "\n" if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(output, encoding="utf-8") print(json.dumps({"output": str(args.output) if args.output else None, "passed": check["passed"], "counts": check["counts"]}, ensure_ascii=False)) return 0 if check["passed"] else 2 if __name__ == "__main__": raise SystemExit(main())