From c8ddfdddbafce94783f002c81e6987a8fa2b719a Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Sun, 26 Jul 2026 02:31:23 +0200 Subject: [PATCH] fix(espidf): las cabeceras incluidas con comillas tambien resuelven libreria Arduino trata igual #include "Lib.h" que #include 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. --- backend/app/services/espidf_compiler.py | 33 +++++++++++++---- test/backend/unit/test_espidf_compiler.py | 45 +++++++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/backend/app/services/espidf_compiler.py b/backend/app/services/espidf_compiler.py index 86fbbf8c..f9a0d94e 100644 --- a/backend/app/services/espidf_compiler.py +++ b/backend/app/services/espidf_compiler.py @@ -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 ` 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] = [] diff --git a/test/backend/unit/test_espidf_compiler.py b/test/backend/unit/test_espidf_compiler.py index d286725e..7f322221 100644 --- a/test/backend/unit/test_espidf_compiler.py +++ b/test/backend/unit/test_espidf_compiler.py @@ -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 ` 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 ')) + + 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 \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))