From 131965583afbb8d93d29a230d0a84149f52b8673 Mon Sep 17 00:00:00 2001 From: Emil <65846814+emil28092005@users.noreply.github.com> Date: Thu, 24 Sep 2026 03:16:26 +0300 Subject: [PATCH] Record configured workflow toolchain binary provenance --- tests/measure_workflows_test.py | 27 ++++++++++++ tools/measure_workflows.py | 74 +++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/tests/measure_workflows_test.py b/tests/measure_workflows_test.py index d2db80c..f89c675 100644 --- a/tests/measure_workflows_test.py +++ b/tests/measure_workflows_test.py @@ -2,6 +2,7 @@ import importlib.util from pathlib import Path +import tempfile import unittest @@ -75,6 +76,32 @@ class WorkflowReportTests(unittest.TestCase): with self.assertRaises(ValueError): workflows.parse_peak_rss("Command exited with non-zero status 1\n") + def test_toolchain_provenance_uses_configured_binaries(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + compiler, slang = root / "chosen-clang", root / "chosen-slangc" + cmake, ninja = root / "chosen-cmake", root / "chosen-ninja" + compiler.write_bytes(b"compiler bytes") + slang.write_bytes(b"slang bytes") + cmake.write_bytes(b"cmake bytes") + ninja.write_bytes(b"ninja bytes") + cache = root / "CMakeCache.txt" + cache.write_text( + f"CMAKE_COMMAND:INTERNAL={cmake}\n" + f"CMAKE_MAKE_PROGRAM:FILEPATH={ninja}\n" + f"CMAKE_C_COMPILER:UNINITIALIZED={compiler}\n" + f"CMAKE_CXX_COMPILER:FILEPATH={compiler}\n" + f"SLANGC_EXECUTABLE:FILEPATH={slang}\n", encoding="utf-8") + self.assertEqual(workflows.read_toolchain_paths(cache), { + "cmake": cmake.resolve(), "ninja": ninja.resolve(), + "c": compiler.resolve(), "cxx": compiler.resolve(), "slang": slang.resolve()}) + self.assertEqual(workflows.sha256_file(slang), + "6193cd06b2d7a7faf124bfc8e35d5e545c3835da61302c51530692dc04080822") + cache.write_text("CMAKE_C_COMPILER:FILEPATH=" + str(compiler) + "\n", + encoding="utf-8") + with self.assertRaises(ValueError): + workflows.read_toolchain_paths(cache) + if __name__ == "__main__": unittest.main() diff --git a/tools/measure_workflows.py b/tools/measure_workflows.py index 24dfa28..015a834 100644 --- a/tools/measure_workflows.py +++ b/tools/measure_workflows.py @@ -45,6 +45,71 @@ def parse_peak_rss(text): raise ValueError("GNU time did not produce a peak RSS record") +def sha256_file(path): + digest = hashlib.sha256() + with Path(path).open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def read_toolchain_paths(cache): + """Read the compiler and Slang paths CMake actually configured for this project.""" + cache = Path(cache) + entries = {} + for line in cache.read_text(encoding="utf-8").splitlines(): + if line.startswith(("#", "//")) or "=" not in line or ":" not in line: + continue + name_and_type, value = line.split("=", 1) + name = name_and_type.split(":", 1)[0] + entries[name] = value + paths = {} + for alias, key in (("cmake", "CMAKE_COMMAND"), + ("ninja", "CMAKE_MAKE_PROGRAM"), + ("c", "CMAKE_C_COMPILER"), + ("cxx", "CMAKE_CXX_COMPILER"), + ("slang", "SLANGC_EXECUTABLE")): + value = entries.get(key) + if not value: + raise ValueError(f"CMake cache lacks configured {key}") + path = Path(value) + if not path.is_absolute(): + path = cache.parent / path + path = path.resolve() + if not path.is_file(): + raise ValueError(f"Configured {key} is not a file: {path}") + paths[alias] = path + return paths + + +def executable_version(path, argument): + try: + result = subprocess.run([str(path), argument], capture_output=True, text=True, + encoding="utf-8", timeout=15) + return next((line for line in (result.stdout + "\n" + result.stderr).splitlines() + if line.strip()), None) + except (OSError, subprocess.TimeoutExpired): + return None + + +def capture_binary_provenance(cache, editor, ui_binary): + """Bind a workflow report to configured tools and the two measured executables.""" + paths = {**read_toolchain_paths(cache), + "editor": Path(editor).resolve(), "ui_latency_binary": Path(ui_binary).resolve()} + arguments = {"cmake": "--version", "ninja": "--version", + "c": "--version", "cxx": "--version", + "slang": "-version", "editor": "--version", + "ui_latency_binary": "--version"} + binaries = {} + for name, path in paths.items(): + if not path.is_file(): + raise ValueError(f"Workflow binary is missing: {path}") + binaries[name] = {"path": str(path), "sha256": sha256_file(path), + "version": executable_version(path, arguments[name]) + if name in {"cmake", "ninja", "c", "cxx", "slang"} else None} + return {"cmake_cache_sha256": sha256_file(cache), "binaries": binaries} + + def validate_report(report): if report.get("format") != "faset.workflow-measurements" or report.get("version") != 2: raise ValueError("Expected a version 2 Faset workflow report") @@ -422,6 +487,9 @@ def main(): except (OSError, subprocess.TimeoutExpired, IndexError): return None + binary_provenance = capture_binary_provenance( + project / ".faset/build/Debug/CMakeCache.txt", editor, ui_binary) + report = { "format": "faset.workflow-measurements", "version": 2, "recorded_at_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(), @@ -431,9 +499,9 @@ def main(): "lua_source_project": str(lua_source), "editor": str(editor), "host": {"platform": platform.platform(), "cpu": cpu, "ram_kib": ram_kib, "logical_cpus": os.cpu_count(), "python": platform.python_version()}, - "toolchain": {"cmake": version(["cmake", "--version"]), - "cxx": version(["clang++", "--version"]), - "slang": version(["slangc", "-version"])}, + "toolchain": {name: binary_provenance["binaries"][name]["version"] + for name in ("cmake", "ninja", "c", "cxx", "slang")}, + "binary_provenance": binary_provenance, "gpu": {"player_device": player_profiles[0]["device"], "ui_device": ui_data["device"], "driver": version(["nvidia-smi", "--query-gpu=driver_version",