|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Fail if the committed ``td_component/tdpilot_API.tox`` is out of sync with source. |
| 3 | +
|
| 4 | +Parallel to ``scripts/check_tox_freshness.py`` (which guards |
| 5 | +``tdpilot-dpsk4.tox``). The .tox is a binary TD artifact and can only |
| 6 | +be rebuilt inside TouchDesigner. Pre-2.1.1 only the dpsk4 .tox had a |
| 7 | +CI gate, so the API .tox went stale silently if a contributor edited |
| 8 | +``tdpilot_api_runtime.py``, ``tdpilot_api_chat.html``, or any other |
| 9 | +file under the API source tree without rebuilding inside TD. Users |
| 10 | +installing the plugin would get an old binary while CI stayed green. |
| 11 | +
|
| 12 | +This guard compares the hash of the source files against the hash |
| 13 | +recorded in ``td_component/.tox-api-source-hash.json`` at build time. |
| 14 | +Mismatch -> "rebuild the API .tox in TD before pushing". |
| 15 | +
|
| 16 | +Runs in CI. Also runnable locally. |
| 17 | +""" |
| 18 | + |
| 19 | +from __future__ import annotations |
| 20 | + |
| 21 | +import hashlib |
| 22 | +import json |
| 23 | +import sys |
| 24 | +from pathlib import Path |
| 25 | + |
| 26 | +ROOT = Path(__file__).resolve().parents[1] |
| 27 | + |
| 28 | +# Must match _API_TOX_SOURCE_FILES in td_component/build_tdpilot_api_tox.py. |
| 29 | +# Add new files in BOTH places — the build script's list drives the hash |
| 30 | +# that gets written; this list drives the check that compares to it. |
| 31 | +# A drift between the two lists shows up as a stable mismatch even with |
| 32 | +# no source edits (different inputs -> different hashes). |
| 33 | +SOURCE_FILES = ( |
| 34 | + # Direct embeds — every file listed in the build script's _SOURCE_FILES |
| 35 | + # tuple as a real path (i.e. not the `<COMPOSE>` sentinel). |
| 36 | + "td_component/tdpilot_api_agent.py", |
| 37 | + "td_component/tdpilot_api_dispatcher.py", |
| 38 | + "td_component/tdpilot_api_config.py", |
| 39 | + "td_component/tdpilot_api_lookup.py", |
| 40 | + "td_component/tdpilot_api_schema_defs.py", |
| 41 | + "td_component/tdpilot_api_schema_map.py", |
| 42 | + "td_component/tdpilot_api_schema.py", |
| 43 | + "td_component/tdpilot_api_runtime.py", |
| 44 | + "td_component/tdpilot_api_extension.py", |
| 45 | + "td_component/tdpilot_api_bm25.py", |
| 46 | + "td_component/tdpilot_api_memory.py", |
| 47 | + "td_component/tdpilot_api_knowledge.py", |
| 48 | + "td_component/tdpilot_api_recipes.py", |
| 49 | + "td_component/tdpilot_api_skills.py", |
| 50 | + "td_component/tdpilot_api_patches.py", |
| 51 | + "td_component/tdpilot_api_user_tools.py", |
| 52 | + "td_component/tdpilot_api_subagents.py", |
| 53 | + "td_component/tdpilot_api_macros.py", |
| 54 | + "td_component/tdpilot_api_official_docs.py", |
| 55 | + "td_component/tdpilot_api_td2025.py", |
| 56 | + "td_component/tdpilot_api_introspect.py", |
| 57 | + "td_component/tdpilot_api_batch.py", |
| 58 | + "td_component/tdpilot_api_recovery.py", |
| 59 | + "td_component/tdpilot_api_tracing.py", |
| 60 | + "td_component/tdpilot_api_compaction.py", |
| 61 | + "td_component/tdpilot_api_chat.html", |
| 62 | + "td_component/tdpilot_api_web_callbacks.py", |
| 63 | + "td_component/tdpilot_api_executor.py", |
| 64 | + "td_component/tdpilot_api_parexec.py", |
| 65 | + # Composed mcp_webserver_callbacks textDAT — its body comes from the |
| 66 | + # callbacks/ split package (overlaps with check_tox_freshness.py by |
| 67 | + # design; both .tox files embed this composed content). |
| 68 | + "td_component/callbacks/_composer.py", |
| 69 | + "td_component/callbacks/__init__.py", |
| 70 | + "td_component/callbacks/_header.py", |
| 71 | + "td_component/callbacks/router.py", |
| 72 | + "td_component/callbacks/auth.py", |
| 73 | + "td_component/callbacks/serializers.py", |
| 74 | + "td_component/callbacks/handlers/__init__.py", |
| 75 | + "td_component/callbacks/handlers/nodes.py", |
| 76 | + "td_component/callbacks/handlers/exec_and_custom_params.py", |
| 77 | + "td_component/callbacks/handlers/exec_python.py", |
| 78 | + "td_component/callbacks/handlers/inspect.py", |
| 79 | + "td_component/callbacks/handlers/search.py", |
| 80 | + "td_component/callbacks/handlers/lifecycle.py", |
| 81 | + "td_component/callbacks/handlers/pulse.py", |
| 82 | + "td_component/callbacks/handlers/monitor.py", |
| 83 | + "td_component/callbacks/handlers/analyze_frame.py", |
| 84 | + # Build script bytes — same reasoning as the dpsk4 gate (any change to |
| 85 | + # how the .tox is laid out forces a rebuild signal even if no embedded |
| 86 | + # source changed). |
| 87 | + "td_component/build_tdpilot_api_tox.py", |
| 88 | +) |
| 89 | +HASH_FILE = ROOT / "td_component" / ".tox-api-source-hash.json" |
| 90 | +TOX_FILE = ROOT / "td_component" / "tdpilot_API.tox" |
| 91 | + |
| 92 | + |
| 93 | +def compute_current_hash() -> str: |
| 94 | + h = hashlib.sha256() |
| 95 | + for rel in SOURCE_FILES: |
| 96 | + path = ROOT / rel |
| 97 | + if not path.exists(): |
| 98 | + continue |
| 99 | + h.update(rel.encode("utf-8")) |
| 100 | + h.update(b"\x00") |
| 101 | + h.update(path.read_bytes()) |
| 102 | + h.update(b"\x00") |
| 103 | + return h.hexdigest() |
| 104 | + |
| 105 | + |
| 106 | +def main() -> int: |
| 107 | + if not TOX_FILE.exists(): |
| 108 | + print("ERROR: " + str(TOX_FILE.relative_to(ROOT)) + " is missing.") |
| 109 | + print(" Rebuild it inside TouchDesigner via") |
| 110 | + print(" td_component/build_tdpilot_api_tox.py in the Textport.") |
| 111 | + return 1 |
| 112 | + |
| 113 | + if not HASH_FILE.exists(): |
| 114 | + print("ERROR: " + str(HASH_FILE.relative_to(ROOT)) + " is missing.") |
| 115 | + print(" Rebuild the API .tox in TouchDesigner — the rebuild writes this file.") |
| 116 | + return 1 |
| 117 | + |
| 118 | + stored = json.loads(HASH_FILE.read_text()) |
| 119 | + stored_hash = stored.get("tox_source_hash") |
| 120 | + current_hash = compute_current_hash() |
| 121 | + |
| 122 | + if stored_hash != current_hash: |
| 123 | + print("ERROR: tdpilot_API.tox is stale relative to td_component source.") |
| 124 | + print(" stored hash: " + str(stored_hash)) |
| 125 | + print(" current hash: " + current_hash) |
| 126 | + print(" built at: " + str(stored.get("built_at"))) |
| 127 | + print("") |
| 128 | + print("Rebuild the API .tox in TouchDesigner before pushing.") |
| 129 | + print(" See td_component/build_tdpilot_api_tox.py and the Textport recipe in") |
| 130 | + print(" feedback_td_tox_rebuild_recipe.md (single-line statements only).") |
| 131 | + print(" Then: git add td_component/tdpilot_API.tox td_component/.tox-api-source-hash.json") |
| 132 | + return 1 |
| 133 | + |
| 134 | + print( |
| 135 | + "tdpilot_API.tox is fresh (hash " |
| 136 | + + current_hash[:16] |
| 137 | + + "..., built " |
| 138 | + + str(stored.get("built_at")) |
| 139 | + + ")" |
| 140 | + ) |
| 141 | + return 0 |
| 142 | + |
| 143 | + |
| 144 | +if __name__ == "__main__": |
| 145 | + sys.exit(main()) |
0 commit comments