#!/usr/bin/env python3 """Check a voice-pack directory before a site build ships it. python3 tools/voices/check_packs.py DIR DIR must hold what tools/voices/build_voices.py built for the committed catalogue (data/voices/index.json): its index.json points at index..json, whose bytes are the committed catalogue's, and every file that catalogue names is there with its exact size and SHA-256. Otherwise this exits non-zero and says what is wrong, so a build never ships packs that disagree with the catalogue the site describes, or that are incomplete. Standard library only; Python 3.9 or later (macOS's own python3). """ import hashlib import json import sys from pathlib import Path HERE = Path(__file__).resolve().parent COMMITTED = HERE.parents[1] / "data" / "voices" / "index.json" SHOW = 10 def sha256(path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def file_refs(cat): """Every FileRef the catalogue names: runtimes, components and packs, each with its licence text, and each pack's sample.""" refs = [] owners = list(cat.get("runtimes", {}).values()) + list(cat.get("components", {}).values()) for lang in cat.get("languages", {}).values(): owners += lang.get("packs", []) for owner in owners: refs += owner.get("files", []) for one in ("licence_text", "sample"): if one in owner: refs.append(owner[one]) return refs def check(out): committed = COMMITTED.read_bytes() want = "index.%s.json" % hashlib.sha256(committed).hexdigest()[:12] try: pointer = json.loads((out / "index.json").read_text(encoding="utf-8")) except (OSError, ValueError) as e: return ["%s: no readable index.json (%s)" % (out, e)] problems = [] if pointer.get("current") != want: problems.append( "index.json points at %s, but the committed catalogue is %s: rebuild the packs " "with tools/voices/build_voices.py" % (pointer.get("current"), want) ) catalogue = out / want if not catalogue.is_file() or catalogue.read_bytes() != committed: problems.append("%s is missing, or differs from data/voices/index.json" % want) return problems for ref in file_refs(json.loads(committed)): path = out / ref["url"] if not path.is_file(): problems.append("missing: %s" % ref["url"]) elif path.stat().st_size != ref["bytes"]: problems.append("%s is %d bytes, not %d" % (ref["url"], path.stat().st_size, ref["bytes"])) elif sha256(path) != ref["sha256"]: problems.append("%s does not match its SHA-256" % ref["url"]) return problems def main(argv): if len(argv) != 2: print(__doc__.strip(), file=sys.stderr) return 2 out = Path(argv[1]) problems = check(out) if problems: print("✗ voice packs in %s do not match the committed catalogue:" % out, file=sys.stderr) for p in problems[:SHOW]: print(" - " + p, file=sys.stderr) if len(problems) > SHOW: print(" ... and %d more" % (len(problems) - SHOW), file=sys.stderr) return 1 print("✓ voice packs in %s match the committed catalogue" % out) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))