From 45525c8a82c7ad1ee7f7e5bcfc6d27e7dde72481 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Apr 2026 04:41:17 -0500 Subject: [PATCH] [api] Make api a buildable cpp unit test target Three fixes so 'script/cpp_unit_test.py api' actually compiles instead of crashing in build setup: 1. script/build_helpers.py: when adding transitive component dependencies to the post-validation config, use {} (dict) instead of [] (list) for non-MULTI_CONF components. socket's FILTER_SOURCE_FILES (and any other code that subscripts CORE.config[component] with a string key) was crashing because socket got config = [] from setdefault. 2. esphome/components/api/api_pb2_service.cpp + the codegen in script/api_protobuf/api_protobuf.py: wrap the generated APIConnection::read_message_ definition in #ifdef USE_API. The class itself is only declared inside #ifdef USE_API in api_connection.h, so without the guard the .cpp fails to compile in any build that pulls in the api source files without setting USE_API (e.g. cpp unit tests of api dependencies). --- esphome/components/api/api_pb2_service.cpp | 2 ++ esphome/components/socket/__init__.py | 9 +-------- script/api_protobuf/api_protobuf.py | 8 +++++++- script/build_helpers.py | 9 +++++++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 6ae2a3e369..0ba2961a13 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -21,6 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) { } #endif +#ifdef USE_API void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements switch (msg_type) { @@ -706,5 +707,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui break; } } +#endif // USE_API } // namespace esphome::api diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index c7b2fec35f..abbbb0f056 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -177,14 +177,7 @@ async def to_code(config): def FILTER_SOURCE_FILES() -> list[str]: """Return list of socket implementation files that aren't selected by the user.""" - socket_config = CORE.config.get("socket") - if not isinstance(socket_config, dict): - # Config has not been validated yet (or socket isn't configured as a - # mapping for this build, e.g. a C++ unit test build that skips config - # validation). Don't filter -- all impl files are guarded by USE_* - # defines so only the selected implementation contributes code anyway. - return [] - impl = socket_config[CONF_IMPLEMENTATION] + impl = CORE.config["socket"][CONF_IMPLEMENTATION] # Build list of files to exclude based on selected implementation excluded = [] diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index a0dd26b1fe..bf672d0567 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -3577,8 +3577,13 @@ static const char *const TAG = "api.service"; # Generate read_message_ as APIConnection method (not base class) so the compiler # can devirtualize and inline the on_* handler calls within the same class. # APIConnection declares this method in api_connection.h. + # Guard with #ifdef USE_API since APIConnection itself is only defined when + # USE_API is set; without this, builds that compile this .cpp without + # USE_API (e.g. C++ unit tests for api dependencies) fail to find the + # class declaration. - out = "void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {\n" + out = "#ifdef USE_API\n" + out += "void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {\n" # Auth check block before dispatch switch out += " // Check authentication/connection requirements\n" @@ -3623,6 +3628,7 @@ static const char *const TAG = "api.service"; out += " break;\n" out += " }\n" out += "}\n" + out += "#endif // USE_API\n" cpp += out hpp += "};\n" diff --git a/script/build_helpers.py b/script/build_helpers.py index 1cfae51fca..59dded66dd 100644 --- a/script/build_helpers.py +++ b/script/build_helpers.py @@ -324,8 +324,13 @@ def compile_and_get_binary( domain_list.append({CONF_PLATFORM: component}) # Skip "core" — it's a pseudo-component handled by the build # system, not a real loadable component (get_component returns None) - elif get_component(component_name) is not None: - config.setdefault(component_name, []) + elif (component := get_component(component_name)) is not None: + # MULTI_CONF components store their config as a list of dicts, + # everything else stores a single dict. Using the wrong shape + # breaks code paths that subscript CORE.config[component] with + # a string key (e.g. socket.FILTER_SOURCE_FILES). + default = [] if component.multi_conf else {} + config.setdefault(component_name, default) # Register platforms from the extra config (benchmark.yaml) so # USE_SENSOR, USE_LIGHT, etc. defines are emitted without needing