fix(esp32): manifest scope resolves to the DECLARED lib, not first-match

P2.0 first cut took the first-alphabetical lib providing a header and then
checked manifest membership. When several installed libs ship the same header
(e.g. DHT118266, DHT_sensor_library, servodht11 all have DHT.h), the stray
first-match got rejected and the header was dropped even though the declared
lib provides it.

_find_manifest_library_for_header: when a manifest is supplied, pick the first
DECLARED library that provides the header. This both selects the right lib and
excludes undeclared ones. No manifest = legacy first-match.

Test strengthened with a stray same-header lib that sorts first.
This commit is contained in:
David Montero 2026-06-06 09:01:01 +02:00
parent 11fabfc67c
commit 1d643797b4
2 changed files with 43 additions and 19 deletions

View File

@ -720,6 +720,23 @@ class ESPIDFCompiler:
props_name = self._parse_library_properties(lib_root).get('name', '') props_name = self._parse_library_properties(lib_root).get('name', '')
return bool(props_name) and self._norm_lib_name(props_name) in allowed_norm return bool(props_name) and self._norm_lib_name(props_name) in allowed_norm
def _find_manifest_library_for_header(
self, header: str, libs_dir: Path, allowed_norm: set[str]
) -> Path | None:
"""Like _find_library_for_header, but returns the source root of the
first library that provides `header` AND is in the project manifest.
Returns None when no DECLARED library provides the header so a stray
same-named lib in the shared dir is never picked up."""
for lib_dir in sorted(libs_dir.iterdir()):
if not lib_dir.is_dir():
continue
for src_root in (lib_dir, lib_dir / 'src'):
if (src_root / header).exists() and self._library_in_manifest(
lib_dir, allowed_norm
):
return src_root
return None
def _resolve_library_components( def _resolve_library_components(
self, self,
ext_headers: list[str], ext_headers: list[str],
@ -801,11 +818,22 @@ class ESPIDFCompiler:
) )
continue continue
src_root = ( # P2 manifest scope. When a manifest is supplied, resolve the header
self._find_library_for_header(header, arduino_libs) # to the DECLARED library that provides it — not the first-
if arduino_libs and arduino_libs.is_dir() # alphabetical lib in the shared dir. Several installed libs may
else None # ship the same header name (e.g. DHT118266, DHT_sensor_library,
# servodht11 all have DHT.h); the legacy first-match would pick a
# stray. The manifest both picks the right lib AND excludes
# undeclared ones (another user's install, a clash). No manifest =
# legacy first-match (unchanged).
if not (arduino_libs and arduino_libs.is_dir()):
src_root = None
elif allowed_norm is not None:
src_root = self._find_manifest_library_for_header(
header, arduino_libs, allowed_norm
) )
else:
src_root = self._find_library_for_header(header, arduino_libs)
# Architecture guard. A user lib that resolves the header but # Architecture guard. A user lib that resolves the header but
# whose library.properties declares architectures= without # whose library.properties declares architectures= without
@ -820,20 +848,6 @@ class ESPIDFCompiler:
) )
src_root = None src_root = None
# P2 manifest scope. A user-installed library is merged only if it's
# in the project's declared manifest. Anything else from the shared
# dir is out of scope — drop it (the header then falls through to the
# core check, or to the "not found" path so install-on-missing can
# offer to add it to the manifest). Core/bundled libs aren't gated.
if src_root is not None and allowed_norm is not None:
_lr = src_root.parent if src_root.name == 'src' else src_root
if not self._library_in_manifest(_lr, allowed_norm):
logger.info(
f'[espidf] <{header}> resolves to "{_lr.name}" but it is not in '
f'the project library manifest — not merging (scope)'
)
src_root = None
# Tracks the "resolved to a core lib that's already compiled into # Tracks the "resolved to a core lib that's already compiled into
# the arduino-esp32 component" case, so we don't fall through to # the arduino-esp32 component" case, so we don't fall through to
# the scary "not found — build may fail" warning below for a # the scary "not found — build may fail" warning below for a

View File

@ -102,6 +102,11 @@ class TestManifestScope(unittest.TestCase):
"name=DHT sensor library\n") "name=DHT sensor library\n")
_mk(self.ulibs / "DHT_sensor_library" / "DHT.h") _mk(self.ulibs / "DHT_sensor_library" / "DHT.h")
_mk(self.ulibs / "DHT_sensor_library" / "DHT.cpp") _mk(self.ulibs / "DHT_sensor_library" / "DHT.cpp")
# A STRAY lib that also ships DHT.h and sorts BEFORE the manifest lib
# (real case: DHT118266 sorts before DHT_sensor_library). The manifest
# must still resolve DHT.h to the declared lib, not this stray.
_mk(self.ulibs / "AAAA_strayDHT" / "DHT.h")
_mk(self.ulibs / "AAAA_strayDHT" / "stray_marker.cpp")
# Out-of-manifest lib (e.g. another user's install / a clash). # Out-of-manifest lib (e.g. another user's install / a clash).
_mk(self.ulibs / "RandomOtherLib" / "Foo.h") _mk(self.ulibs / "RandomOtherLib" / "Foo.h")
_mk(self.ulibs / "RandomOtherLib" / "Foo.cpp") _mk(self.ulibs / "RandomOtherLib" / "Foo.cpp")
@ -126,6 +131,11 @@ class TestManifestScope(unittest.TestCase):
_, hdr2comp = self._resolve(["DHT.h", "Foo.h"], {"DHT sensor library"}) _, hdr2comp = self._resolve(["DHT.h", "Foo.h"], {"DHT sensor library"})
self.assertEqual(hdr2comp.get("DHT.h"), "user_libs_all") # declared → merged self.assertEqual(hdr2comp.get("DHT.h"), "user_libs_all") # declared → merged
self.assertNotIn("Foo.h", hdr2comp) # undeclared → dropped self.assertNotIn("Foo.h", hdr2comp) # undeclared → dropped
merged = self.out / "user_libs_all"
copied = [p.name for p in merged.rglob("*")] if merged.exists() else []
# The DECLARED DHT lib was merged, not the stray that sorts first.
self.assertIn("DHT.cpp", copied)
self.assertNotIn("stray_marker.cpp", copied)
def test_none_manifest_is_scan_all(self): def test_none_manifest_is_scan_all(self):
# No manifest → legacy behaviour: both resolve. # No manifest → legacy behaviour: both resolve.