Record configured workflow toolchain binary provenance
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
@@ -75,6 +76,32 @@ class WorkflowReportTests(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
workflows.parse_peak_rss("Command exited with non-zero status 1\n")
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -45,6 +45,71 @@ def parse_peak_rss(text):
|
|||||||
raise ValueError("GNU time did not produce a peak RSS record")
|
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):
|
def validate_report(report):
|
||||||
if report.get("format") != "faset.workflow-measurements" or report.get("version") != 2:
|
if report.get("format") != "faset.workflow-measurements" or report.get("version") != 2:
|
||||||
raise ValueError("Expected a version 2 Faset workflow report")
|
raise ValueError("Expected a version 2 Faset workflow report")
|
||||||
@@ -422,6 +487,9 @@ def main():
|
|||||||
except (OSError, subprocess.TimeoutExpired, IndexError):
|
except (OSError, subprocess.TimeoutExpired, IndexError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
binary_provenance = capture_binary_provenance(
|
||||||
|
project / ".faset/build/Debug/CMakeCache.txt", editor, ui_binary)
|
||||||
|
|
||||||
report = {
|
report = {
|
||||||
"format": "faset.workflow-measurements", "version": 2,
|
"format": "faset.workflow-measurements", "version": 2,
|
||||||
"recorded_at_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
"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),
|
"lua_source_project": str(lua_source), "editor": str(editor),
|
||||||
"host": {"platform": platform.platform(), "cpu": cpu, "ram_kib": ram_kib,
|
"host": {"platform": platform.platform(), "cpu": cpu, "ram_kib": ram_kib,
|
||||||
"logical_cpus": os.cpu_count(), "python": platform.python_version()},
|
"logical_cpus": os.cpu_count(), "python": platform.python_version()},
|
||||||
"toolchain": {"cmake": version(["cmake", "--version"]),
|
"toolchain": {name: binary_provenance["binaries"][name]["version"]
|
||||||
"cxx": version(["clang++", "--version"]),
|
for name in ("cmake", "ninja", "c", "cxx", "slang")},
|
||||||
"slang": version(["slangc", "-version"])},
|
"binary_provenance": binary_provenance,
|
||||||
"gpu": {"player_device": player_profiles[0]["device"],
|
"gpu": {"player_device": player_profiles[0]["device"],
|
||||||
"ui_device": ui_data["device"],
|
"ui_device": ui_data["device"],
|
||||||
"driver": version(["nvidia-smi", "--query-gpu=driver_version",
|
"driver": version(["nvidia-smi", "--query-gpu=driver_version",
|
||||||
|
|||||||
Reference in New Issue
Block a user