From 6d1cd3f62eae3cf1a4ed55c474e0c925ec90a5de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Aug 2026 21:17:21 -0500 Subject: [PATCH] [esp32] Intercept the Log V2 formatter with linker wrap A plain strong definition of esp_log_format collides with liblog's copy on IDF 6.1: log.c.obj references the symbol and the linker resolves it from log_format_text.c.obj inside the same archive before reaching ESPHome's archive, producing a duplicate definition error. Use -Wl,--wrap=esp_log_format and define __wrap_esp_log_format instead, which also lets the unused liblog formatter be dead stripped. --- esphome/components/esp32/__init__.py | 4 ++++ esphome/core/log.cpp | 22 +++++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f8465772d5..f24c49e0ca 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2697,6 +2697,10 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_LOG_VERSION_2", True) add_idf_sdkconfig_option("CONFIG_LOG_API_CONSTRAINED_ENV_SAFE", False) cg.add_define("USE_ESP32_LOG_V2") + # Intercept liblog's formatter via --wrap; a plain strong definition + # collides instead of overriding because liblog resolves the symbol + # within its own archive (see core/log.cpp). + cg.add_build_flag("-Wl,--wrap=esp_log_format") # Reduce PHY TX power in the event of a brownout add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index d0ce297345..346c68eaa3 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -85,8 +85,11 @@ int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT // Override esp_log_format to prevent V2's 3-call vprintf fragmentation. // Without this, Log V2 calls the vprintf hook 3 times per message (header, // body, newline) which creates 3 separate log entries in ESPHome's logger. -// This strong definition overrides the archive symbol from ESP-IDF's liblog, -// affecting all callers including precompiled blobs (e.g. wifi). +// Interception uses the linker's --wrap (added in esp32/__init__.py when Log +// V2 is enabled): a plain strong definition cannot win here because liblog's +// log.c.obj references esp_log_format and the linker resolves it from +// log_format_text.c.obj within the same archive, before ever reaching +// ESPHome's archive, which then collides as a duplicate definition. #include #include @@ -136,13 +139,14 @@ static void __attribute__((noinline)) esp_log_format_direct_(esp_log_msg_t *mess } extern "C" { -// Override esp_log_format from liblog.a to prevent V2's 3-call vprintf -// fragmentation. Deliberately NOT IRAM_ATTR: every caller that must work with -// flash cache disabled (ESP_DRAM_LOGx, ESP_EARLY_LOGx) bypasses esp_log() -// entirely under CONFIG_LOG_API_CONSTRAINED_ENV_SAFE=n, and both paths below -// immediately call flash-resident code anyway, so IRAM placement would only -// spend the IRAM this change exists to save. -void esp_log_format(esp_log_msg_t *message) { +// Wrap of esp_log_format from liblog.a (via -Wl,--wrap=esp_log_format) to +// prevent V2's 3-call vprintf fragmentation. Deliberately NOT IRAM_ATTR: every +// caller that must work with flash cache disabled (ESP_DRAM_LOGx, +// ESP_EARLY_LOGx) bypasses esp_log() entirely under +// CONFIG_LOG_API_CONSTRAINED_ENV_SAFE=n, and both paths below immediately call +// flash-resident code anyway, so IRAM placement would only spend the IRAM this +// change exists to save. +void __wrap_esp_log_format(esp_log_msg_t *message) { // NOLINT extern vprintf_like_t esp_log_vprint_func; extern int vprintf(const char *, __gnuc_va_list); // NOLINT if (esp_log_vprint_func == &vprintf || message->config.opts.constrained_env) [[unlikely]] {