71 lines
3.3 KiB
Python
71 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Inspect one module's published files without loading its CAD runtime."""
|
|
import argparse
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import time
|
|
from urllib.parse import unquote, urlsplit
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def inspect(module: str, root: Path = ROOT) -> dict:
|
|
started = time.perf_counter()
|
|
public = (root / "web/public").resolve()
|
|
manifest = json.loads((public / "modules/manifest.json").read_text(encoding="utf-8"))
|
|
descriptor = manifest.get("modules", {}).get(module)
|
|
if not descriptor:
|
|
raise ValueError(f"module not published: {module}")
|
|
files = {}
|
|
for worker, artifact in descriptor.get("workers", {}).items():
|
|
for kind, field in (("runtime", "runtimeUrl"), ("wasm", "wasmUrl"), ("data", "dataUrl"), ("pthreadWorker", "pthreadWorkerUrl")):
|
|
url = artifact.get(field)
|
|
if not url and kind not in {"runtime", "wasm"}:
|
|
continue
|
|
if not isinstance(url, str):
|
|
raise ValueError(f"missing {field}: {worker}")
|
|
parts = urlsplit(url)
|
|
path = (public / unquote(parts.path).lstrip("/")).resolve()
|
|
if parts.scheme or parts.netloc or not path.is_relative_to(public):
|
|
raise ValueError(f"artifact must be inside local public directory: {url}")
|
|
expected = artifact.get("hashes", {}).get(kind)
|
|
if url not in files:
|
|
if not path.is_file():
|
|
files[url] = {"url": url, "present": False, "matches": False}
|
|
else:
|
|
with path.open("rb") as stream:
|
|
checksum = hashlib.file_digest(stream, "sha256").hexdigest()
|
|
files[url] = {"url": url, "path": str(path.relative_to(root)), "present": True,
|
|
"bytes": path.stat().st_size, "sha256": checksum, "matches": True}
|
|
files[url]["matches"] = files[url]["matches"] and isinstance(expected, str) and files[url].get("sha256") == expected
|
|
passed = bool(files) and all(row["present"] and row["matches"] for row in files.values())
|
|
return {"schema": "freecad-web-module-artifact-inspection/v1", "module": module, "status": "passed" if passed else "failed",
|
|
"executedAt": datetime.now(timezone.utc).isoformat(), "seconds": round(time.perf_counter() - started, 6),
|
|
"files": list(files.values()), "scope": "Published file presence and hash inspection; not a CAD business acceptance result."}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--module", required=True)
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
result = inspect(args.module)
|
|
except (OSError, ValueError) as error:
|
|
print(json.dumps({"status": "failed", "error": str(error)}))
|
|
return 1
|
|
if args.output:
|
|
output = (ROOT / args.output).resolve()
|
|
if not output.is_relative_to(ROOT):
|
|
parser.error("output must remain inside the project")
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(result))
|
|
return 0 if result["status"] == "passed" else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|