Merge branch 'fix/harden_empty_toolchain_output_v5.5' into 'release/v5.5'

fix: harden build against empty toolchain output (v5.5)

See merge request espressif/esp-idf!51645
This commit is contained in:
Roland Dobai
2026-08-24 13:27:43 +02:00
7 changed files with 161 additions and 77 deletions
+1 -5
View File
@@ -2,11 +2,7 @@
# Warn if the toolchain version doesn't match
#
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()
+3 -10
View File
@@ -2,20 +2,13 @@
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"))
message(FATAL_ERROR "Internal error, toolchain has not been set correctly by project "
"(or an invalid CMakeCache.txt file has been generated somehow)")
endif()
+4 -4
View File
@@ -4,10 +4,10 @@
# SPDX-License-Identifier: Apache-2.0
import argparse
import subprocess
from sys import exit
import sys
try:
from typing import List
from typing import List # noqa: F401
except ImportError:
# Only used for type annotations
pass
@@ -29,7 +29,7 @@ def types_valid_ignored_rules(file_name): # type: (str) -> bool
"""
Run Mypy check with rules for ignore list on the given file, return TRUE if Mypy check passes
"""
mypy_exit_code = subprocess.call('mypy {} --python-version 3.8 --allow-untyped-defs'.format(file_name), shell=True)
mypy_exit_code = subprocess.call('mypy {} --python-version 3.9 --allow-untyped-defs'.format(file_name), shell=True)
return not bool(mypy_exit_code)
@@ -84,7 +84,7 @@ def main(): # type: () -> None
print('mypy check failed for:')
for file_name in type_issues:
print('\t', file_name)
exit(1)
sys.exit(1)
if __name__ == '__main__':
+40
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 the build system utilities, so that
# __compiler_query is available to the esp_common and xtensa
# project_include.cmake files that call it.
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()
+6
View File
@@ -393,3 +393,9 @@ function(remove_duplicated_flags FLAGS UNIQFLAGS)
# Return that string to the caller
set(${UNIQFLAGS} "${FLAGS_LIST}" PARENT_SCOPE)
endfunction()
# __compiler_query is defined in a standalone module, included here so it is
# available to the esp_common and xtensa project_include.cmake files that call
# it.
include(${CMAKE_CURRENT_LIST_DIR}/compiler_query.cmake)
+19 -1
View File
@@ -1004,7 +1004,25 @@ class IDFTool(object):
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:
"""
+88 -57
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python
#
# SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
import argparse
@@ -23,7 +23,7 @@ from pyparsing import ParseFatalException
def _update_environment(args):
env = [(name, value) for (name,value) in (e.split('=',1) for e in args.env)]
env = [(name, value) for (name, value) in (e.split('=', 1) for e in args.env)]
for name, value in env:
value = ' '.join(value.split())
os.environ[name] = value
@@ -33,72 +33,90 @@ def _update_environment(args):
os.environ.update(env)
def main():
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 main():
argparser = argparse.ArgumentParser(description='ESP-IDF linker script generator')
argparser.add_argument(
'--input', '-i',
help='Linker template file',
type=argparse.FileType('r'))
argparser.add_argument('--input', '-i', help='Linker template file', type=argparse.FileType('r'))
fragments_group = argparser.add_mutually_exclusive_group()
fragments_group.add_argument(
'--fragments', '-f',
type=argparse.FileType('r'),
help='Input fragment files',
nargs='+'
'--fragments', '-f', type=argparse.FileType('r'), help='Input fragment files', nargs='+'
)
fragments_group.add_argument(
'--fragments-list',
help='Input fragment files as a semicolon-separated list',
type=str
'--fragments-list', help='Input fragment files as a semicolon-separated list', type=str
)
argparser.add_argument(
'--libraries-file',
'--libraries-file', type=argparse.FileType('r'), help='File that contains the list of libraries in the build'
)
argparser.add_argument('--output', '-o', help='Output linker script', type=str)
argparser.add_argument('--config', '-c', help='Project configuration')
argparser.add_argument('--kconfig', '-k', help='IDF Kconfig file')
argparser.add_argument(
'--check-mapping', help='Perform a check if a mapping (archive, obj, symbol) exists', action='store_true'
)
argparser.add_argument(
'--check-mapping-exceptions', help='Mappings exempted from check', type=argparse.FileType('r')
)
argparser.add_argument(
'--env',
'-e',
action='append',
default=[],
help='Environment to set when evaluating the config file',
metavar='NAME=VAL',
)
argparser.add_argument(
'--env-file',
type=argparse.FileType('r'),
help='File that contains the list of libraries in the build')
argparser.add_argument(
'--output', '-o',
help='Output linker script',
type=str)
argparser.add_argument(
'--config', '-c',
help='Project configuration')
argparser.add_argument(
'--kconfig', '-k',
help='IDF Kconfig file')
argparser.add_argument(
'--check-mapping',
help='Perform a check if a mapping (archive, obj, symbol) exists',
action='store_true'
help='Optional file to load environment variables from. Contents '
'should be a JSON object where each key/value pair is a variable.',
)
argparser.add_argument(
'--check-mapping-exceptions',
help='Mappings exempted from check',
type=argparse.FileType('r')
)
argparser.add_argument(
'--env', '-e',
action='append', default=[],
help='Environment to set when evaluating the config file', metavar='NAME=VAL')
argparser.add_argument('--env-file', type=argparse.FileType('r'),
help='Optional file to load environment variables from. Contents '
'should be a JSON object where each key/value pair is a variable.')
argparser.add_argument(
'--objdump',
help='Path to toolchain objdump')
argparser.add_argument('--objdump', help='Path to toolchain objdump')
args = argparser.parse_args()
@@ -126,11 +144,22 @@ def main():
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}'
)
generation_model = Generation(check_mapping, check_mapping_exceptions)
@@ -165,7 +194,9 @@ def main():
if exc.errno != errno.EEXIST:
raise
with open(output_path, 'w', encoding='utf-8') as f: # only create output file after generation has succeeded
with open(
output_path, 'w', encoding='utf-8'
) as f: # only create output file after generation has succeeded
f.write(output.read())
except LdGenFailure as e:
print('linker script generation failed for %s\nERROR: %s' % (input_file.name, e))