From ba473bf529fa8ca4017e2d574a8deab96cad127e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Wed, 8 Jul 2026 14:22:26 +0200 Subject: [PATCH] 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 --- bin/collect_sizes.py | 27 ++++++++++++------ bin/platformio-custom.py | 61 +++++++++++++++++++++++++++++++--------- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/bin/collect_sizes.py b/bin/collect_sizes.py index 5b780ad02..ad40a723f 100644 --- a/bin/collect_sizes.py +++ b/bin/collect_sizes.py @@ -5,10 +5,13 @@ Output schema (consumed by bin/size_report.py): {"": {"flash_bytes": , "ram_bytes": }} -flash_bytes is the size of the main firmware image (.bin). ram_bytes is the -static RAM footprint (.data + .bss) emitted into the manifest by -bin/platformio-custom.py; it is omitted for manifests that predate it, and -size_report.py renders those as "n/a". +flash_bytes is the size of the main firmware image (.bin); for targets whose +packaged artifacts include no raw .bin (e.g. nRF52) it falls back to the +flash_bytes value (ELF text + data) emitted into the manifest by +bin/platformio-custom.py. ram_bytes is the static RAM footprint (.data + .bss) +emitted into the manifest by bin/platformio-custom.py; either metric is +omitted for manifests that predate it, and size_report.py renders those as +"n/a". """ import json @@ -42,11 +45,19 @@ def collect_sizes(manifest_dir): ): bin_size = entry["bytes"] break + # Fallback: flash footprint emitted into the manifest (ELF text + data), + # for targets that package no raw .bin (e.g. nRF52 hex/uf2/DFU zip) + if bin_size is None: + flash_bytes = data.get("flash_bytes") + if isinstance(flash_bytes, int) and not isinstance(flash_bytes, bool): + bin_size = flash_bytes + entry = {} if bin_size is not None: - entry = {"flash_bytes": bin_size} - ram_bytes = data.get("ram_bytes") - if isinstance(ram_bytes, int) and not isinstance(ram_bytes, bool): - entry["ram_bytes"] = ram_bytes + entry["flash_bytes"] = bin_size + ram_bytes = data.get("ram_bytes") + if isinstance(ram_bytes, int) and not isinstance(ram_bytes, bool): + entry["ram_bytes"] = ram_bytes + if entry: sizes[board] = entry return sizes diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index 61fa03610..cb32f18a4 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -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