fix: harden build against empty toolchain output

On some Windows systems, antivirus, endpoint-security or DLP/encryption
software intercepts short-lived toolchain processes and strips their
stdout when the build captures it through a pipe, while the same command
prints its output normally when run by hand. The tool exits successfully
but returns nothing, and each build step that reads toolchain output then
failed with a different, cryptic error far from the real cause:

- CMake configuration aborted with "Unknown arguments specified" in
  components/xtensa/project_include.cmake, or "check_expected_tool_version
  invoked with incorrect arguments" in components/esp_common.
- ldgen turned the empty objdump output into an opaque pyparsing
  "Expected 'In archive'" traceback.
- idf_tools.py silently reported the compiler/debugger version as
  "unknown", sending users into a fruitless reinstall loop.

Detect the empty result at each consumer and fail (or warn) with an
actionable message that names the likely cause and the remedy:

- tools/cmake/compiler_query.cmake: new __compiler_query() helper runs a
  compiler query and fails with a clear error on empty or failed output.
  It is a standalone module included by both the cmakev1 and cmakev2
  utilities, since the esp_common and xtensa project_include.cmake that
  call it are shared by both build systems. The xtensa if() arguments are
  now quoted so an empty result no longer collapses into a parse error.
- tools/ldgen/ldgen.py: _run_objdump() rejects empty objdump output, and
  non-empty-but-unparsable section info is caught and re-raised as a clear
  LdGenFailure instead of a raw pyparsing traceback.
- tools/idf_tools.py: empty version output now warns with the cause and
  returns UNKNOWN_VERSION instead of silently reporting "unknown".

Closes https://github.com/espressif/esp-idf/issues/18727

Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
This commit is contained in:
Frantisek Hrbata
2026-06-23 10:53:06 +02:00
committed by BOT
parent 6498d81eb4
commit 5c5d15cfc7
7 changed files with 122 additions and 20 deletions

View File

@@ -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. "<compiler> -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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:
"""

View File

@@ -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.