diff --git a/components/esp_common/project_include.cmake b/components/esp_common/project_include.cmake index b50959c509b..d4e1e5d0fec 100644 --- a/components/esp_common/project_include.cmake +++ b/components/esp_common/project_include.cmake @@ -4,11 +4,7 @@ idf_build_get_property(target IDF_TARGET) if(NOT (${target} STREQUAL "linux" OR CMAKE_C_COMPILER_ID MATCHES "Clang")) - execute_process( - COMMAND ${CMAKE_C_COMPILER} -dumpmachine - OUTPUT_VARIABLE toolchain_name - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET) + __compiler_query(toolchain_name ${CMAKE_C_COMPILER} -dumpmachine) check_expected_tool_version(${toolchain_name} ${CMAKE_C_COMPILER}) endif() diff --git a/components/xtensa/project_include.cmake b/components/xtensa/project_include.cmake index acdb7d66f29..5a15672f417 100644 --- a/components/xtensa/project_include.cmake +++ b/components/xtensa/project_include.cmake @@ -7,20 +7,13 @@ endif() if(CMAKE_C_COMPILER_ID MATCHES "Clang") # without '--target' option 'clang -dumpmachine' prints default target arch and it might be not Xtensa # so use `-print-targets` option - execute_process( - COMMAND ${CMAKE_C_COMPILER} -print-targets - OUTPUT_VARIABLE dump_machine - ) + __compiler_query(dump_machine ${CMAKE_C_COMPILER} -print-targets) else() - execute_process( - COMMAND ${CMAKE_C_COMPILER} -dumpmachine - OUTPUT_VARIABLE dump_machine - OUTPUT_STRIP_TRAILING_WHITESPACE - ) + __compiler_query(dump_machine ${CMAKE_C_COMPILER} -dumpmachine) endif() message(STATUS "Compiler supported targets: ${dump_machine}") -if(NOT (${CMAKE_SYSTEM_NAME} STREQUAL "Generic" AND ${dump_machine} MATCHES xtensa)) +if(NOT ("${CMAKE_SYSTEM_NAME}" STREQUAL "Generic" AND "${dump_machine}" MATCHES "xtensa")) # Sub-projects (e.g. ULP RISC-V on an xtensa target) may use a # non-IDF toolchain provided by the parent build. Skip validation # when a custom toolchain is explicitly declared. diff --git a/tools/cmake/compiler_query.cmake b/tools/cmake/compiler_query.cmake new file mode 100644 index 00000000000..f9c51bee600 --- /dev/null +++ b/tools/cmake/compiler_query.cmake @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +# __compiler_query +# +# Run a compiler query command (e.g. " -dumpmachine") given in ARGN +# and store its trimmed stdout in the variable named by "output_var". +# +# Fails with an actionable error if the command fails or returns no output. On +# some Windows systems, antivirus, endpoint-security or DLP/encryption software +# intercepts short-lived toolchain processes and strips their stdout when it is +# captured by the build system, while the same command works when run directly +# in a terminal. Without this guard the empty result collapses the callers' +# parsing into a cryptic CMake error (see +# https://github.com/espressif/esp-idf/issues/18727). +# +# This module is included by both the cmakev1 and cmakev2 utilities, because the +# esp_common and xtensa project_include.cmake files that call __compiler_query +# are shared by both build systems. +function(__compiler_query output_var) + execute_process( + COMMAND ${ARGN} + OUTPUT_VARIABLE query_output + RESULT_VARIABLE query_result + OUTPUT_STRIP_TRAILING_WHITESPACE) + + if(NOT query_result EQUAL 0 OR query_output STREQUAL "") + string(REPLACE ";" " " query_command "${ARGN}") + message(FATAL_ERROR + "Failed to query the compiler: '${query_command}' (result: ${query_result}).\n" + "The command produced no output when run by the build system. This is usually caused " + "by antivirus, endpoint-security or DLP/encryption software intercepting the compiler " + "process and discarding its output; the same command often works when run directly in " + "a terminal.\n" + "Add an exclusion for the ESP-IDF tools directory in that software (on a managed " + "machine you may need your IT department), then run 'idf.py fullclean' and build again.") + endif() + + set(${output_var} "${query_output}" PARENT_SCOPE) +endfunction() diff --git a/tools/cmake/utilities.cmake b/tools/cmake/utilities.cmake index 9584098dcfa..239155ae795 100644 --- a/tools/cmake/utilities.cmake +++ b/tools/cmake/utilities.cmake @@ -502,3 +502,9 @@ function(add_deprecated_target_alias old_target new_target) ) add_dependencies(${old_target} ${new_target}) endfunction() + + +# __compiler_query is defined in a standalone module so it can also be included +# by the cmakev2 utilities; the esp_common and xtensa project_include.cmake +# files that call it are shared by both build systems. +include(${CMAKE_CURRENT_LIST_DIR}/compiler_query.cmake) diff --git a/tools/cmakev2/utilities.cmake b/tools/cmakev2/utilities.cmake index a077ccb5f0b..a914f2057a9 100644 --- a/tools/cmakev2/utilities.cmake +++ b/tools/cmakev2/utilities.cmake @@ -7,6 +7,10 @@ # used by toolchain CMake files. include(${CMAKE_CURRENT_LIST_DIR}/../cmake/deduplicate_flags.cmake) +# __compiler_query is shared with cmakev1. The esp_common and xtensa +# project_include.cmake files that call it are used by both build systems. +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/compiler_query.cmake) + # Note: CMake does not support nested lists. The functions idf_die, idf_warn, # idf_msg, and idf_dbg use ARGV# values because this is the only way to prevent # arguments from being altered by CMake. ARGV and ARGN contain a flattened list diff --git a/tools/idf_tools.py b/tools/idf_tools.py index 802bddad9b0..40b3cdacc1e 100755 --- a/tools/idf_tools.py +++ b/tools/idf_tools.py @@ -998,7 +998,25 @@ class IDFTool: f'non-zero exit code ({e.returncode}) with message: {e.stderr.decode("utf-8", errors="ignore")}' ) # type: ignore - return self.parse_tool_version(version_cmd_result.decode('utf-8')) + version_str = version_cmd_result.decode('utf-8') + if not version_str.strip(): + # The tool ran and exited successfully, but produced no output when its + # output was captured. On some Windows systems, antivirus, endpoint-security + # or DLP/encryption software intercepts short-lived toolchain processes and + # strips their stdout when it is captured through a pipe, while the same + # command works when run directly in a terminal. Surface an actionable hint + # instead of silently reporting the version as 'unknown', which otherwise + # sends users into a fruitless reinstall loop. + # See https://github.com/espressif/esp-idf/issues/18727 + warn( + f'tool {self.name} ran but returned no version output. This is usually caused by ' + 'antivirus, endpoint-security or DLP/encryption software stripping the output of ' + 'toolchain processes; the same command often works when run directly in a terminal. ' + 'Add an exclusion for the ESP-IDF tools directory in that software. If the problem ' + 'persists, run the tool manually to check for a missing DLL.' + ) + return UNKNOWN_VERSION + return self.parse_tool_version(version_str) def get_version_from_file(self, version: str) -> str: """ diff --git a/tools/ldgen/ldgen.py b/tools/ldgen/ldgen.py index 13e93d94af0..3e7934e06a9 100755 --- a/tools/ldgen/ldgen.py +++ b/tools/ldgen/ldgen.py @@ -142,6 +142,40 @@ def _update_environment(env, env_file): os.environ.update(env_vars) +def _run_objdump(objdump, library): + """Run ``objdump -h`` on a library and return its output. + + On some Windows systems, antivirus, endpoint-security or DLP/encryption + software intercepts short-lived toolchain processes and strips their output + when the build captures it, so objdump exits successfully but returns empty + output, while the same command works when run by hand. The output is read + through a pipe so that this empty result is reliably detectable: it is + rejected here with an actionable error instead of being fed to the parser + (which would otherwise report it as a confusing pyparsing error). Capturing + to a file is deliberately avoided: it would not be guaranteed complete + either, and a truncated-but-non-empty result could be parsed into a wrong + linker script instead of failing. See + https://github.com/espressif/esp-idf/issues/18665 and + https://github.com/espressif/esp-idf/issues/18727. + """ + new_env = os.environ.copy() + # Force the C locale so objdump emits the English 'In archive' header that + # the section parser expects, regardless of the host locale (see + # https://github.com/espressif/esp-idf/issues/7903). + new_env['LC_ALL'] = 'C' + + output = subprocess.check_output([objdump, '-h', library], env=new_env).decode() + if not output.strip(): + raise LdGenFailure( + f"'{objdump} -h {library}' ran successfully but returned no output. The toolchain ran " + 'but its output was empty when captured by the build system. This is usually caused by ' + 'antivirus, endpoint-security or DLP/encryption software stripping the output of ' + 'toolchain processes; the same command often works when run directly in a terminal. ' + 'Add an exclusion for the ESP-IDF tools directory in that software, then build again.' + ) + return output + + def _run( input_file, fragments, @@ -181,11 +215,22 @@ def _run( for library in libraries_file: library = library.strip() if library: - new_env = os.environ.copy() - new_env['LC_ALL'] = 'C' - dump = StringIO(subprocess.check_output([objdump, '-h', library], env=new_env).decode()) + dump = StringIO(_run_objdump(objdump, library)) dump.name = library - sections_infos.add_sections_info(dump) + try: + sections_infos.add_sections_info(dump) + except ParseException as e: + # Non-empty but unparsable section info (for example truncated or + # corrupted toolchain output) is reported here rather than allowed to + # propagate as a raw pyparsing traceback. The same root cause as the + # empty case in _run_objdump applies. + raise LdGenFailure( + f'failed to parse section information from {library}. The toolchain output ' + 'is incomplete or corrupted. This can be caused by antivirus, ' + 'endpoint-security or DLP/encryption software tampering with the output of ' + 'toolchain processes; the same command often works when run directly in a ' + f'terminal. Add an exclusion for the ESP-IDF tools directory, then build again.\n{e}' + ) # Check if we can skip generation entirely — section names and other # inputs unchanged since last run.