size gate: emit flash_bytes from ELF for targets without a packaged .bin (#10940)

* size gate: emit flash_bytes from ELF for targets without a packaged .bin

nRF52 builds package hex/uf2/DFU zip but no raw .bin, so collect_sizes.py
dropped budgeted envs like rak4631 entirely and the size-budget-gate failed
closed. Emit ELF text+data as flash_bytes in the manifest and use it as the
fallback flash measurement.

* Factor shared size-tool invocation into run_size_tool helper
This commit is contained in:
Thomas Göttgens
2026-07-08 07:22:26 -05:00
committed by GitHub
co-authored by GitHub
parent 8d20606203
commit ba473bf529
2 changed files with 66 additions and 22 deletions
+47 -14
View File
@@ -45,6 +45,27 @@ def infer_architecture(board_cfg):
return "stm32"
return None
def run_size_tool(env, flag, purpose):
"""Run the toolchain size tool against the built ELF and return its output.
Shared plumbing for compute_ram_bytes / compute_flash_bytes. Returns None
when the ELF is missing or the tool fails; the manifest then simply omits
the metric and downstream size reports show "n/a".
"""
elf = env.File(env.subst("$BUILD_DIR/${PROGNAME}.elf"))
if not elf.exists():
return None
size_tool = env.subst("$SIZETOOL") or "size"
try:
return subprocess.check_output(
[size_tool, flag, elf.get_abspath()],
env=env["ENV"],
universal_newlines=True,
)
except Exception as exc:
print(f"mtjson: skipping {purpose} ({size_tool} failed: {exc})")
return None
def compute_ram_bytes(env):
"""Static RAM usage (.data + .bss) of the ELF, via the toolchain size tool.
@@ -54,18 +75,8 @@ def compute_ram_bytes(env):
Returns None when the value cannot be determined; the manifest then simply
omits ram_bytes and downstream size reports show "n/a".
"""
elf = env.File(env.subst("$BUILD_DIR/${PROGNAME}.elf"))
if not elf.exists():
return None
size_tool = env.subst("$SIZETOOL") or "size"
try:
output = subprocess.check_output(
[size_tool, "-A", elf.get_abspath()],
env=env["ENV"],
universal_newlines=True,
)
except Exception as exc:
print(f"mtjson: skipping ram_bytes ({size_tool} failed: {exc})")
output = run_size_tool(env, "-A", "ram_bytes")
if output is None:
return None
ram = 0
found = False
@@ -88,6 +99,24 @@ def compute_ram_bytes(env):
continue
return ram if found else None
def compute_flash_bytes(env):
"""Flash footprint (text + data) of the ELF via the toolchain size tool.
Approximates the flashed app image for targets whose packaged artifacts do
not include a raw .bin (e.g. nRF52: hex/uf2/DFU zip only); consumed by
bin/collect_sizes.py as a fallback flash measurement for the size report
and the bin/ram_budgets.json budget gate. Returns None when the value
cannot be determined; the manifest then simply omits flash_bytes.
"""
output = run_size_tool(env, "-B", "flash_bytes")
if output is None:
return None
for line in output.splitlines():
parts = line.split()
if len(parts) >= 3 and parts[0].isdigit() and parts[1].isdigit():
return int(parts[0]) + int(parts[1])
return None
def manifest_gather(source, target, env):
global manifest_ran
if manifest_ran:
@@ -141,9 +170,9 @@ def manifest_gather(source, target, env):
d["part_name"] = partition_map[p]
out.append(d)
print(d)
manifest_write(out, env, compute_ram_bytes(env))
manifest_write(out, env, compute_ram_bytes(env), compute_flash_bytes(env))
def manifest_write(files, env, ram_bytes=None):
def manifest_write(files, env, ram_bytes=None, flash_bytes=None):
# Defensive: also skip manifest writing if we cannot determine architecture
def get_project_option(name):
try:
@@ -184,6 +213,10 @@ def manifest_write(files, env, ram_bytes=None):
# the CI size report and the bin/ram_budgets.json budget gate.
if ram_bytes is not None:
manifest["ram_bytes"] = ram_bytes
# Flash footprint (text + data); fallback flash measurement for targets
# whose packaged artifacts include no raw .bin (e.g. nRF52).
if flash_bytes is not None:
manifest["flash_bytes"] = flash_bytes
# Get partition table (generated in esp32_pre.py) if it exists
if env.get("custom_mtjson_part"):
# custom_mtjson_part is a JSON string, convert it back to a dict