Context
Found while tracing #957. Distinct from #957 (which is the depfile-own-mtime false-stale, fixed separately).
The per-TU .cmdhash rebuild signature has a WRITE-vs-COMPUTE asymmetry around build_unflags:
- WRITE (after a successful compile): the flags reach
compile_source via the default trait compile_cpp/compile_c (crates/fbuild-build/src/compiler.rs:~109-125), which calls apply_compile_unflags(self.cpp_flags(), extra_flags, self.build_unflags()) before compiling. So the signature written to .cmdhash is hashed from flags with build_unflags stripped.
- COMPUTE (staleness check on rebuild):
Esp32Compiler::rebuild_signature (crates/fbuild-build/src/esp32/esp32_compiler.rs:199-220) hashes raw self.c_flags()/self.cpp_flags() with build_unflags NOT applied.
When platformio.ini sets a non-empty build_unflags that strips a real token, WRITE ≠ COMPUTE on every rebuild → the sketch/core TUs recompile persistently (not just once). NightDriverStrip's demo env has empty build_unflags, so it's latent there (which is why #951/#956 measured a clean 2.1 s no-op rebuild) — but any project using build_unflags regresses to full recompiles each build.
The same latent asymmetry exists in the other rebuild_signature overrides that don't apply unflags: the default trait impl (compiler.rs:~161), ch32v_compiler.rs:~194, renesas_compiler.rs:~229.
Fix
Apply apply_compile_unflags(base_flags, extra_flags, self.build_unflags()) inside the signature computation so WRITE and COMPUTE hash the identical post-unflag flag set. Prefer centralizing it (strip unflags inside the shared build_rebuild_signature path, or a shared helper both sides call) so every platform's rebuild_signature is covered rather than fixing per-platform. Note: shipping this shifts all .cmdhash values once → a one-time recompile on the first build after it lands (acceptable, same as any signature-format change).
Acceptance criteria
Decisions
Context
Found while tracing #957. Distinct from #957 (which is the depfile-own-mtime false-stale, fixed separately).
The per-TU
.cmdhashrebuild signature has a WRITE-vs-COMPUTE asymmetry aroundbuild_unflags:compile_sourcevia the default traitcompile_cpp/compile_c(crates/fbuild-build/src/compiler.rs:~109-125), which callsapply_compile_unflags(self.cpp_flags(), extra_flags, self.build_unflags())before compiling. So the signature written to.cmdhashis hashed from flags withbuild_unflagsstripped.Esp32Compiler::rebuild_signature(crates/fbuild-build/src/esp32/esp32_compiler.rs:199-220) hashes rawself.c_flags()/self.cpp_flags()withbuild_unflagsNOT applied.When
platformio.inisets a non-emptybuild_unflagsthat strips a real token, WRITE ≠ COMPUTE on every rebuild → the sketch/core TUs recompile persistently (not just once). NightDriverStrip'sdemoenv has emptybuild_unflags, so it's latent there (which is why #951/#956 measured a clean 2.1 s no-op rebuild) — but any project usingbuild_unflagsregresses to full recompiles each build.The same latent asymmetry exists in the other
rebuild_signatureoverrides that don't apply unflags: the default trait impl (compiler.rs:~161),ch32v_compiler.rs:~194,renesas_compiler.rs:~229.Fix
Apply
apply_compile_unflags(base_flags, extra_flags, self.build_unflags())inside the signature computation so WRITE and COMPUTE hash the identical post-unflag flag set. Prefer centralizing it (strip unflags inside the sharedbuild_rebuild_signaturepath, or a shared helper both sides call) so every platform'srebuild_signatureis covered rather than fixing per-platform. Note: shipping this shifts all.cmdhashvalues once → a one-time recompile on the first build after it lands (acceptable, same as any signature-format change).Acceptance criteria
build_unflags = <token>gets a clean no-op rebuild (no recompile) after a warm build.rebuild_signaturefor a compiler with non-emptybuild_unflagsequals the signature the compile path writes.Decisions
build_unflagsuser; latent for NightDriverStrip. Part of the perf(build): no-change rebuild recompiles the entire sketch + core variant (~103s; should be seconds) #951/fix(build): stop no-change rebuilds from recompiling every TU #956/perf(build): first rebuild after a cold build recompiles once (signature drift between cold and steady state) #957 rebuild-correctness family / meta: profiling-driven build performance burndown (Docker Linux, NightDriverStrip, cold vs hot cache) #942.