STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
reproducible_build.py
Go to the documentation of this file.
1"""Make the built image independent of where it was built.
2
3PlatformIO extra script. Wire it in with, in a project's platformio.ini:
4
5 extra_scripts = pre:../../tools/reproducible_build.py
6
7Why this exists
8---------------
9Every board reports a SHA-256 of its own firmware in its heartbeat, and CI keeps
10a registry mapping that digest back to the commit that produced it (see
11firmware/tools/which_firmware.py). That only works if the same source produces
12the same bytes on every machine.
13
14It did not. `ESP_ERROR_CHECK`, `assert` and the ESP log macros all expand
15`__FILE__`, and the compiler sees absolute paths, so the image carried strings
16like
17
18 /home/aidan/.platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-spi.c
19 /home/aidan/STAR-daq/firmware/Hotfire_Code/common/SensorHotfireCore.h
20
21baked into the hashed bytes -- the username, the home directory and the checkout
22path. The same commit built by two people, or by CI, produced three different
23digests, so the digest identified the machine rather than the firmware.
24
25`-ffile-prefix-map=OLD=NEW` rewrites those paths at compile time (it covers
26`__FILE__` and the debug info both). GCC compares OLD as a literal prefix of the
27path it has already normalised, so the mapping has to be an absolute, canonical
28path -- which is why this is a script and not a line of `build_flags`: the header
29that leaks sits at `-I../common`, outside `$PROJECT_DIR`, and no interpolation
30available in platformio.ini can name its directory without a `..` that then
31fails to match.
32
33Keep the replacement names stable. Changing `/star` or `/pio` changes every
34digest in the registry.
35"""
36
37from pathlib import Path
38
39Import("env") # noqa: F821 (injected by PlatformIO/SCons)
40
41# This script's own path is not available: SCons runs it through exec(), so
42# `__file__` is undefined. Find the repo root by walking up from the project
43# until we see this very file, which works at any nesting depth and needs no git.
44_MARKER = Path("firmware/tools/reproducible_build.py")
45
46
47def _find_repo_root(start):
48 for candidate in [start, *start.parents]:
49 if (candidate / _MARKER).is_file():
50 return candidate
51 return None
52
53
54REPO_ROOT = _find_repo_root(Path(env.subst("$PROJECT_DIR")).resolve()) # noqa: F821
55
56# General prefix first, most specific last: the packages dir normally sits inside
57# the core dir, and when two mappings both match GCC applies the LAST one (checked
58# against GCC 8.4.0, the esp32s3 toolchain -- with the order reversed the packages
59# paths came out under the core dir's replacement).
60_MAPPINGS = [
61 (env.subst("$PROJECT_CORE_DIR"), "/piocore"), # noqa: F821
62 (env.subst("$PROJECT_PACKAGES_DIR"), "/pio"), # noqa: F821
63 (str(REPO_ROOT) if REPO_ROOT else "", "/star"),
64]
65
66flags = []
67seen = set()
68
69
70def _add(old, new):
71 if not old:
72 return
73 old = str(Path(old).resolve())
74 if old in seen:
75 return
76 seen.add(old)
77 flags.append(f"-ffile-prefix-map={old}={new}")
78
79
80for _old, _new in _MAPPINGS:
81 _add(_old, _new)
82
83# Strip the @version suffix off each package directory.
84#
85# Mapping the packages dir alone is not enough: PlatformIO names a package
86# directory `<name>` when it holds one version of it and `<name>@<version>`
87# when it holds more than one, and that name lands in __FILE__. So a clean
88# store compiled the Arduino core as
89# /pio/framework-arduinoespressif32/cores/esp32/esp32-hal-spi.c
90# while a store that had ever seen a second espressif32 got
91# /pio/framework-arduinoespressif32@4.20017.260907+sha.dcc1105b/cores/...
92# -- nine such strings, ~33 characters longer each, which moved the image by
93# 256 bytes and gave that machine a different digest from CI for the same
94# commit. These mappings come last because GCC applies the last mapping that
95# matches (checked on GCC 8.4.0), so they win over the packages-dir mapping.
96_packages = env.subst("$PROJECT_PACKAGES_DIR") # noqa: F821
97if _packages:
98 _pkgdir = Path(_packages)
99 if _pkgdir.is_dir():
100 for _child in sorted(_pkgdir.iterdir()):
101 if _child.is_dir() and "@" in _child.name:
102 _add(_child, "/pio/" + _child.name.split("@", 1)[0])
103
104# CCFLAGS rather than CFLAGS/CXXFLAGS: it reaches C and C++ alike, and appending
105# to `env` (not `projenv`) in a pre: script reaches the Arduino core and the
106# libraries too -- which is the point, since 9 of the 10 leaked paths were the
107# framework's own sources, not ours.
108env.Append(CCFLAGS=flags) # noqa: F821
109
110if REPO_ROOT is None:
111 # Do not fail the build -- a leaking image still runs fine, it just cannot be
112 # named by the registry. CI's reproducibility gate is what must go red here.
113 print(
114 "[reproducible_build] WARNING: could not locate the repo root from "
115 f"{env.subst('$PROJECT_DIR')}; this image will carry absolute source " # noqa: F821
116 "paths and its digest will not match CI's"
117 )
118print("[reproducible_build] " + " ".join(flags))