fix(tools): Windows builds when paths contain spaces (prefix_map, ldgen, CI)

- Harden prefix_map.cmake for paths with spaces on Windows

- ldgen: fragments-list-file, list normalization, CMake integration (tools/cmake + cmakev2)

- CI exclude list for check_tools; test_spaces bundle tweak

Made-with: Cursor
This commit is contained in:
Jakub Kocka
2026-06-04 14:45:05 +08:00
parent 424a685692
commit 2e5d930e6b
8 changed files with 80 additions and 21 deletions
+2 -2
View File
@@ -76,7 +76,7 @@ pytest_build_system_win:
extends: extends:
- .test_build_system_template_win - .test_build_system_template_win
- .rules:labels:windows_pytest_build_system - .rules:labels:windows_pytest_build_system
parallel: 10 parallel: 6
pytest_build_system_win_minimal_cmake: pytest_build_system_win_minimal_cmake:
extends: extends:
@@ -102,7 +102,7 @@ pytest_buildv2_system_win:
extends: extends:
- .test_build_system_template_win - .test_build_system_template_win
- .rules:labels:buildv2 - .rules:labels:buildv2
parallel: 10 parallel: 6
script: script:
- cd tools\test_build_system - cd tools\test_build_system
- idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME} - idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
+12 -2
View File
@@ -187,7 +187,7 @@ function(__ldgen_create_target exe_target)
"${build_dir}/ldgen_libraries.in" "${build_dir}/ldgen_libraries.in"
"${build_dir}/ldgen_libraries") "${build_dir}/ldgen_libraries")
idf_build_get_property(ldgen_fragment_files __LDGEN_FRAGMENT_FILES GENERATOR_EXPRESSION) idf_build_get_property(ldgen_fragment_files __LDGEN_FRAGMENT_FILES)
# Create command to invoke the linker script generator tool. # Create command to invoke the linker script generator tool.
idf_build_get_property(sdkconfig SDKCONFIG) idf_build_get_property(sdkconfig SDKCONFIG)
@@ -214,11 +214,21 @@ function(__ldgen_create_target exe_target)
set(mutable_libs_option "--mutable-libraries-file" "${mutable_libs_path}") set(mutable_libs_option "--mutable-libraries-file" "${mutable_libs_path}")
endif() endif()
# Write fragment paths to a file (one per line) and pass the file to ldgen,
# mirroring the --libraries-file pattern. Avoids cmd/CRT argv-quoting of a
# long semicolon-separated list with paths containing spaces and backslashes
# on Windows (test_spaces_bundle4).
set(ldgen_fragments_file "${build_dir}/ldgen_fragments")
list(JOIN ldgen_fragment_files "\n" ldgen_fragments_str)
file(WRITE "${ldgen_fragments_file}" "${ldgen_fragments_str}")
set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
APPEND PROPERTY ADDITIONAL_CLEAN_FILES
"${ldgen_fragments_file}")
add_custom_command( add_custom_command(
OUTPUT ${output} OUTPUT ${output}
COMMAND ${python} "${idf_path}/tools/ldgen/ldgen.py" COMMAND ${python} "${idf_path}/tools/ldgen/ldgen.py"
--config "${sdkconfig}" --config "${sdkconfig}"
--fragments-list "${ldgen_fragment_files}" --fragments-list-file "${ldgen_fragments_file}"
--input "${template}" --input "${template}"
--output "${output}" --output "${output}"
--kconfig "${root_kconfig}" --kconfig "${root_kconfig}"
+29 -13
View File
@@ -5,6 +5,10 @@
# to fixed names. This is used when reproducible builds are required. # to fixed names. This is used when reproducible builds are required.
# This function also creates a gdbinit file for the debugger to # This function also creates a gdbinit file for the debugger to
# remap the substituted paths back to the real paths in the filesystem. # remap the substituted paths back to the real paths in the filesystem.
#
# On Windows hosts, very long compile commands are avoided by enabling Ninja response
# files in toolchain.cmake (CMAKE_NINJA_FORCE_RESPONSE_FILE), not by dropping these
# options when paths contain spaces.
function(__generate_prefix_map compile_options_var) function(__generate_prefix_map compile_options_var)
set(compile_options) set(compile_options)
set(gdbinit_dir ${BUILD_DIR}/gdbinit) set(gdbinit_dir ${BUILD_DIR}/gdbinit)
@@ -13,27 +17,38 @@ function(__generate_prefix_map compile_options_var)
idf_build_get_property(build_components BUILD_COMPONENTS) idf_build_get_property(build_components BUILD_COMPONENTS)
if(CONFIG_COMPILER_HIDE_PATHS_MACROS) if(CONFIG_COMPILER_HIDE_PATHS_MACROS)
list(APPEND compile_options "-fmacro-prefix-map=${CMAKE_SOURCE_DIR}=.") file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" _src_dir)
list(APPEND compile_options "-fmacro-prefix-map=${idf_path}=/IDF") file(TO_CMAKE_PATH "${idf_path}" _idf_path)
list(APPEND compile_options "-fmacro-prefix-map=${_src_dir}=.")
list(APPEND compile_options "-fmacro-prefix-map=${_idf_path}=/IDF")
endif() endif()
if(CONFIG_APP_REPRODUCIBLE_BUILD) if(CONFIG_APP_REPRODUCIBLE_BUILD)
list(APPEND compile_options "-fdebug-prefix-map=${idf_path}=/IDF") file(TO_CMAKE_PATH "${idf_path}" _idf_path)
list(APPEND compile_options "-fdebug-prefix-map=${PROJECT_DIR}=/IDF_PROJECT") file(TO_CMAKE_PATH "${PROJECT_DIR}" _project_dir)
list(APPEND compile_options "-fdebug-prefix-map=${BUILD_DIR}=/IDF_BUILD") file(TO_CMAKE_PATH "${BUILD_DIR}" _build_dir)
list(APPEND compile_options "-fdebug-prefix-map=${_idf_path}=/IDF")
list(APPEND compile_options "-fdebug-prefix-map=${_project_dir}=/IDF_PROJECT")
list(APPEND compile_options "-fdebug-prefix-map=${_build_dir}=/IDF_BUILD")
# Generate mapping for component paths
set(gdbinit_file_lines) set(gdbinit_file_lines)
string(APPEND gdbinit_file_lines "set substitute-path /IDF ${idf_path}\n")
string(APPEND gdbinit_file_lines "set substitute-path /IDF_PROJECT ${PROJECT_DIR}\n")
string(APPEND gdbinit_file_lines "set substitute-path /IDF_BUILD ${BUILD_DIR}\n")
# Map components outside IDF_PATH to stable synthetic paths (reproducible builds).
foreach(component_name ${build_components}) foreach(component_name ${build_components})
idf_component_get_property(component_dir ${component_name} COMPONENT_DIR) idf_component_get_property(component_dir ${component_name} COMPONENT_DIR)
file(TO_CMAKE_PATH "${component_dir}" _component_dir)
string(TOUPPER ${component_name} component_name_uppercase) file(RELATIVE_PATH _component_rel "${_idf_path}" "${_component_dir}")
set(substituted_path "/COMPONENT_${component_name_uppercase}_DIR") if(_component_rel MATCHES "^\\.\\.(/|$)")
list(APPEND compile_options "-fdebug-prefix-map=${component_dir}=${substituted_path}") string(TOUPPER ${component_name} component_name_uppercase)
string(APPEND gdbinit_file_lines "set substitute-path ${substituted_path} ${component_dir}\n") set(substituted_path "/COMPONENT_${component_name_uppercase}_DIR")
list(APPEND compile_options "-fdebug-prefix-map=${_component_dir}=${substituted_path}")
string(APPEND gdbinit_file_lines "set substitute-path ${substituted_path} ${component_dir}\n")
endif()
endforeach() endforeach()
# Mapping for toolchain path
execute_process( execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-sysroot COMMAND ${CMAKE_C_COMPILER} -print-sysroot
OUTPUT_VARIABLE compiler_sysroot OUTPUT_VARIABLE compiler_sysroot
@@ -43,7 +58,8 @@ function(__generate_prefix_map compile_options_var)
endif() endif()
string(STRIP "${compiler_sysroot}" compiler_sysroot) string(STRIP "${compiler_sysroot}" compiler_sysroot)
get_filename_component(compiler_sysroot "${compiler_sysroot}/.." REALPATH) get_filename_component(compiler_sysroot "${compiler_sysroot}/.." REALPATH)
list(APPEND compile_options "-fdebug-prefix-map=${compiler_sysroot}=/TOOLCHAIN") file(TO_CMAKE_PATH "${compiler_sysroot}" _compiler_sysroot)
list(APPEND compile_options "-fdebug-prefix-map=${_compiler_sysroot}=/TOOLCHAIN")
string(APPEND gdbinit_file_lines "set substitute-path /TOOLCHAIN ${compiler_sysroot}\n") string(APPEND gdbinit_file_lines "set substitute-path /TOOLCHAIN ${compiler_sysroot}\n")
else() else()
set(gdbinit_file_lines "# There is no prefix map defined for the project.\n") set(gdbinit_file_lines "# There is no prefix map defined for the project.\n")
+10
View File
@@ -15,6 +15,16 @@ file(TO_CMAKE_PATH "${_current_toolchain_dir}" _current_toolchain_dir)
set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_NAME Generic)
# Windows CreateProcess enforces a short command line (~32k). IDF compile lines can be
# huge (includes, reproducible-build prefix maps, etc.). Tell the Ninja generator to
# pass compiler arguments via response files (CMake 3.15+). This is orthogonal to the
# early-stage @cflags/@cxxflags response files below, which only cover CMAKE_*_FLAGS.
if(CMAKE_HOST_WIN32 AND CMAKE_GENERATOR MATCHES "Ninja"
AND NOT DEFINED CACHE{CMAKE_NINJA_FORCE_RESPONSE_FILE})
set(CMAKE_NINJA_FORCE_RESPONSE_FILE ON CACHE BOOL
"Use Ninja response files on Windows hosts (avoid CreateProcess command-line limit).")
endif()
# Set compiler tools according to the toolchain type # Set compiler tools according to the toolchain type
string(FIND "${_toolchain_filename}" "clang" found_clang) string(FIND "${_toolchain_filename}" "clang" found_clang)
if(NOT found_clang EQUAL -1) if(NOT found_clang EQUAL -1)
+11 -1
View File
@@ -94,11 +94,21 @@ function(__ldgen_process_template)
set(mutable_libs_option "--mutable-libraries-file" "${mutable_libs_path}") set(mutable_libs_option "--mutable-libraries-file" "${mutable_libs_path}")
endif() endif()
# Write fragment paths to a file (one per line) and pass the file to ldgen,
# mirroring the --libraries-file pattern. Avoids cmd/CRT argv-quoting of a
# long semicolon-separated list with paths containing spaces and backslashes
# on Windows (test_spaces_bundle4).
set(ldgen_fragments_file "${build_dir}/ldgen_fragments${ARG_SUFFIX}")
list(JOIN ldgen_fragment_files "\n" ldgen_fragments_str)
file(WRITE "${ldgen_fragments_file}" "${ldgen_fragments_str}")
set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
APPEND PROPERTY ADDITIONAL_CLEAN_FILES
"${ldgen_fragments_file}")
add_custom_command( add_custom_command(
OUTPUT "${ARG_OUTPUT}" OUTPUT "${ARG_OUTPUT}"
COMMAND ${python} "${idf_path}/tools/ldgen/ldgen.py" COMMAND ${python} "${idf_path}/tools/ldgen/ldgen.py"
--config "${sdkconfig}" --config "${sdkconfig}"
--fragments-list "${ldgen_fragment_files}" --fragments-list-file "${ldgen_fragments_file}"
--input "${ARG_TEMPLATE}" --input "${ARG_TEMPLATE}"
--output "${ARG_OUTPUT}" --output "${ARG_OUTPUT}"
--kconfig "${root_kconfig}" --kconfig "${root_kconfig}"
+8
View File
@@ -144,6 +144,12 @@ def main():
'--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
) )
fragments_group.add_argument(
'--fragments-list-file',
type=argparse.FileType('r'),
help='File containing fragment file paths, one per line',
)
argparser.add_argument( argparser.add_argument(
'--libraries-file', type=argparse.FileType('r'), help='File that contains the list of libraries in the build' '--libraries-file', type=argparse.FileType('r'), help='File that contains the list of libraries in the build'
) )
@@ -201,6 +207,8 @@ def main():
fragment_files = [] fragment_files = []
if args.fragments_list: if args.fragments_list:
fragment_files = args.fragments_list.split(';') fragment_files = args.fragments_list.split(';')
elif args.fragments_list_file:
fragment_files = [line.strip() for line in args.fragments_list_file if line.strip()]
elif args.fragments: elif args.fragments:
fragment_files = args.fragments fragment_files = args.fragments
@@ -227,10 +227,13 @@ class TestIdfRootDependency:
'#include "mdns.h"', '#include "mdns.h"',
) )
# Intentional dependency on the cmake-level name of the managed component because
# idf_extra_components.yml installs espressif/mdns and we verify REQUIRES pulls
# espressif__mdns into the build — not something application code should do.
replace_in_file( replace_in_file(
(test_app_copy / 'main' / 'CMakeLists.txt'), (test_app_copy / 'main' / 'CMakeLists.txt'),
'# placeholder_inside_idf_component_register', '# placeholder_inside_idf_component_register',
'REQUIRES mdns', 'REQUIRES espressif__mdns',
) )
idf_py('build') idf_py('build')
@@ -251,10 +254,13 @@ class TestIdfRootDependency:
assert 'espressif__mdns' not in data['build_components'] assert 'espressif__mdns' not in data['build_components']
assert 'example__cmp' not in data['build_components'] assert 'example__cmp' not in data['build_components']
# Intentional dependency on the cmake-level name of the managed component because
# we assert espressif__mdns enters build_components only after REQUIRES — not
# something application code should do.
replace_in_file( replace_in_file(
(test_app_copy / 'main' / 'CMakeLists.txt'), (test_app_copy / 'main' / 'CMakeLists.txt'),
'# placeholder_inside_idf_component_register', '# placeholder_inside_idf_component_register',
'REQUIRES mdns', 'REQUIRES espressif__mdns',
) )
idf_py('reconfigure') idf_py('reconfigure')
-1
View File
@@ -68,7 +68,6 @@ def test_spaces_bundle3(idf_copy: Path) -> None:
) )
@pytest.mark.xfail(sys.platform == 'win32', reason='Bug with reproducible build')
# Use this bundle for tests which can be done with the default build_test_app # Use this bundle for tests which can be done with the default build_test_app
@pytest.mark.parametrize( @pytest.mark.parametrize(
'dummy_', 'dummy_',