fix(espidf): las cabeceras incluidas con comillas tambien resuelven libreria

Arduino trata igual #include "Lib.h" que #include <Lib.h> para librerias, y
los ejemplos de los propios fabricantes usan la forma con comillas: los de
M5Stack para el Cardputer abren con #include "M5Cardputer.h". Al escanear solo
la forma con angulos, ese sketch no llegaba siquiera al resolutor de librerias y
moria con 'fatal error: M5Cardputer.h: No such file', mientras el mismo sketch
con angulos compilaba sin problema. Cualquiera que pegue un ejemplo oficial se
daba de bruces con esto.

Se pasan ademas los nombres de los ficheros del propio sketch, para que un
#include "Common.h" local siga siendo una cabecera del proyecto y no se
confunda con una libreria.

test/backend/unit/test_espidf_compiler.py cubre ambas formas, el espaciado, las
cabeceras internas de IDF y la cabecera propia del proyecto.
This commit is contained in:
David Montero Crespo 2026-07-26 02:31:23 +02:00
parent 0f68037c35
commit c8ddfdddba
2 changed files with 71 additions and 7 deletions

View File

@ -1360,11 +1360,27 @@ class ESPIDFCompiler:
)
return ['user_libs_all'], header_to_comp
def _detect_external_includes(self, code: str) -> list[str]:
"""Return library header names that are likely from external libraries."""
def _detect_external_includes(
self, code: str, own_files: set[str] | None = None
) -> list[str]:
"""Return library header names that are likely from external libraries.
BOTH include forms count. Arduino treats `#include "Lib.h"` and
`#include <Lib.h>` alike for libraries, and vendors' own examples lean on
the quoted form M5Stack ships `#include "M5Cardputer.h"` in theirs. Only
scanning the angled form meant such a sketch never reached the library
resolver at all and died on `fatal error: M5Cardputer.h: No such file`,
while the very same sketch with angle brackets built fine.
`own_files` are the sketch's own file names; a quoted include naming one
of them is a project-local header, not a library.
"""
headers = []
for m in re.finditer(r'#\s*include\s*<([^>]+)>', code):
h = m.group(1)
own = own_files or set()
for m in re.finditer(r'#\s*include\s*(?:<([^>]+)>|"([^"]+)")', code):
h = m.group(1) or m.group(2)
if h in own:
continue
if h in self._BUILTIN_HEADERS:
continue
# Skip paths with / (esp-idf internal headers like freertos/FreeRTOS.h)
@ -2497,9 +2513,10 @@ class ESPIDFCompiler:
# which causes intermittent "cmake configure failed" and stale-object
# false positives when a different project/manifest compiles next.
_sketch_text = '\n'.join(f.get('content', '') for f in files)
_own_names = {Path(str(f.get('name') or '')).name for f in files}
_core_hdrs = self._core_provided_headers()
_ext_inc_token = ','.join(sorted(
h for h in set(self._detect_external_includes(_sketch_text))
h for h in set(self._detect_external_includes(_sketch_text, _own_names))
if h not in _core_hdrs
))
@ -2811,13 +2828,15 @@ class ESPIDFCompiler:
# was scanned, so libs only referenced from project headers
# never reached _resolve_library_components and the build
# died with "fatal error: ESP32Servo.h: No such file".
own_names = {PurePosixPath(str(_f.get('name') or '').replace('\\', '/')).name
for _f in files}
ext_headers_set: set[str] = set(
self._detect_external_includes(main_content)
self._detect_external_includes(main_content, own_names)
)
for _f in files:
if _f.get('name', '').endswith(('.h', '.hpp', '.ino', '.c', '.cpp')):
ext_headers_set.update(
self._detect_external_includes(_f.get('content', ''))
self._detect_external_includes(_f.get('content', ''), own_names)
)
ext_headers = list(ext_headers_set)
component_names: list[str] = []

View File

@ -455,3 +455,48 @@ if __name__ == '__main__':
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
sys.exit(0 if result.wasSuccessful() else 1)
class TestDetectExternalIncludesQuotedForm(unittest.TestCase):
"""Both include forms must reach the library resolver.
Arduino treats `#include "Lib.h"` and `#include <Lib.h>` alike for
libraries, and vendors' own examples use the quoted form — M5Stack's
Cardputer examples open with `#include "M5Cardputer.h"`. Scanning only the
angled form meant those sketches never reached the resolver and died on
`fatal error: M5Cardputer.h: No such file`, while the same sketch with angle
brackets built fine.
"""
def setUp(self):
self.comp = make_compiler()
def test_angled_include_is_detected(self):
self.assertIn('M5Cardputer.h',
self.comp._detect_external_includes('#include <M5Cardputer.h>'))
def test_quoted_include_is_detected(self):
self.assertIn('M5Cardputer.h',
self.comp._detect_external_includes('#include "M5Cardputer.h"'))
def test_both_forms_in_one_sketch(self):
code = '#include "M5Cardputer.h"\n#include <M5GFX.h>\n'
found = self.comp._detect_external_includes(code)
self.assertIn('M5Cardputer.h', found)
self.assertIn('M5GFX.h', found)
def test_a_projects_own_header_is_not_a_library(self):
code = '#include "Common.h"\n#include "M5Cardputer.h"\n'
found = self.comp._detect_external_includes(code, {'Common.h', 'sketch.ino'})
self.assertNotIn('Common.h', found)
self.assertIn('M5Cardputer.h', found)
def test_idf_internal_headers_are_still_skipped(self):
code = '#include "freertos/FreeRTOS.h"\n#include "esp_wifi.h"\n'
self.assertEqual(self.comp._detect_external_includes(code), [])
def test_spacing_variants(self):
for code in ('#include"M5Cardputer.h"', '# include "M5Cardputer.h"'):
with self.subTest(code=code):
self.assertIn('M5Cardputer.h',
self.comp._detect_external_includes(code))