Foundry/tt/tools/regen_tensix_program_header.sh

146 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 David (Corey) Herrera
#
# regen_tensix_program_header.sh -- the ONE entry point that regenerates
# foundry/tt/ft_tt_programs.h from the program directories under foundry/tt/programs/.
#
# WHY THIS EXISTS, IN ONE SENTENCE: a binary that reads its Tensix programs out of
# one absolute programs directory runs on exactly one machine, and this is the command that
# puts them inside the binary instead.
#
# It is the program-level twin of regen_tensix_kernel_header.sh, and it inherits that
# script's discipline for the same reasons:
#
# * IT REFUSES A STALE DIRECTORY rather than embedding it, BY CONTENT. Every image
# carries a `.prov` record hashing the files its compile opened and the recipe that
# reached code generation, and the emitter re-hashes them against the tree before it
# writes anything (tools/tensix_image_provenance.py). The `.src` sidecar's mtime
# comparison is still run and is still useful for a source that has VANISHED, but it is
# not the staleness check any more: an `explode` resets every mtime, which is how 21
# directories reported `current` with two of them provably stale. A stale directory on
# disk costs a re-run. A stale directory BAKED INTO THE BINARY survives a rebuild.
# * IT WRITES NOTHING WHEN IT REFUSES. The emitter builds the whole header in memory
# before it opens the output, so a refusal cannot leave a half-written header behind.
# * IT SAYS WHAT MOVED, ALWAYS, before anything is written.
#
# WHAT IT DOES **NOT** DO, so the guarantee is not overread: it does not build the programs.
# `build_tensix_program.sh` does that, per program, with its own CTARGS and CB table — there
# is no single recipe that reconstructs all of them, and inventing one here would be a
# second source of truth for how each program is built. This script embeds what is there and
# refuses what it cannot vouch for.
#
# Usage:
# tools/regen_tensix_program_header.sh [program-dir ...]
#
# With no arguments it takes every directory under $PROGRAMS that holds a brisc.elf.
#
# PROGRAMS where the program directories are (default ../programs)
# KERNELS the kernel source tree the sidecars name (default ../kernels)
# TOOLCHAIN recorded in the blob for logs (default 7.68.0)
# CHECK=1 diff against the committed header only; write nothing
#
# Runs on the card host: the program directories and the SFPI builds that produced them are
# there. THIS HAS NEVER BEEN RUN FOR REAL -- it was written on the Mac and gated against
# synthetic fixtures (tests/Makefile.programblob). The first real run needs a bank session
# and should be followed by `verify_tensix_program_blob ../programs`.
#
set -u
HERE=$(cd -- "$(dirname -- "$0")" && pwd)
TT=$(cd -- "$HERE/.." && pwd)
PROGRAMS=${PROGRAMS:-$TT/programs}
KERNELS=${KERNELS:-$TT/kernels}
TOOLCHAIN=${TOOLCHAIN:-7.68.0}
CHECK=${CHECK:-0}
HEADER="$TT/ft_tt_programs.h"
STAGED=$(mktemp "${TMPDIR:-/tmp}/ft_tt_programs.XXXXXX.h")
say() { printf '\n=== %s\n' "$*"; }
die() { printf '\nregen-programs: FAILED -- %s\n' "$*" >&2; rm -f "$STAGED"; exit 1; }
[ -d "$KERNELS" ] || die "no kernel source tree at $KERNELS -- nothing can be checked
against it, and a program directory nobody can check must not be embedded"
# ---------------------------------------------------------------------------
# 1. Which directories. Named on the command line, or every one that looks like a
# build_tensix_program.sh output.
# ---------------------------------------------------------------------------
DIRS=("$@")
if [ ${#DIRS[@]} -eq 0 ]; then
[ -d "$PROGRAMS" ] || die "no program directory tree at $PROGRAMS"
while IFS= read -r elf; do
DIRS+=("$(dirname "$elf")")
done < <(find "$PROGRAMS" -mindepth 2 -maxdepth 2 -name brisc.elf | sort)
fi
[ ${#DIRS[@]} -gt 0 ] || die "no program directories found under $PROGRAMS. An EMPTY blob is
a real state, but it is not something a glob should be able to produce by accident:
ask for it explicitly with
python3 $HERE/emit_tensix_program_header.py -o $HEADER --allow-empty"
say "embedding ${#DIRS[@]} program director(y|ies)"
for dir in "${DIRS[@]}"; do
printf ' %s\n' "$dir"
done
# ---------------------------------------------------------------------------
# 2. Emit. The emitter runs check_tensix_program_sources.sh itself and refuses on a
# stale sidecar, a missing firmware image, a duplicate name or an unloadable ELF.
# ---------------------------------------------------------------------------
say "emitting (the stale-source check runs inside this step)"
ARGS=()
for dir in "${DIRS[@]}"; do
ARGS+=(--program "$dir")
done
python3 "$HERE/emit_tensix_program_header.py" -o "$STAGED" --recorded-output "$HEADER" \
--kernels "$KERNELS" --toolchain "$TOOLCHAIN" \
--recipe "built by build_tensix_program.sh; see the log beside the directory" \
"${ARGS[@]}" || die "the emitter refused. Nothing was written."
# ---------------------------------------------------------------------------
# 3. The firmware sets, reported ALWAYS. Programs built from one tree share one set; more
# than one set is legal and is carried correctly, but it means a run that arms two
# families from different sets will be refused at load, which is worth knowing here
# rather than on the card.
# ---------------------------------------------------------------------------
say "firmware sets in this blob"
grep -c 'static const ft_tt_program_blob_firmware' "$STAGED" \
| awk '{printf " %s distinct firmware set(s)\n", $1}'
grep 'first seen in' "$STAGED" | sed 's/^/ /'
if [ "$(grep -c 'static const ft_tt_program_blob_firmware' "$STAGED")" -gt 1 ]; then
printf ' NOTE: these programs were NOT all built against one firmware. A kernel is
linked at its own firmware'"'"'s __fw_export_text_end, so any run that boots one
set and loads another set'"'"'s kernels is a wedged core -- and is refused. If that
was not intended, rebuild them together.\n'
fi
# ---------------------------------------------------------------------------
# 4. Against the committed header, before anything is written.
# ---------------------------------------------------------------------------
say "against the committed header"
if cmp -s "$STAGED" "$HEADER"; then
printf ' IDENTICAL: these directories reproduce the committed ft_tt_programs.h\n'
header_changed=0
else
printf ' DIFFERS. %s changed lines (the payload is one line per 12 bytes):\n' \
"$(diff "$HEADER" "$STAGED" 2>/dev/null | grep -c '^[<>]')"
diff "$HEADER" "$STAGED" 2>/dev/null | grep '^[<>]' | grep -v '0x' | head -30
header_changed=1
fi
if [ "$CHECK" != "0" ]; then
say "CHECK=1: nothing written"
rm -f "$STAGED"
exit $header_changed
fi
cp "$STAGED" "$HEADER" || die "cannot write $HEADER"
rm -f "$STAGED"
printf '\nregen-programs: wrote %s\n' "$HEADER"
printf 'regen-programs: NOW PROVE IT, by comparison and not by construction:\n'
printf ' make -f tools/Makefile verify-programs\n'
printf ' tools/build/verify_tensix_program_blob %s\n' "$PROGRAMS"
exit 0