From 3fd8f00e97a4e63bfd7c0c3260530b0e9ade29d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Oct 2026 07:31:41 -0500 Subject: [PATCH] [socket] Compile the lookup and client link helpers only when required (#20002) --- esphome/components/socket/__init__.py | 16 +++++++- esphome/core/defines.h | 2 + tests/components/socket/__init__.py | 3 ++ .../ipv4_resolve_test_component/__init__.py | 2 + .../__init__.py | 2 + .../socket/test_socket_source_filter.py | 38 +++++++++++++++++++ 6 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/components/socket/test_socket_source_filter.py diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index edebed5d40..a814f46e3b 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -140,6 +140,17 @@ def require_wake_loop_threadsafe() -> None: cg.add_define("USE_SOCKET_SELECT_SUPPORT") +def require_ipv4_resolve() -> None: + """Compile the shared IPv4 lookup; call from a consumer's to_code.""" + cg.add_define("USE_SOCKET_IPV4_RESOLVE") + + +def require_tcp_client_link() -> None: + """Compile the reconnecting TCP client link; call from a consumer's to_code.""" + require_ipv4_resolve() + cg.add_define("USE_SOCKET_TCP_CLIENT_LINK") + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( @@ -184,11 +195,14 @@ async def to_code(config: ConfigType) -> None: # Each implementation file is fully #ifdef'd on the define set in to_code -# for the selected implementation. +# for the selected implementation. The helper files compile only for +# consumers that called the matching require_ function. FILTER_SOURCE_FILES = filter_source_files_from_defines( { "lwip_raw_tcp_impl.cpp": "USE_SOCKET_IMPL_LWIP_TCP", "bsd_sockets_impl.cpp": "USE_SOCKET_IMPL_BSD_SOCKETS", "lwip_sockets_impl.cpp": "USE_SOCKET_IMPL_LWIP_SOCKETS", + "ipv4_resolve.cpp": "USE_SOCKET_IPV4_RESOLVE", + "tcp_client_link.cpp": "USE_SOCKET_TCP_CLIENT_LINK", } ) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9b3252fa05..88c82b0d7c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -417,6 +417,8 @@ #define USE_SENDSPIN_VISUALIZER #define USE_SENDSPIN_PORT 8928 // NOLINT #define USE_SOCKET_IMPL_BSD_SOCKETS +#define USE_SOCKET_IPV4_RESOLVE +#define USE_SOCKET_TCP_CLIENT_LINK #define USE_LWIP_FAST_SELECT #define USE_SPEAKER diff --git a/tests/components/socket/__init__.py b/tests/components/socket/__init__.py index 8a3da97e23..6c1d2b1c08 100644 --- a/tests/components/socket/__init__.py +++ b/tests/components/socket/__init__.py @@ -2,6 +2,7 @@ # shadowing the stdlib module -- no test_*.py (Python-level) tests can live here. # required for cpp unit testing. import esphome.codegen as cg +from esphome.components import socket as socket_component from tests.testing_helpers import ComponentManifestOverride @@ -12,5 +13,7 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: async def to_code_testing(config): await real_to_code(config) cg.add_define("USE_NETWORK_IPV6", True) + # The gtests link against the filtered helper files. + socket_component.require_tcp_client_link() manifest.to_code = to_code_testing diff --git a/tests/integration/fixtures/external_components/ipv4_resolve_test_component/__init__.py b/tests/integration/fixtures/external_components/ipv4_resolve_test_component/__init__.py index 3e397d6186..aee9e3ec13 100644 --- a/tests/integration/fixtures/external_components/ipv4_resolve_test_component/__init__.py +++ b/tests/integration/fixtures/external_components/ipv4_resolve_test_component/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.components import socket import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.types import ConfigType @@ -18,5 +19,6 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config: ConfigType) -> None: + socket.require_ipv4_resolve() var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/tcp_client_link_test_component/__init__.py b/tests/integration/fixtures/external_components/tcp_client_link_test_component/__init__.py index 8a7703d025..24637ae586 100644 --- a/tests/integration/fixtures/external_components/tcp_client_link_test_component/__init__.py +++ b/tests/integration/fixtures/external_components/tcp_client_link_test_component/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.components import socket from esphome.components.const import CONF_HOST import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PORT @@ -28,6 +29,7 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config: ConfigType) -> None: + socket.require_tcp_client_link() var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) cg.add(var.set_host(config[CONF_HOST])) diff --git a/tests/unit_tests/components/socket/test_socket_source_filter.py b/tests/unit_tests/components/socket/test_socket_source_filter.py new file mode 100644 index 0000000000..3967c9568d --- /dev/null +++ b/tests/unit_tests/components/socket/test_socket_source_filter.py @@ -0,0 +1,38 @@ +"""Tests for the socket component's source filtering and require functions.""" + +from unittest.mock import patch + +from esphome.components import socket +from esphome.core import Define + + +def test_helper_files_filtered_until_required() -> None: + """ipv4_resolve.cpp and tcp_client_link.cpp compile only when required.""" + with patch("esphome.config_helpers.CORE") as mock_core: + mock_core.defines = set() + filtered = socket.FILTER_SOURCE_FILES() + assert "ipv4_resolve.cpp" in filtered + assert "tcp_client_link.cpp" in filtered + + mock_core.defines = {Define("USE_SOCKET_IPV4_RESOLVE")} + filtered = socket.FILTER_SOURCE_FILES() + assert "ipv4_resolve.cpp" not in filtered + assert "tcp_client_link.cpp" in filtered + + mock_core.defines = { + Define("USE_SOCKET_IPV4_RESOLVE"), + Define("USE_SOCKET_TCP_CLIENT_LINK"), + } + filtered = socket.FILTER_SOURCE_FILES() + assert "ipv4_resolve.cpp" not in filtered + assert "tcp_client_link.cpp" not in filtered + + +def test_require_tcp_client_link_pulls_in_the_resolver() -> None: + """require_tcp_client_link() sets both defines; the link reads the resolver.""" + with patch.object(socket.cg, "add_define") as add_define: + socket.require_tcp_client_link() + assert {call.args[0] for call in add_define.call_args_list} == { + "USE_SOCKET_IPV4_RESOLVE", + "USE_SOCKET_TCP_CLIENT_LINK", + }