From 52d0f1cc682e6a77afaa699c9b2a989f2b8d20a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jan 2026 11:51:40 -1000 Subject: [PATCH] reduce esp32 compile times --- esphome/components/adc/sensor.py | 5 +- esphome/components/esp32/__init__.py | 79 +++++++++++++++++++ esphome/components/esp32/const.py | 1 + .../components/esp32_rmt_led_strip/light.py | 4 + esphome/components/esp32_touch/__init__.py | 4 + esphome/components/ethernet/__init__.py | 4 + esphome/components/http_request/__init__.py | 3 + esphome/components/i2s_audio/__init__.py | 11 ++- esphome/components/mqtt/__init__.py | 4 +- esphome/components/neopixelbus/light.py | 11 ++- .../components/remote_receiver/__init__.py | 3 + .../components/remote_transmitter/__init__.py | 3 + 12 files changed, 127 insertions(+), 5 deletions(-) diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 64dd22b0c3..ba87495e9d 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -2,7 +2,7 @@ import logging import esphome.codegen as cg from esphome.components import sensor, voltage_sampler -from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32 import get_esp32_variant, include_idf_component from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC from esphome.components.zephyr import ( zephyr_add_overlay, @@ -118,6 +118,9 @@ async def to_code(config): cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) if CORE.is_esp32: + # Re-enable ESP-IDF's ADC driver (excluded by default to save compile time) + include_idf_component("esp_adc") + if attenuation := config.get(CONF_ATTENUATION): if attenuation == "auto": cg.add(var.set_autorange(cg.global_ns.true)) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index fccf0ed09f..006bd45bce 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -53,6 +53,7 @@ from .const import ( # noqa KEY_BOARD, KEY_COMPONENTS, KEY_ESP32, + KEY_EXCLUDE_COMPONENTS, KEY_EXTRA_BUILD_FILES, KEY_FLASH_SIZE, KEY_FULL_CERT_BUNDLE, @@ -114,6 +115,41 @@ COMPILER_OPTIMIZATIONS = { "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", } +# ESP-IDF components that ESPHome never uses. +# Excluding these reduces compile time - the linker would discard them anyway. +# +# NOTE: It is fine to remove components from this list when adding new features, +# but when you do, make sure the component is still excluded via exclude_idf_component() +# when the new feature is not enabled, to keep compile times short for users who +# don't use that feature. +# +# Cannot be excluded (dependencies of required components): +# - "console": espressif/mdns unconditionally depends on it +# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain +NEVER_USED_IDF_COMPONENTS = ( + "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing + "esp_adc", # ADC driver - only needed by adc component + "esp_driver_i2s", # I2S driver - only needed by i2s_audio component + "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus + "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch + "esp_eth", # Ethernet driver - only needed by ethernet component + "esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality + "esp_http_client", # HTTP client - only needed by http_request component + "esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation + "esp_https_server", # HTTPS server - ESPHome has its own web server + "esp_lcd", # LCD controller drivers - ESPHome has its own display components + "esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API + "espcoredump", # Core dump support - ESPHome has its own debug component + "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage + "mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation + "perfmon", # Xtensa performance monitor - ESPHome has its own debug component + "protocomm", # Protocol communication for provisioning - unused by ESPHome + "spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only) + "unity", # Unit testing framework - ESPHome doesn't use IDF's testing + "wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused + "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation +) + # ESP32 (original) chip revision options # Setting minimum revision to 3.0 or higher: # - Reduces flash size by excluding workaround code for older chip bugs @@ -203,6 +239,9 @@ def set_core_data(config): ) CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} + # Initialize with default exclusions - components can call include_idf_component() + # to re-enable any they need + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(NEVER_USED_IDF_COMPONENTS) CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) @@ -328,6 +367,28 @@ def add_idf_component( } +def exclude_idf_component(name: str) -> None: + """Exclude an ESP-IDF component from the build. + + This reduces compile time by skipping components that are not needed. + The component will be passed to ESP-IDF's EXCLUDE_COMPONENTS cmake variable. + + Note: Components that are dependencies of other required components + cannot be excluded - ESP-IDF will still build them. + """ + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].add(name) + + +def include_idf_component(name: str) -> None: + """Remove an ESP-IDF component from the exclusion list. + + Call this from components that need an ESP-IDF component that is + excluded by default in NEVER_USED_IDF_COMPONENTS. This ensures the + component will be built when needed. + """ + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name) + + def add_extra_script(stage: str, filename: str, path: Path): """Add an extra script to the project.""" key = f"{stage}:{filename}" @@ -982,6 +1043,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None: add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_exclude_components() -> None: + """Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions.""" + if KEY_ESP32 not in CORE.data: + return + excluded = CORE.data[KEY_ESP32].get(KEY_EXCLUDE_COMPONENTS) + if excluded: + exclude_list = ";".join(sorted(excluded)) + cg.add_platformio_option( + "board_build.cmake_extra_args", f"-DEXCLUDE_COMPONENTS={exclude_list}" + ) + + @coroutine_with_priority(CoroPriority.FINAL) async def _add_yaml_idf_components(components: list[ConfigType]): """Add IDF components from YAML config with final priority to override code-added components.""" @@ -1324,6 +1398,11 @@ async def to_code(config): if conf[CONF_COMPONENTS]: CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS]) + # Write EXCLUDE_COMPONENTS at FINAL priority after all components have had + # a chance to call include_idf_component() to re-enable components they need. + # Default exclusions are added in set_core_data() during config validation. + CORE.add_job(_write_exclude_components) + APP_PARTITION_SIZES = { "2MB": 0x0C0000, # 768 KB diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index 9f8165818b..db3eddebd5 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -6,6 +6,7 @@ KEY_FLASH_SIZE = "flash_size" KEY_VARIANT = "variant" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_COMPONENTS = "components" +KEY_EXCLUDE_COMPONENTS = "exclude_components" KEY_REPO = "repo" KEY_REF = "ref" KEY_REFRESH = "refresh" diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 3be3c758f1..4eed459bd1 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -5,6 +5,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import esp32, light from esphome.components.const import CONF_USE_PSRAM +from esphome.components.esp32 import include_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -129,6 +130,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) + include_idf_component("esp_driver_rmt") + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(var, config) await cg.register_component(var, config) diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index c54ed8b9ea..f36a1171b7 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -6,6 +6,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S3, get_esp32_variant, gpio, + include_idf_component, ) import esphome.config_validation as cv from esphome.const import ( @@ -266,6 +267,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + # Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time) + include_idf_component("esp_driver_touch_sens") + touch = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(touch, config) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 1f2fe61fe1..aaba12b6d1 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -14,6 +14,7 @@ from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, get_esp32_variant, + include_idf_component, ) from esphome.components.network import ip_address_literal from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface @@ -419,6 +420,9 @@ async def to_code(config): # Also disable WiFi/BT coexistence since WiFi is disabled add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) + # Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time) + include_idf_component("esp_eth") + if config[CONF_TYPE] == "LAN8670": # Add LAN867x 10BASE-T1S PHY support component add_idf_component(name="espressif/lan867x", ref="2.0.0") diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 07bc758037..eeed2dd411 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -155,6 +155,9 @@ async def to_code(config): cg.add(var.set_watchdog_timeout(timeout_ms)) if CORE.is_esp32: + # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) + esp32.include_idf_component("esp_http_client") + cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL])) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index d3128c5f4c..df7c464364 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,6 +1,11 @@ from esphome import pins import esphome.codegen as cg from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + get_esp32_variant, + include_idf_component, +) +from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32C5, @@ -10,8 +15,6 @@ from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, - add_idf_sdkconfig_option, - get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE @@ -272,6 +275,10 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + # Re-enable ESP-IDF's I2S driver (excluded by default to save compile time) + include_idf_component("esp_driver_i2s") + if use_legacy(): cg.add_define("USE_I2S_LEGACY") diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index f53df5564c..db49a7c6c3 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -4,7 +4,7 @@ from esphome import automation from esphome.automation import Condition import esphome.codegen as cg from esphome.components import logger, socket -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32 import add_idf_sdkconfig_option, include_idf_component from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -360,6 +360,8 @@ async def to_code(config): # This enables low-latency MQTT event processing instead of waiting for select() timeout if CORE.is_esp32: socket.require_wake_loop_threadsafe() + # Re-enable ESP-IDF's mqtt component (excluded by default to save compile time) + include_idf_component("mqtt") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index c77217243c..7e3de1df15 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -1,7 +1,12 @@ from esphome import pins import esphome.codegen as cg from esphome.components import light -from esphome.components.esp32 import VARIANT_ESP32C3, VARIANT_ESP32S3, get_esp32_variant +from esphome.components.esp32 import ( + VARIANT_ESP32C3, + VARIANT_ESP32S3, + get_esp32_variant, + include_idf_component, +) import esphome.config_validation as cv from esphome.const import ( CONF_CHANNEL, @@ -205,6 +210,10 @@ async def to_code(config): has_white = "W" in config[CONF_TYPE] method = config[CONF_METHOD] + # Re-enable ESP-IDF's RMT driver if using RMT method (excluded by default) + if CORE.is_esp32 and method[CONF_TYPE] == METHOD_ESP32_RMT: + include_idf_component("esp_driver_rmt") + method_template = METHODS[method[CONF_TYPE]].to_code( method, config[CONF_VARIANT], config[CONF_INVERT] ) diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index f5d89f2f0f..7d0634714c 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -170,6 +170,9 @@ CONFIG_SCHEMA = remote_base.validate_triggers( async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: + # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) + esp32.include_idf_component("esp_driver_rmt") + var = cg.new_Pvariable(config[CONF_ID], pin) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS])) diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index f182a1ec0d..46f4155234 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -112,6 +112,9 @@ async def digital_write_action_to_code(config, action_id, template_arg, args): async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: + # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) + esp32.include_idf_component("esp_driver_rmt") + var = cg.new_Pvariable(config[CONF_ID], pin) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING]))