fix(esp32): wipe persistent build/ when the resolved library set changes

The persistent per-target build/ caches ESP-IDF's cmake configuration, ninja's
incremental graph and ccache-backed objects, all assuming a stable component
set. When consecutive compiles on the same dir have a DIFFERENT resolved
user_libs set (a different project/user, or a different library manifest) that
cache is inconsistent and produces two real failures:

  - cmake reconfigure intermittently fails ('cmake configure failed') even
    though each manifest compiles fine on a clean dir;
  - ninja/ccache reuse a previous compile's objects/headers, letting a
    now-absent library slip through as a false-positive success against a lib
    the current sketch/manifest no longer includes.

Fingerprint the materialized user_libs/ (sorted relative paths + sizes) and,
when it changes vs the last compile on this dir, wipe build/ to force a clean
configure. ccache (enabled) refills the objects so the rebuild stays cheap.
No-op on the ephemeral path and the first compile. Fixes both symptoms; will be
superseded by the fully ephemeral per-compile workspace (P1).
This commit is contained in:
David Montero 2026-06-06 20:32:26 +02:00
parent c671c9b21c
commit 161deb93e2
1 changed files with 49 additions and 0 deletions

View File

@ -779,6 +779,24 @@ class ESPIDFCompiler:
out[h] = cands out[h] = cands
return out return out
@staticmethod
def _fingerprint_dir(d: Path) -> str:
"""Stable fingerprint of a directory's file set: sorted relative paths
+ sizes. Captures libraries added / removed / version-changed without
reading file contents. Empty / missing dir -> a stable constant."""
h = hashlib.sha256()
if d.is_dir():
for f in sorted(d.rglob('*')):
if f.is_file():
h.update(f.relative_to(d).as_posix().encode())
h.update(b'\0')
try:
h.update(str(f.stat().st_size).encode())
except OSError:
pass
h.update(b'\0')
return h.hexdigest()
def _resolve_library_components( def _resolve_library_components(
self, self,
ext_headers: list[str], ext_headers: list[str],
@ -1945,6 +1963,37 @@ class ESPIDFCompiler:
if main_cpp.exists(): if main_cpp.exists():
main_cpp.unlink() main_cpp.unlink()
# ── Persistent-build-dir staleness guard ─────────────────────────
# The persistent build/ caches ESP-IDF's cmake configuration, ninja's
# incremental graph and (via ccache) compiled objects, all assuming a
# STABLE component set. When the resolved user_libs set changes between
# consecutive compiles on this dir (a different project/user, or a
# different library manifest) that cache is inconsistent: cmake
# reconfigure can fail ("cmake configure failed"), or ninja/ccache can
# reuse a previous compile's objects/headers and let a now-absent
# library slip through — a false-positive success against a lib the
# current sketch/manifest no longer includes. Force a clean configure
# by wiping build/ whenever the user_libs fingerprint changes. ccache
# (enabled) refills the objects so the rebuild stays cheap. No-op on
# the ephemeral path (fresh dir, no build/) and the first compile.
if _USE_PERSISTENT_DIR:
ul_fp = self._fingerprint_dir(project_dir / 'user_libs')
fp_sentinel = project_dir / '.user_libs_fingerprint'
prior_fp = (
fp_sentinel.read_text(encoding='utf-8').strip()
if fp_sentinel.exists() else ''
)
if ul_fp != prior_fp:
_bd = project_dir / 'build'
if _bd.exists():
logger.info(
'[espidf] resolved library set changed (%s -> %s); '
'wiping build/ for a clean configure',
prior_fp[:12] or 'none', ul_fp[:12],
)
shutil.rmtree(_bd, ignore_errors=True)
fp_sentinel.write_text(ul_fp, encoding='utf-8')
# Build using cmake + ninja (more portable than idf.py on Windows) # Build using cmake + ninja (more portable than idf.py on Windows)
build_dir = project_dir / 'build' build_dir = project_dir / 'build'
build_dir.mkdir(exist_ok=True) build_dir.mkdir(exist_ok=True)