fix(espidf): a header-only library broke every ESP32 build
Installing ArduinoJson made the project stop compiling, with an error that
never mentioned ArduinoJson:
CMake Error at user_libs/user_libs_all/CMakeLists.txt:15
(target_compile_definitions):
target_compile_definitions may only set INTERFACE properties on
INTERFACE targets
-- Configuring incomplete, errors occurred!
ArduinoJson ships headers only. With no sources, idf_component_register()
builds the merged user-libs component as an INTERFACE library, and PRIVATE
scope on an INTERFACE target is a hard CMake error - so the configure step
died before a single file was compiled. The user saw only a failing build
and reported it as "ArduinoJson can't be found even after it has been
installed" (#124).
Those definitions map alloca/memcpy_P/memcmp_P for library C sources that
use them undeclared; with no sources there is nothing to define, so they are
emitted only when the component actually compiles something. A library that
carries its own .cpp is unchanged - which is why LiquidCrystal_I2C (#257)
worked and hid this.
This commit is contained in:
parent
6877099e78
commit
0d9a26d951
|
|
@ -1491,6 +1491,32 @@ class ESPIDFCompiler:
|
|||
extra_requires += f' {_comp}'
|
||||
break
|
||||
|
||||
# LovyanGFX (the engine inside M5GFX) uses alloca()/memcpy_P/memcmp_P
|
||||
# without a prior declaration — its C utility files (lgfx_pngle.c, qoi …)
|
||||
# call the PROGMEM aliases but never include pgmspace.h, so the flat
|
||||
# IDF-component merge trips over an implicit declaration. Map them to
|
||||
# always-available equivalents (identical to pgmspace.h, so redefinition
|
||||
# is benign) so C++ graphics libs compile as a merged component.
|
||||
#
|
||||
# ONLY when this component actually compiles something. With no SRCS,
|
||||
# idf_component_register() builds an INTERFACE library, and PRIVATE (or
|
||||
# PUBLIC) on an INTERFACE target is a hard CMake error:
|
||||
# target_compile_definitions may only set INTERFACE properties on
|
||||
# INTERFACE targets
|
||||
# which killed the whole configure step before a single file was
|
||||
# compiled. That is the state a HEADER-ONLY library leaves us in —
|
||||
# ArduinoJson is the common one — and it is why installing it made every
|
||||
# ESP32 build fail while a library with its own .cpp (LiquidCrystal_I2C)
|
||||
# was fine. The definitions exist for this component's own C sources, so
|
||||
# with no sources there is nothing to define.
|
||||
defs_block = ''
|
||||
if cpp_files:
|
||||
defs_block = (
|
||||
'# PROGMEM aliases for library C sources that use them undeclared.\n'
|
||||
'target_compile_definitions(${COMPONENT_LIB} PRIVATE'
|
||||
' alloca=__builtin_alloca memcpy_P=memcpy memcmp_P=memcmp)\n'
|
||||
)
|
||||
|
||||
cmake_content = (
|
||||
'# Auto-generated by Velxio — all user libraries merged into one component.\n'
|
||||
'# Directory structure preserved for libraries like ArduinoJson with src/ layout.\n'
|
||||
|
|
@ -1499,15 +1525,7 @@ class ESPIDFCompiler:
|
|||
f' {include_dirs_line}\n'
|
||||
f' REQUIRES {arduino_comp_name}{extra_requires}\n'
|
||||
')\n'
|
||||
'# LovyanGFX (the engine inside M5GFX) uses alloca()/memcpy_P/\n'
|
||||
'# memcmp_P without a prior declaration — its C utility files\n'
|
||||
'# (lgfx_pngle.c, qoi …) call the PROGMEM aliases but never include\n'
|
||||
'# pgmspace.h, so the flat IDF-component merge trips over an implicit\n'
|
||||
'# declaration. Map them to always-available equivalents (identical\n'
|
||||
'# to pgmspace.h, so redefinition is benign) so C++ graphics libs\n'
|
||||
'# compile as a merged component.\n'
|
||||
'target_compile_definitions(${COMPONENT_LIB} PRIVATE'
|
||||
' alloca=__builtin_alloca memcpy_P=memcpy memcmp_P=memcmp)\n'
|
||||
f'{defs_block}'
|
||||
)
|
||||
(comp_dir / 'CMakeLists.txt').write_text(cmake_content, encoding='utf-8')
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -500,3 +500,84 @@ class TestDetectExternalIncludesQuotedForm(unittest.TestCase):
|
|||
with self.subTest(code=code):
|
||||
self.assertIn('M5Cardputer.h',
|
||||
self.comp._detect_external_includes(code))
|
||||
|
||||
|
||||
# ── Test: header-only libraries (ArduinoJson) ────────────────────────────────
|
||||
|
||||
class TestHeaderOnlyLibrary(unittest.TestCase):
|
||||
"""
|
||||
A library with no .c/.cpp of its own must still produce a CMakeLists.txt
|
||||
ESP-IDF can configure.
|
||||
|
||||
With no SRCS, idf_component_register() builds the component as an INTERFACE
|
||||
library, and target_compile_definitions(... PRIVATE ...) on an INTERFACE
|
||||
target is a hard CMake error:
|
||||
|
||||
target_compile_definitions may only set INTERFACE properties on
|
||||
INTERFACE targets
|
||||
|
||||
That killed the configure step outright, so installing ArduinoJson broke
|
||||
every ESP32 build in the project — reported as "ArduinoJson can't be found
|
||||
even after it has been installed" (issue #124), because the user only ever
|
||||
saw the build fail. A library carrying its own .cpp was unaffected, which is
|
||||
why LiquidCrystal_I2C (#257) looked fine.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.comp = make_compiler()
|
||||
self.tmp = tempfile.mkdtemp()
|
||||
self.arduino_libs = Path(self.tmp) / 'arduino_libs'
|
||||
self.esp32_libs = Path(self.tmp) / 'esp32_libs'
|
||||
self.user_libs = Path(self.tmp) / 'user_libs'
|
||||
for d in (self.arduino_libs, self.esp32_libs, self.user_libs):
|
||||
d.mkdir()
|
||||
# ArduinoJson ships headers only, under src/.
|
||||
make_library(self.arduino_libs, 'ArduinoJson',
|
||||
headers=['ArduinoJson.h'], sources=[], use_src_subdir=True)
|
||||
# A conventional library, for the contrast case.
|
||||
make_library(self.arduino_libs, 'LiquidCrystal_I2C',
|
||||
headers=['LiquidCrystal_I2C.h'],
|
||||
sources=['LiquidCrystal_I2C.cpp'])
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp)
|
||||
|
||||
def _cmake_for(self, headers: list[str]) -> str:
|
||||
names, _ = self.comp._resolve_library_components(
|
||||
headers, self.arduino_libs, self.esp32_libs, 'arduino-esp32', self.user_libs
|
||||
)
|
||||
self.assertEqual(names, ['user_libs_all'])
|
||||
return (self.user_libs / 'user_libs_all' / 'CMakeLists.txt').read_text(encoding='utf-8')
|
||||
|
||||
def test_header_only_library_emits_no_private_scope(self):
|
||||
cmake = self._cmake_for(['ArduinoJson.h'])
|
||||
self.assertNotIn('SRCS', cmake, 'header-only component must register with no SRCS')
|
||||
self.assertNotIn(
|
||||
'PRIVATE', cmake,
|
||||
'PRIVATE on a source-less (INTERFACE) component is a CMake configure error',
|
||||
)
|
||||
|
||||
def test_header_only_library_still_exposes_its_header(self):
|
||||
cmake = self._cmake_for(['ArduinoJson.h'])
|
||||
self.assertIn('idf_component_register', cmake)
|
||||
# The library's src/ layout is preserved, so the header lands at
|
||||
# src/ArduinoJson.h and "src" has to be on the include path for the
|
||||
# sketch's #include <ArduinoJson.h> to resolve.
|
||||
merged = self.user_libs / 'user_libs_all'
|
||||
self.assertTrue(
|
||||
(merged / 'src' / 'ArduinoJson.h').exists(),
|
||||
'ArduinoJson.h not copied — the sketch would fail on "No such file"',
|
||||
)
|
||||
self.assertIn('INCLUDE_DIRS "." "src"', cmake)
|
||||
|
||||
def test_library_with_sources_keeps_the_progmem_definitions(self):
|
||||
cmake = self._cmake_for(['LiquidCrystal_I2C.h'])
|
||||
self.assertIn('SRCS', cmake)
|
||||
self.assertIn('target_compile_definitions(${COMPONENT_LIB} PRIVATE', cmake)
|
||||
self.assertIn('memcpy_P=memcpy', cmake)
|
||||
|
||||
def test_mixed_libraries_keep_the_definitions(self):
|
||||
"""One library with sources is enough to make the component compile code."""
|
||||
cmake = self._cmake_for(['ArduinoJson.h', 'LiquidCrystal_I2C.h'])
|
||||
self.assertIn('SRCS', cmake)
|
||||
self.assertIn('PRIVATE', cmake)
|
||||
|
|
|
|||
Loading…
Reference in New Issue