Skip to content

0014. Build: generated sources reproduce on any platform, checked by a separate job

Date: 2026-09-30

No platform is authoritative for regenerating the committed nanopb and board sources: measurement showed nanopb's output platform-independent (2026-09-28), and generated-sources-reproduce.yml now shows the same for the board files every week. That job is deliberately separate from the freshness check. Extracted verbatim from Tag Firmware Build Reproducibility.

Evidence: generator output does not depend on the platform

For nanopb 0.4.9.1, the macOS x86 release binary and the Linux PyPI wheel produce byte-identical output: all 24 files across the six variants tested compared equal, .pb.c and .pb.h alike, line endings included. The two runs also used different include-path spellings, so the output is not sensitive to that either.

A regeneration on a Mac can therefore be committed directly; there is no need to make Linux authoritative or to route protocol changes through a particular machine. This is what makes the CI freshness check meaningful -- had it not held, the check would have failed for everyone not building on the blessed platform.

Evidence: the board files do not depend on the machine that rendered them

This was the last open question about fmpp, and it now has a standing answer rather than a one-off measurement.

generated-sources-reproduce.yml installs the pinned generators on a Linux runner, regenerates every committed source, and compares. It passes: all five proto variants and all five boards come back byte-identical to files rendered on macOS, with a different JVM underneath fmpp and a different platform under everything.

nanopb's output had already been shown platform-independent -- the macOS release binary and the Linux wheel producing identical output across 24 files. fmpp's never had been, and it was the weaker case of the two, since a template renderer on a JVM has more ways to inherit machine state than a Python code generator does. It reproduces.

The result is worth more as a job than as a measurement. A measurement says the files matched on the day someone checked; the job says they still match every week, and says so loudly on the day they stop.

Decision: the freshness check cannot detect a divergent generator -- a second job does

CheckGeneratedSourcesFresh.cmake compares the recorded input hashes against the tree. It does not re-render the output and diff it. The difference matters for any generator whose own version is not recorded -- fmpp above all.

If a different fmpp rendered board.c differently, the developer would commit that output alongside a perfectly correct inputs.sha256 -- the inputs really did not change, only the renderer did -- and every check thereafter would pass. AUTO then seals it: no other machine regenerates, because the recorded inputs still match, so the competing output that would expose the disagreement is never produced. The failure is self-certifying and stable.

Pinning the renderer prevents this; it does not detect it. Detection is what generated-sources-reproduce.yml adds: it installs the pinned generators, regenerates every committed source, and fails if the generated code differs from what is in the tree. inputs.sha256 is excluded from that comparison, since it records the tool versions the runner had and those may legitimately differ; the generated code may not.

It is a separate workflow rather than an extension of embedded-reproducibility.yml, and deliberately so. That check was built to need nothing but CMake and a checkout, on the reasoning that a check proving the generators unnecessary must not itself require them. Detection needs the opposite -- every generator installed -- so the two cannot be the same job without destroying the property the first one demonstrates. It runs weekly and on demand rather than per pull request, for the same reason.