Foundry/tt/tools/record_tensix_image_provena...

166 lines
6.8 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 David (Corey) Herrera
"""
record_tensix_image_provenance.py
Write the `<image>.prov` record beside a Tensix ELF the two builders just linked.
This is the WRITE half of tensix_image_provenance.py; check_tensix_program_provenance.py
is the read half. It is called by build_tensix_kernel.sh and
build_tensix_dataflow_kernel.sh at the point each `.src` sidecar is written, because
that is the only place in this tree that knows BOTH the recipe it was handed and the
depfile the compiler just produced.
Usage:
record_tensix_image_provenance.py --tt-root ../ --image OUT/brisc.elf \\
--source kernels/resident_gdn_reader.c \\
--depfile OUT/gen_brisc/kernel_body.d [--depfile ...] \\
[--extra-input kernels/cb_tables/gdn_float32.cbtable] \\
--recipe ctargs=0,1,2 --recipe noc=0 ...
THE INPUT SET COMES FROM THE COMPILER, NOT FROM A SCAN. `--depfile` takes the `-MMD`
output of the compile that produced this image, so the recorded closure is the files
the compiler actually opened. A hand-written list of "what a kernel includes" would be
a second, drifting model of the include graph, and the whole point of this record is
that it cannot drift.
`--extra-input` is for an input the COMPILER NEVER SEES: a `.cbtable` is read by
tools/emit_tensix_cb_formats.py, which writes the `ft_tensix_cb_formats.h` the compute
images are built against, so the table's content is part of those images and appears in
no depfile. The generated header itself is deliberately NOT recorded -- it lives in the
scratch build directory, not in the tree, so re-hashing it later would check a file
that no longer exists.
Files OUTSIDE --tt-root are dropped: the SFPI toolchain headers are pinned
and are not in this tree, so recording them would make the check unrunnable anywhere
but the card host. tensix_image_provenance.py's header states that limit.
Status: added 2026-08-19 with the provenance format.
Issues: None
Todo: None
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from tensix_image_provenance import RECIPE_KEYS, build_record, hash_file # noqa: E402
def parse_depfile(path):
"""Every prerequisite in a `-MMD` depfile, as written. The target before the first
colon is dropped; `\\`-continuations are joined."""
with open(path) as handle:
text = handle.read()
text = text.replace("\\\n", " ")
prerequisites = []
for line in text.splitlines():
_, separator, rest = line.partition(":")
if not separator:
continue
prerequisites.extend(word for word in rest.split() if word)
return prerequisites
def in_tree_inputs(paths, tt_root):
"""{tt-relative path: content hash} for the paths that are real files inside tt_root."""
inputs = {}
root = os.path.realpath(tt_root)
for path in paths:
full = os.path.realpath(path)
if not os.path.isfile(full):
continue
if not (full == root or full.startswith(root + os.sep)):
continue
inputs[os.path.relpath(full, root)] = hash_file(full)
return inputs
def parse_arguments(argv):
options = {
"tt_root": None,
"image": None,
"source": None,
"depfiles": [],
"extra": [],
"recipe": {},
}
index = 0
while index < len(argv):
argument = argv[index]
index += 1
if argument == "--tt-root":
options["tt_root"] = argv[index]
elif argument == "--image":
options["image"] = argv[index]
elif argument == "--source":
options["source"] = argv[index]
elif argument == "--depfile":
options["depfiles"].append(argv[index])
elif argument == "--extra-input":
options["extra"].append(argv[index])
elif argument == "--recipe":
key, _, value = argv[index].partition("=")
key = key.strip()
# REFUSED RATHER THAN DROPPED. `format_recipe()` writes RECIPE_KEYS and nothing
# else, so a key that is not on that tuple used to be accepted here and then
# vanish -- which is how a SyncFull variant came to record the shipped recipe
# and rebuild itself into a SyncHalf kernel. A build that passes a field the
# record cannot carry must stop, not be quietly recorded as something else.
if key not in RECIPE_KEYS:
raise SystemExit("record-provenance: --recipe %s is not a recorded field. "
"Add it to RECIPE_KEYS in tensix_image_provenance.py, or "
"the image would be vouched for by a recipe that does not "
"mention it." % key)
options["recipe"][key] = value.strip()
elif argument in ("-h", "--help"):
print(__doc__)
raise SystemExit(0)
else:
raise SystemExit("record-provenance: unrecognised argument %r" % argument)
index += 1
for required in ("tt_root", "image", "source"):
if not options[required]:
raise SystemExit("record-provenance: --%s is required" % required.replace("_", "-"))
return options
def main(argv):
options = parse_arguments(argv)
image_path = options["image"]
if not os.path.isfile(image_path):
raise SystemExit("record-provenance: no image at %s -- nothing to vouch for"
% image_path)
prerequisites = list(options["extra"])
prerequisites.append(options["source"])
for depfile in options["depfiles"]:
if not os.path.isfile(depfile):
raise SystemExit("record-provenance: no depfile at %s. The recorded closure "
"comes from the compiler; without it this record would hash "
"only the kernel source and miss every header it includes."
% depfile)
prerequisites.extend(parse_depfile(depfile))
inputs = in_tree_inputs(prerequisites, options["tt_root"])
if not inputs:
raise SystemExit("record-provenance: none of the %u prerequisites is inside %s -- "
"refusing to write a record that hashes nothing"
% (len(prerequisites), options["tt_root"]))
record = build_record(os.path.basename(image_path),
os.path.basename(options["source"]),
options["recipe"], inputs)
with open(image_path + ".prov", "w") as handle:
handle.write(record)
print(" provenance: %u input(s), build_id %s"
% (len(inputs), record.rsplit("build_id ", 1)[1].strip()))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))